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');
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 -----------------------------------
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');

View File

@ -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;

View File

@ -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

View File

@ -5,7 +5,18 @@
<span v-show="required" class="text-red-800">&nbsp;*</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>

View File

@ -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>