Add membersearch for contribution selection

This commit is contained in:
philipp lang 2022-08-11 23:19:52 +02:00
parent a8b137ef67
commit c38f92314e
6 changed files with 248 additions and 101 deletions
app
resources/js

View File

@ -18,7 +18,7 @@ class ContributionController extends Controller
session()->put('title', 'Zuschüsse'); session()->put('title', 'Zuschüsse');
return Inertia::render('contribution/VIndex', [ return Inertia::render('contribution/VIndex', [
'allMembers' => MemberResource::collection(Member::get()), 'allMembers' => MemberResource::collection(Member::slangOrdered()->get()),
]); ]);
} }

View File

@ -194,6 +194,16 @@ class Member extends Model
} }
// ---------------------------------- Scopes ----------------------------------- // ---------------------------------- 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 public function scopeWithIsConfirmed(Builder $q): Builder
{ {
return $q->selectSub('DATEDIFF(NOW(), IFNULL(confirmed_at, DATE_SUB(NOW(), INTERVAL 3 YEAR))) < 712', 'is_confirmed'); return $q->selectSub('DATEDIFF(NOW(), IFNULL(confirmed_at, DATE_SUB(NOW(), INTERVAL 3 YEAR))) < 712', 'is_confirmed');

View File

@ -1,14 +1,24 @@
<template> <template>
<label class="flex flex-col relative field-checkbox cursor-pointer" :for="id" :class="{[`size-${size}`]: true}"> <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"> <div class="relative flex items-start">
<input :id="id" type="checkbox" v-model="v" :disabled="disabled" class="invisible absolute" /> <input :id="id" type="checkbox" v-model="v" :disabled="disabled" class="invisible absolute" />
<span class="display-wrapper flex items-center"> <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> <svg-sprite src="check" class="w-4 h-4 check-icon text-white"></svg-sprite>
</span> </span>
</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>
<span v-text="label" v-if="!html"></span> <span v-text="label" v-if="!html"></span>
<span v-html="label" v-if="html"></span> <span v-html="label" v-if="html"></span>
@ -23,41 +33,41 @@
export default { export default {
model: { model: {
prop: 'items', prop: 'items',
event: 'input' event: 'input',
}, },
props: { props: {
html: { html: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
required: { required: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
inset: { inset: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
size: { size: {
default: null, default: null,
required: false required: false,
}, },
id: { id: {
required: true required: true,
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
value: { value: {
default: false default: false,
}, },
label: { label: {
default: false default: false,
}, },
items: { items: {
default: undefined default: undefined,
} },
}, },
computed: { computed: {
v: { v: {
@ -71,7 +81,7 @@ export default {
return; return;
} }
var a = this.items.filter(i => i !== this.value); var a = this.items.filter((i) => i !== this.value);
if (v) { if (v) {
a.push(this.value); a.push(this.value);
} }
@ -88,15 +98,15 @@ export default {
} }
return this.items.indexOf(this.value) !== -1; return this.items.indexOf(this.value) !== -1;
} },
} },
}, },
created() { created() {
if (typeof this.items === 'undefined') { if (typeof this.items === 'undefined') {
this.$emit('input', false); this.$emit('input', false);
} }
} },
}; };
</script> </script>
@ -111,7 +121,8 @@ export default {
transition: background 0.2s; transition: background 0.2s;
} }
.display-wrapper, .checkbox-label { .display-wrapper,
.checkbox-label {
min-height: 34px; min-height: 34px;
} }
@ -119,7 +130,7 @@ export default {
width: var(--checkbox-width); width: var(--checkbox-width);
height: var(--checkbox-width); height: var(--checkbox-width);
border-radius: 0.3rem; 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 { .check-icon {
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;

View File

@ -17,10 +17,11 @@
:value="value" :value="value"
v-model="v" v-model="v"
:disabled="disabled" :disabled="disabled"
class="invisible absolute" class="absolute peer"
@keypress="$emit('keypress', $event)"
/> />
<span <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}" :class="{'bg-switch': v === true, 'bg-gray-700': v === false}"
> >
<span <span

View File

@ -5,7 +5,18 @@
<span v-show="required" class="text-red-800">&nbsp;*</span> <span v-show="required" class="text-red-800">&nbsp;*</span>
</span> </span>
<div class="real-field-wrap" :class="`size-${size}`"> <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-if="hint" class="info-wrap">
<div v-tooltip="hint"> <div v-tooltip="hint">
<svg-sprite src="info-button" class="info-button"></svg-sprite> <svg-sprite src="info-button" class="info-button"></svg-sprite>
@ -28,7 +39,7 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 100; return a / 100;
} },
}), }),
naturalRaw: wNumb({ naturalRaw: wNumb({
mark: '', mark: '',
@ -39,7 +50,7 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 100; return a / 100;
} },
}), }),
naturalDetailRaw: wNumb({ naturalDetailRaw: wNumb({
mark: '', mark: '',
@ -50,7 +61,7 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 10000; return a / 10000;
} },
}), }),
area: wNumb({ area: wNumb({
mark: ',', mark: ',',
@ -61,7 +72,7 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 100; return a / 100;
} },
}), }),
areaDetail: wNumb({ areaDetail: wNumb({
mark: ',', mark: ',',
@ -72,7 +83,7 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 10000; return a / 10000;
} },
}), }),
twoDecimalRaw: wNumb({ twoDecimalRaw: wNumb({
mark: ',', mark: ',',
@ -83,7 +94,7 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 100; return a / 100;
} },
}), }),
fourDecimalRaw: wNumb({ fourDecimalRaw: wNumb({
mark: ',', mark: ',',
@ -94,142 +105,198 @@ var numb = {
}, },
encoder(a) { encoder(a) {
return a / 10000; return a / 10000;
} },
}) }),
}; };
var transformers = { var transformers = {
none: { none: {
display: { display: {
to(v) { return v; }, to(v) {
from(v) { return v; } return v;
},
from(v) {
return v;
},
}, },
edit: { edit: {
to(v) { return v; }, to(v) {
from(v) { return v; } return v;
} },
from(v) {
return v;
},
},
}, },
natural: { natural: {
display: { display: {
to(v) { return isNaN(parseInt(v)) ? '' : numb.natural.to(v); }, to(v) {
from(v) { return v === '' ? null : numb.natural.from(v); } return isNaN(parseInt(v)) ? '' : numb.natural.to(v);
},
from(v) {
return v === '' ? null : numb.natural.from(v);
},
}, },
edit: { edit: {
to(v) { return isNaN(parseInt(v)) ? '' : numb.naturalRaw.to(v); }, to(v) {
from(v) { return v === '' ? null : numb.naturalRaw.from(v); } return isNaN(parseInt(v)) ? '' : numb.naturalRaw.to(v);
} },
from(v) {
return v === '' ? null : numb.naturalRaw.from(v);
},
},
}, },
area: { area: {
display: { display: {
to(v) { return v === null ? '' : numb.area.to(v); }, to(v) {
from(v) { return v === '' ? null : numb.area.from(v); } return v === null ? '' : numb.area.to(v);
},
from(v) {
return v === '' ? null : numb.area.from(v);
},
}, },
edit: { edit: {
to(v) { to(v) {
if (v === null) { return ''; } if (v === null) {
if (Math.round(v / 100) * 100 === v) { return numb.naturalRaw.to(v); } return '';
}
if (Math.round(v / 100) * 100 === v) {
return numb.naturalRaw.to(v);
}
return numb.twoDecimalRaw.to(v); return numb.twoDecimalRaw.to(v);
}, },
from(v) { from(v) {
if (v === '') { return null; } if (v === '') {
if (v.indexOf(',') === -1) { return numb.naturalRaw.from(v); } return null;
}
if (v.indexOf(',') === -1) {
return numb.naturalRaw.from(v);
}
return numb.twoDecimalRaw.from(v); return numb.twoDecimalRaw.from(v);
} },
} },
}, },
currency: { currency: {
display: { display: {
to(v) { return v === null ? '' : numb.area.to(v); }, to(v) {
from(v) { return v === '' ? null : numb.area.from(v); } return v === null ? '' : numb.area.to(v);
},
from(v) {
return v === '' ? null : numb.area.from(v);
},
}, },
edit: { edit: {
to(v) { to(v) {
if (v === null) { return ''; } if (v === null) {
if (Math.round(v / 100) * 100 === v) { return numb.naturalRaw.to(v); } return '';
}
if (Math.round(v / 100) * 100 === v) {
return numb.naturalRaw.to(v);
}
return numb.twoDecimalRaw.to(v); return numb.twoDecimalRaw.to(v);
}, },
from(v) { from(v) {
if (v === '') { return null; } if (v === '') {
if (v.indexOf(',') === -1) { return numb.naturalRaw.from(v); } return null;
}
if (v.indexOf(',') === -1) {
return numb.naturalRaw.from(v);
}
return numb.twoDecimalRaw.from(v); return numb.twoDecimalRaw.from(v);
} },
} },
}, },
currencyDetail: { currencyDetail: {
display: { display: {
to(v) { return v === null ? '' : numb.areaDetail.to(v); }, to(v) {
from(v) { return v === '' ? null : numb.areaDetail.from(v); } return v === null ? '' : numb.areaDetail.to(v);
},
from(v) {
return v === '' ? null : numb.areaDetail.from(v);
},
}, },
edit: { edit: {
to(v) { to(v) {
if (v === null) { return ''; } if (v === null) {
if (Math.round(v / 10000) * 10000 === v) { return numb.naturalDetailRaw.to(v); } return '';
}
if (Math.round(v / 10000) * 10000 === v) {
return numb.naturalDetailRaw.to(v);
}
return numb.fourDecimalRaw.to(v); return numb.fourDecimalRaw.to(v);
}, },
from(v) { from(v) {
if (v === '') { return null; } if (v === '') {
if (v.indexOf(',') === -1) { return numb.naturalDetailRaw.from(v); } return null;
}
if (v.indexOf(',') === -1) {
return numb.naturalDetailRaw.from(v);
}
return numb.fourDecimalRaw.from(v); return numb.fourDecimalRaw.from(v);
} },
} },
} },
}; };
export default { export default {
data: function() { data: function () {
return { return {
focus: false focus: false,
}; };
}, },
props: { props: {
placeholder: { placeholder: {
default: function() { default: function () {
return ''; return '';
} },
}, },
default: {}, default: {},
mode: { mode: {
default: function() { return 'none'; } default: function () {
return 'none';
},
}, },
required: { required: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
inset: { inset: {
default: function() { default: function () {
return null; return null;
} },
}, },
size: { size: {
default: function() { default: function () {
return 'base'; return 'base';
} },
}, },
id: { id: {
required: true required: true,
}, },
hint: { hint: {
default: null default: null,
}, },
value: { value: {
default: undefined default: undefined,
}, },
mask: { mask: {
default: undefined default: undefined,
}, },
label: { label: {
default: false default: false,
}, },
type: { type: {
required: false, required: false,
default: function() { return 'text'; } default: function () {
return 'text';
},
}, },
disabled: { disabled: {
default: false, default: false,
type: Boolean type: Boolean,
}, },
name: {}, name: {},
}, },
@ -249,7 +316,7 @@ export default {
if (this.mode === 'none') { if (this.mode === 'none') {
this.transformedValue = v.target.value; this.transformedValue = v.target.value;
} }
} },
}, },
computed: { computed: {
transformedValue: { transformedValue: {
@ -258,25 +325,35 @@ export default {
}, },
set(v) { set(v) {
this.$emit('input', transformers[this.mode][this.focus ? 'edit' : 'display'].from(v)); this.$emit('input', transformers[this.mode][this.focus ? 'edit' : 'display'].from(v));
} },
}, },
insetClass() { insetClass() {
if (this.inset === '') { return 'bg-inset'; } if (this.inset === '') {
if (this.inset === undefined) { return null; } return 'bg-inset';
}
if (this.inset === undefined) {
return null;
}
return `bg-${this.inset}`; return `bg-${this.inset}`;
} },
}, },
created() { created() {
if (typeof this.value === 'undefined') { if (typeof this.value === 'undefined') {
this.$emit('input', this.default === undefined ? '' : this.default); this.$emit('input', this.default === undefined ? '' : this.default);
} }
} },
}; };
</script> </script>
<style scope> <style scope>
.bg-inset { .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> </style>

View File

@ -18,16 +18,29 @@
required required
></f-text> ></f-text>
<div class="col-span-2"> <div class="border-gray-200 shadow shadow-primary-700 p-3 shadow-[0_0_4px_gray] col-span-2">
<f-switch <f-text
:id="`members-${member.id}`" class="col-span-2"
:key="member.id" id="membersearch"
:label="`${member.firstname} ${member.lastname}`" name="membersearch"
v-for="member in allMembers" v-model="membersearch"
name="members[]" label="Suchen …"
:value="member.id" size="sm"
v-model="values.members" ref="membersearchfield"
></f-switch> @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> </div>
<button <button
@ -46,6 +59,7 @@
export default { export default {
data: function () { data: function () {
return { return {
membersearch: '',
values: { values: {
members: [], members: [],
event_name: '', event_name: '',
@ -57,5 +71,39 @@ export default {
props: { props: {
allMembers: {}, 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> </script>