From c38f92314ee835f497122f6cce7c49346fe80aad Mon Sep 17 00:00:00 2001 From: philipp lang <philipp@aweos.de> Date: Thu, 11 Aug 2022 23:19:52 +0200 Subject: [PATCH] Add membersearch for contribution selection --- app/Contribution/ContributionController.php | 2 +- app/Member/Member.php | 10 + resources/js/components/FCheckbox.vue | 51 +++-- resources/js/components/FSwitch.vue | 5 +- resources/js/components/FText.vue | 213 +++++++++++++------- resources/js/views/contribution/VIndex.vue | 68 ++++++- 6 files changed, 248 insertions(+), 101 deletions(-) diff --git a/app/Contribution/ContributionController.php b/app/Contribution/ContributionController.php index 558203d2..63a8f5d2 100644 --- a/app/Contribution/ContributionController.php +++ b/app/Contribution/ContributionController.php @@ -18,7 +18,7 @@ class ContributionController extends Controller session()->put('title', 'Zuschüsse'); return Inertia::render('contribution/VIndex', [ - 'allMembers' => MemberResource::collection(Member::get()), + 'allMembers' => MemberResource::collection(Member::slangOrdered()->get()), ]); } diff --git a/app/Member/Member.php b/app/Member/Member.php index 4bcacc0b..d67d8247 100644 --- a/app/Member/Member.php +++ b/app/Member/Member.php @@ -194,6 +194,16 @@ class Member extends Model } // ---------------------------------- Scopes ----------------------------------- + public function scopeOrdered(Builder $q): Builder + { + return $q->orderByRaw('lastname, firstname'); + } + + public function scopeSlangOrdered(Builder $q): Builder + { + return $q->orderByRaw('firstname, lastname'); + } + public function scopeWithIsConfirmed(Builder $q): Builder { return $q->selectSub('DATEDIFF(NOW(), IFNULL(confirmed_at, DATE_SUB(NOW(), INTERVAL 3 YEAR))) < 712', 'is_confirmed'); diff --git a/resources/js/components/FCheckbox.vue b/resources/js/components/FCheckbox.vue index 8adbb97a..ecb188ed 100644 --- a/resources/js/components/FCheckbox.vue +++ b/resources/js/components/FCheckbox.vue @@ -1,14 +1,24 @@ <template> <label class="flex flex-col relative field-checkbox cursor-pointer" :for="id" :class="{[`size-${size}`]: true}"> - <span v-if="label && inset" class="z-10 absolute top-0 left-0 -mt-2 px-1 ml-3 inset-bg font-semibold text-gray-700">{{ label }}</span> + <span + v-if="label && inset" + class="z-10 absolute top-0 left-0 -mt-2 px-1 ml-3 inset-bg font-semibold text-gray-700" + >{{ label }}</span + > <div class="relative flex items-start"> <input :id="id" type="checkbox" v-model="v" :disabled="disabled" class="invisible absolute" /> <span class="display-wrapper flex items-center"> - <span class="relative cursor-pointer flex flex-none justify-center items-center display" :class="{'bg-terminoto-2': v === true, 'bg-white': v === false}"> + <span + class="relative cursor-pointer flex flex-none justify-center items-center display" + :class="{'bg-terminoto-2': v === true, 'bg-white': v === false}" + > <svg-sprite src="check" class="w-4 h-4 check-icon text-white"></svg-sprite> </span> </span> - <span v-if="label && !inset" class="text-sm leading-tight ml-3 text-gray-700 checkbox-label flex items-center"> + <span + v-if="label && !inset" + class="text-sm leading-tight ml-3 text-gray-700 checkbox-label flex items-center" + > <span> <span v-text="label" v-if="!html"></span> <span v-html="label" v-if="html"></span> @@ -23,41 +33,41 @@ export default { model: { prop: 'items', - event: 'input' + event: 'input', }, props: { html: { type: Boolean, - default: false + default: false, }, required: { type: Boolean, - default: false + default: false, }, inset: { type: Boolean, - default: false + default: false, }, size: { default: null, - required: false + required: false, }, id: { - required: true + required: true, }, disabled: { type: Boolean, - default: false + default: false, }, value: { - default: false + default: false, }, label: { - default: false + default: false, }, items: { - default: undefined - } + default: undefined, + }, }, computed: { v: { @@ -71,7 +81,7 @@ export default { return; } - var a = this.items.filter(i => i !== this.value); + var a = this.items.filter((i) => i !== this.value); if (v) { a.push(this.value); } @@ -88,15 +98,15 @@ export default { } return this.items.indexOf(this.value) !== -1; - } - } + }, + }, }, created() { if (typeof this.items === 'undefined') { this.$emit('input', false); } - } + }, }; </script> @@ -111,7 +121,8 @@ export default { transition: background 0.2s; } - .display-wrapper, .checkbox-label { + .display-wrapper, + .checkbox-label { min-height: 34px; } @@ -119,7 +130,7 @@ export default { width: var(--checkbox-width); height: var(--checkbox-width); border-radius: 0.3rem; - border: solid 2px hsl(60.0, 1.8%, 10.8%); + border: solid 2px hsl(60, 1.8%, 10.8%); .check-icon { opacity: 0; transition: opacity 0.2s; diff --git a/resources/js/components/FSwitch.vue b/resources/js/components/FSwitch.vue index 2dc13415..6f3afd6b 100644 --- a/resources/js/components/FSwitch.vue +++ b/resources/js/components/FSwitch.vue @@ -17,10 +17,11 @@ :value="value" v-model="v" :disabled="disabled" - class="invisible absolute" + class="absolute peer" + @keypress="$emit('keypress', $event)" /> <span - class="relative cursor-pointer flex grow display" + class="relative cursor-pointer peer-focus:bg-red-500 flex grow display" :class="{'bg-switch': v === true, 'bg-gray-700': v === false}" > <span diff --git a/resources/js/components/FText.vue b/resources/js/components/FText.vue index a8d16be1..5fb2d435 100644 --- a/resources/js/components/FText.vue +++ b/resources/js/components/FText.vue @@ -5,7 +5,18 @@ <span v-show="required" class="text-red-800"> *</span> </span> <div class="real-field-wrap" :class="`size-${size}`"> - <input :name="name" :type="type" :value="transformedValue" @input="onInput" @change="onChange" :disabled="disabled" :placeholder="placeholder" @focus="onFocus" @blur="onBlur"> + <input + @keypress="$emit('keypress', $event)" + :name="name" + :type="type" + :value="transformedValue" + @input="onInput" + @change="onChange" + :disabled="disabled" + :placeholder="placeholder" + @focus="onFocus" + @blur="onBlur" + /> <div v-if="hint" class="info-wrap"> <div v-tooltip="hint"> <svg-sprite src="info-button" class="info-button"></svg-sprite> @@ -28,7 +39,7 @@ var numb = { }, encoder(a) { return a / 100; - } + }, }), naturalRaw: wNumb({ mark: '', @@ -39,7 +50,7 @@ var numb = { }, encoder(a) { return a / 100; - } + }, }), naturalDetailRaw: wNumb({ mark: '', @@ -50,7 +61,7 @@ var numb = { }, encoder(a) { return a / 10000; - } + }, }), area: wNumb({ mark: ',', @@ -61,7 +72,7 @@ var numb = { }, encoder(a) { return a / 100; - } + }, }), areaDetail: wNumb({ mark: ',', @@ -72,7 +83,7 @@ var numb = { }, encoder(a) { return a / 10000; - } + }, }), twoDecimalRaw: wNumb({ mark: ',', @@ -83,7 +94,7 @@ var numb = { }, encoder(a) { return a / 100; - } + }, }), fourDecimalRaw: wNumb({ mark: ',', @@ -94,142 +105,198 @@ var numb = { }, encoder(a) { return a / 10000; - } - }) + }, + }), }; var transformers = { none: { display: { - to(v) { return v; }, - from(v) { return v; } + to(v) { + return v; + }, + from(v) { + return v; + }, }, edit: { - to(v) { return v; }, - from(v) { return v; } - } + to(v) { + return v; + }, + from(v) { + return v; + }, + }, }, natural: { display: { - to(v) { return isNaN(parseInt(v)) ? '' : numb.natural.to(v); }, - from(v) { return v === '' ? null : numb.natural.from(v); } + to(v) { + return isNaN(parseInt(v)) ? '' : numb.natural.to(v); + }, + from(v) { + return v === '' ? null : numb.natural.from(v); + }, }, edit: { - to(v) { return isNaN(parseInt(v)) ? '' : numb.naturalRaw.to(v); }, - from(v) { return v === '' ? null : numb.naturalRaw.from(v); } - } + to(v) { + return isNaN(parseInt(v)) ? '' : numb.naturalRaw.to(v); + }, + from(v) { + return v === '' ? null : numb.naturalRaw.from(v); + }, + }, }, area: { display: { - to(v) { return v === null ? '' : numb.area.to(v); }, - from(v) { return v === '' ? null : numb.area.from(v); } + to(v) { + return v === null ? '' : numb.area.to(v); + }, + from(v) { + return v === '' ? null : numb.area.from(v); + }, }, edit: { to(v) { - if (v === null) { return ''; } - if (Math.round(v / 100) * 100 === v) { return numb.naturalRaw.to(v); } + if (v === null) { + return ''; + } + if (Math.round(v / 100) * 100 === v) { + return numb.naturalRaw.to(v); + } return numb.twoDecimalRaw.to(v); }, from(v) { - if (v === '') { return null; } - if (v.indexOf(',') === -1) { return numb.naturalRaw.from(v); } + if (v === '') { + return null; + } + if (v.indexOf(',') === -1) { + return numb.naturalRaw.from(v); + } return numb.twoDecimalRaw.from(v); - } - } + }, + }, }, currency: { display: { - to(v) { return v === null ? '' : numb.area.to(v); }, - from(v) { return v === '' ? null : numb.area.from(v); } + to(v) { + return v === null ? '' : numb.area.to(v); + }, + from(v) { + return v === '' ? null : numb.area.from(v); + }, }, edit: { to(v) { - if (v === null) { return ''; } - if (Math.round(v / 100) * 100 === v) { return numb.naturalRaw.to(v); } + if (v === null) { + return ''; + } + if (Math.round(v / 100) * 100 === v) { + return numb.naturalRaw.to(v); + } return numb.twoDecimalRaw.to(v); }, from(v) { - if (v === '') { return null; } - if (v.indexOf(',') === -1) { return numb.naturalRaw.from(v); } + if (v === '') { + return null; + } + if (v.indexOf(',') === -1) { + return numb.naturalRaw.from(v); + } return numb.twoDecimalRaw.from(v); - } - } + }, + }, }, currencyDetail: { display: { - to(v) { return v === null ? '' : numb.areaDetail.to(v); }, - from(v) { return v === '' ? null : numb.areaDetail.from(v); } + to(v) { + return v === null ? '' : numb.areaDetail.to(v); + }, + from(v) { + return v === '' ? null : numb.areaDetail.from(v); + }, }, edit: { to(v) { - if (v === null) { return ''; } - if (Math.round(v / 10000) * 10000 === v) { return numb.naturalDetailRaw.to(v); } + if (v === null) { + return ''; + } + if (Math.round(v / 10000) * 10000 === v) { + return numb.naturalDetailRaw.to(v); + } return numb.fourDecimalRaw.to(v); }, from(v) { - if (v === '') { return null; } - if (v.indexOf(',') === -1) { return numb.naturalDetailRaw.from(v); } + if (v === '') { + return null; + } + if (v.indexOf(',') === -1) { + return numb.naturalDetailRaw.from(v); + } return numb.fourDecimalRaw.from(v); - } - } - } + }, + }, + }, }; export default { - data: function() { + data: function () { return { - focus: false + focus: false, }; }, props: { placeholder: { - default: function() { + default: function () { return ''; - } + }, }, default: {}, mode: { - default: function() { return 'none'; } + default: function () { + return 'none'; + }, }, required: { type: Boolean, - default: false + default: false, }, inset: { - default: function() { + default: function () { return null; - } + }, }, size: { - default: function() { + default: function () { return 'base'; - } + }, }, id: { - required: true + required: true, }, hint: { - default: null + default: null, }, value: { - default: undefined + default: undefined, }, mask: { - default: undefined + default: undefined, }, label: { - default: false + default: false, }, type: { required: false, - default: function() { return 'text'; } + default: function () { + return 'text'; + }, }, disabled: { default: false, - type: Boolean + type: Boolean, }, name: {}, }, @@ -249,7 +316,7 @@ export default { if (this.mode === 'none') { this.transformedValue = v.target.value; } - } + }, }, computed: { transformedValue: { @@ -258,25 +325,35 @@ export default { }, set(v) { this.$emit('input', transformers[this.mode][this.focus ? 'edit' : 'display'].from(v)); - } + }, }, insetClass() { - if (this.inset === '') { return 'bg-inset'; } - if (this.inset === undefined) { return null; } + if (this.inset === '') { + return 'bg-inset'; + } + if (this.inset === undefined) { + return null; + } return `bg-${this.inset}`; - } + }, }, created() { if (typeof this.value === 'undefined') { this.$emit('input', this.default === undefined ? '' : this.default); } - } + }, }; </script> <style scope> .bg-inset { - background: linear-gradient(to bottom, hsl(247.5, 66.7%, 97.6%) 0%, hsl(247.5, 66.7%, 97.6%) 41%, hsl(0deg 0% 100%) 41%, hsl(180deg 0% 100%) 100%); + background: linear-gradient( + to bottom, + hsl(247.5, 66.7%, 97.6%) 0%, + hsl(247.5, 66.7%, 97.6%) 41%, + hsl(0deg 0% 100%) 41%, + hsl(180deg 0% 100%) 100% + ); } </style> diff --git a/resources/js/views/contribution/VIndex.vue b/resources/js/views/contribution/VIndex.vue index 6c045dee..47733db3 100644 --- a/resources/js/views/contribution/VIndex.vue +++ b/resources/js/views/contribution/VIndex.vue @@ -18,16 +18,29 @@ required ></f-text> - <div class="col-span-2"> - <f-switch - :id="`members-${member.id}`" - :key="member.id" - :label="`${member.firstname} ${member.lastname}`" - v-for="member in allMembers" - name="members[]" - :value="member.id" - v-model="values.members" - ></f-switch> + <div class="border-gray-200 shadow shadow-primary-700 p-3 shadow-[0_0_4px_gray] col-span-2"> + <f-text + class="col-span-2" + id="membersearch" + name="membersearch" + v-model="membersearch" + label="Suchen …" + size="sm" + ref="membersearchfield" + @keypress.enter.prevent="onSubmitFirstMemberResult" + ></f-text> + <div class="mt-2 grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-2 col-span-2"> + <f-switch + :id="`members-${member.id}`" + :key="member.id" + :label="`${member.firstname} ${member.lastname}`" + v-for="member in memberResults" + name="members[]" + :value="member.id" + v-model="values.members" + @keypress.enter.prevent="onSubmitMemberResult(member)" + ></f-switch> + </div> </div> <button @@ -46,6 +59,7 @@ export default { data: function () { return { + membersearch: '', values: { members: [], event_name: '', @@ -57,5 +71,39 @@ export default { props: { allMembers: {}, }, + computed: { + memberResults() { + if (this.membersearch.length === 0) { + return this.allMembers; + } + + return this.allMembers.filter( + (member) => + (member.firstname + ' ' + member.lastname) + .toLowerCase() + .indexOf(this.membersearch.toLowerCase()) !== -1 + ); + }, + }, + methods: { + onSubmitMemberResult(selected) { + if (this.values.members.find((m) => m === selected.id) !== undefined) { + this.values.members = this.values.members.filter((m) => m === selected.id); + } else { + this.values.members.push(selected.id); + } + + this.membersearch = ''; + this.$refs.membersearchfield.$el.querySelector('input').focus(); + }, + onSubmitFirstMemberResult() { + if (this.memberResults.length === 0) { + this.membersearch = ''; + return; + } + + this.onSubmitMemberResult(this.memberResults[0]); + }, + }, }; </script>