Compare commits

...

3 Commits

Author SHA1 Message Date
philipp lang e0ab18a469 Add member completion 2024-02-29 23:48:48 +01:00
philipp lang 1f7f70a459 Add pagination 2024-02-29 22:44:48 +01:00
philipp lang 678f068c9c Add spinner 2024-02-29 21:45:19 +01:00
7 changed files with 289 additions and 81 deletions

View File

@ -0,0 +1,72 @@
<template>
<div class="flex flex-col md:flex-row justify-between items-center space-y-3 md:space-y-0">
<div class="text-sm text-gray-600" v-text="desc"></div>
<div v-if="modelValue.last_page > 1" class="items-center flex space-x-2">
<div class="hidden sm:flex text-gray-600 text-sm" v-text="pages"></div>
<button
v-if="modelValue.current_page !== 1"
href="#"
class="rounded !ml-0 sm:!ml-2 flex w-6 h-6 items-center justify-center leading-none shadow bg-blue-700 hover:bg-blue-600 items-center justify-center"
@click.prevent="goto(modelValue.current_page - 1)"
>
<chevron class="rotate-90 text-white w-2 h-2"></chevron>
</button>
<button
v-for="(button, index) in pageButtons"
:key="index"
href="#"
class="rounded text-sm w-6 h-6 text-white flex items-center justify-center leading-none shadow"
:class="{'bg-blue-500': button.current, 'bg-blue-700 hover:bg-blue-600': !button.current}"
@click.prevent="goto(button.page)"
v-text="button.page"
></button>
<button
v-if="modelValue.current_page !== modelValue.last_page"
href="#"
class="flex rounded text-sm w-6 h-6 items-center justify-center leading-none shadow bg-blue-700 hover:bg-blue-600 items-center justify-center"
@click.prevent="goto(modelValue.current_page + 1)"
>
<chevron class="-rotate-90 text-white w-2 h-2"></chevron>
</button>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue';
import Chevron from './icons/Chevron.vue';
const emits = defineEmits(['reload']);
const props = defineProps({
modelValue: {
required: true,
},
});
function goto(page) {
if (page === props.modelValue.current_page) {
return;
}
emits('reload', page);
}
const pageButtons = computed(() => {
var from = Math.max(1, props.modelValue.current_page - 2);
var to = Math.min(props.modelValue.last_page, props.modelValue.current_page + 2);
return Array(to + 1)
.fill(0)
.map((index, key) => key)
.slice(from)
.map((page) => {
return {
page: page,
current: page === props.modelValue.current_page,
};
});
});
const pages = computed(() => `Seite ${props.modelValue.current_page} von ${props.modelValue.last_page}`);
const desc = computed(() => `${props.modelValue.from} - ${props.modelValue.to} von ${props.modelValue.total} Einträgen`);
</script>

View File

@ -0,0 +1,18 @@
<template>
<div role="status">
<svg
aria-hidden="true"
class="w-full h-full text-gray-200 animate-[spin_0.8s_linear_infinite] dark:text-gray-600 fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</template>
<script setup></script>

View File

@ -1,21 +1,10 @@
<template>
<div class="relative">
<div class="grid grid-cols-1 gap-2">
<label :for="innerId" class="p-0 block leading-none relative flex items-start">
<input :id="innerId" v-model="inner" type="checkbox" :name="field.key" class="peer absolute invisible" />
<span class="border-neutral-400 border-4 border-solid peer-checked:border-primary absolute left-0 w-6 h-6 rounded block top-0"></span>
<span class="peer-checked:bg-primary left-[0.5rem] top-[0.5rem] w-2 h-2 absolute rounded block top-0"></span>
<span class="pl-8 pt-1 @sm:pt-0 text-gray-600 text-sm @sm:text-base">
<span v-text="field.description"></span>
<span v-show="field.required" class="text-red-800">*</span>
</span>
</label>
</div>
</div>
<v-checkbox :id="innerId" :name="field.key" :label="field.description" :required="field.required" v-model="inner"></v-checkbox>
</template>
<script setup>
import {computed} from 'vue';
import VCheckbox from './VCheckbox.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({

View File

@ -1,12 +1,6 @@
<template>
<div class="relative w-full flex flex-col">
<info :step="1" v-model="step">
<template #finished>
<div class="flex items-center justify-between">
<div>Erfolgreich eingeloggt als {{ user }}.</div>
<v-btn class="self-end" @click.prevent="innerLogout">Abmelden</v-btn>
</div>
</template>
<template #current>
<div class="space-y-1">
<p>Bitte melde dich mit deinen <span class="font-semibold">NaMi Zugangsdaten</span> an. Im Anschluss kannst du deine Grüpplinge aus deiner Gruppierung hier hinzufügen.</p>
@ -26,75 +20,126 @@
<v-btn class="self-end" @click.prevent="innerLogin">Anmelden</v-btn>
</div>
</template>
<template #finished>
<div class="flex items-center justify-between">
<div>Erfolgreich eingeloggt als {{ user }}.</div>
<v-btn class="self-end" @click.prevent="innerLogout">Abmelden</v-btn>
</div>
</template>
</info>
<info class="mt-4" :step="2" v-model="step">
<template #finished>
<div class="flex items-center justify-between">
<div>{{ inner.length }} Mitglieder ausgewählt</div>
<v-btn class="self-end" @click.prevent="logout">Bearbeiten</v-btn>
</div>
</template>
<template #due>
<div>Mitglieder auswählen</div>
</template>
<template #current>
<p>
Nun kannst du hier nach Mitgliedern suchen. Wähle die Mitglieder aus, die <span class="font-semibold">mit zur Veranstaltung fahren</span>. Dich selbst musst du hier nicht nochmal
auswählen.
Nun kannst du hier nach Mitgliedern suchen. Wähle die Mitglieder aus, die <span class="font-semibold">an der Veranstaltung teilnehmen</span>. Dich selbst musst du hier nicht
nochmal auswählen.
</p>
<div class="flex mt-4 space-x-3">
<v-text name="search_firstname" label="Vorname" id="search_firstname" v-model="searchData.vorname" @input="searchForMember"></v-text>
<v-text name="search_lastname" label="Nachname" id="search_lastname" v-model="searchData.nachname" @input="searchForMember"></v-text>
<v-dropdown name="search_group" label="Stufe" id="search_group" v-model="searchData.untergliederungId" @input="searchForMember" :options="eventMeta.agegroups"></v-dropdown>
</div>
<div v-for="member in searchResults" :id="member.id">
{{ member.name }}
<div class="relative min-h-48">
<div class="relative grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-2 mt-2">
<v-checkbox
:modelValue="memberSelected(member)"
@update:modelValue="toggleMember(member)"
:name="`${field.key}-memberselect-${member.id}`"
:id="`${field.key}-memberselect-${member.id}`"
:label="member.name"
v-for="member in searchResults.data"
></v-checkbox>
</div>
<div v-for="(member, index) in inner" class="flex space-x-2 mt-6" :key="index">
<label class="w-full border border-solid border-gray-500 focus-within:border-primary rounded-lg relative flex mt-2">
<input
:id="`${field.key}-${index}`"
v-model="member.id"
:name="`${field.key}-${index}`"
type="text"
placeholder=""
class="bg-white rounded-lg focus:outline-none text-gray-600 text-left py-1 px-2 @sm:py-2 text-sm @sm:text-base @sm:px-3 w-full"
/>
<field-label name="Mitgliedsnr" :required="true"></field-label>
</label>
<div class="w-full" v-for="(memberField, memberIndex) in memberFields" :key="field.key">
<component
:is="resolveComponentName(memberField)"
:id="`${field.key}-${memberIndex}`"
v-model="member[memberField.key]"
:fields="fields"
:payload="member"
:field="memberField"
>
</component>
<pagination class="mt-5" :model-value="searchResults" @reload="searchForMember($event)" v-if="searchResults.current_page"></pagination>
<div class="absolute flex h-full w-full top-0 left-0 justify-center items-center backdrop-blur-sm" v-if="searching">
<spinner class="w-20 h-20"></spinner>
</div>
</div>
<div class="flex justify-center mt-5">
<v-btn @click.prevent="membersAccepted = true">Weiter</v-btn>
</div>
</template>
<template #finished>
<div class="flex items-center justify-between">
<div>{{ inner.length }} Mitglieder ausgewählt</div>
<v-btn class="self-end" @click.prevent="membersAccepted = false">Bearbeiten</v-btn>
</div>
</template>
</info>
<button type="button" class="bg-primary hover:bg-secondary px-4 py-2 shadow text-font leading-none rounded-lg mt-5" @click.prevent="addMember">Mitglieder hinzufügen</button>
<info class="mt-4" :step="3" v-model="step">
<template #due>
<div>Mitgliederdaten vervollständigen</div>
</template>
<template #current>
<p>
Hier siehst du noch einmal alle ausgewählten Mitglieder. Ggf sind hier pro Mitglied noch weitere Informationen erforderlich. Bitte gebe diese pro Mitglied an und klicke dann auf
"weiter".
</p>
<div class="grid items-center gap-2 mt-6" :style="{'grid-template-columns': `max-content repeat(${memberFields.length}, 1fr)`}">
<template v-for="member in inner" :key="member.id">
<div v-text="member.innerFormName"></div>
<template v-for="(memberField, memberIndex) in memberFields">
<v-checkbox
v-if="memberField.type === 'CheckboxField'"
v-model="member[memberField.key]"
:name="`${field.key}-memberattr-${member.id}-${memberField.key}`"
:id="`${field.key}-memberattr-${member.id}-${memberField.key}`"
:label="memberField.name"
></v-checkbox>
<v-dropdown
v-if="memberField.type === 'DropdownField'"
v-model="member[memberField.key]"
:name="`${field.key}-memberattr-${member.id}-${memberField.key}`"
:id="`${field.key}-memberattr-${member.id}-${memberField.key}`"
:label="memberField.name"
:options="
memberField.options.map((o) => {
return {id: o, name: o};
})
"
></v-dropdown>
</template>
</template>
</div>
<div class="flex justify-center mt-5">
<v-btn @click.prevent="membersCompleted = true">Mitgliederdaten speichern</v-btn>
</div>
</template>
<template #finished>
<div class="flex items-center justify-between">
<div>Daten vervollständigt</div>
<v-btn class="self-end" @click.prevent="membersCompleted = false">Bearbeiten</v-btn>
</div>
</template>
</info>
</div>
</template>
<script setup>
import {computed, ref} from 'vue';
import {computed, ref, warn} from 'vue';
import FieldLabel from '../FieldLabel.vue';
import VText from './VText.vue';
import VDropdown from './VDropdown.vue';
import useFields from '../../composables/useFieldsWithoutNami.js';
import Info from '../Info.vue';
import VBtn from '../VBtn.vue';
import VCheckbox from './VCheckbox.vue';
import Spinner from '../Spinner.vue';
import useAdremaLogin from '../../composables/useAdremaLogin.js';
import useEventMeta from '../../composables/useEventMeta.js';
import Pagination from '../Pagination.vue';
const eventMeta = useEventMeta();
const {login, logout, user, loginData, searchData, searchForMember, resetSearchData, searchResults} = useAdremaLogin();
const {login, logout, user, loginData, searchData, searchForMember, resetSearchData, searchResults, searching} = useAdremaLogin();
if (user.value !== null) {
resetSearchData();
searchForMember();
}
const {resolveComponentName} = useFields();
const step = computed(() => {
@ -102,9 +147,19 @@ const step = computed(() => {
return 1;
}
if (!membersAccepted.value) {
return 2;
}
if (!membersCompleted.value) {
return 3;
}
return 4;
});
const membersAccepted = ref(false);
const membersCompleted = ref(false);
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
modelValue: {
@ -131,10 +186,6 @@ const defaultMember = computed(() => {
const memberFields = computed(() => props.fields.filter((field) => field.for_members === true && field.nami_type === null));
function addMember() {
inner.value.push({id: '', ...defaultMember.value});
}
const inner = computed(
{
get: () => props.modelValue,
@ -144,13 +195,29 @@ const inner = computed(
);
async function innerLogin() {
await login();
membersAccepted.value = false;
membersCompleted.value = false;
resetSearchData();
searchForMember();
}
function memberSelected(member) {
return inner.value.map((m) => m.id).includes(member.id);
}
function toggleMember(member) {
if (memberSelected(member)) {
inner.value = inner.value.filter((m) => m.id !== member.id);
} else {
inner.value.push({id: member.id, innerFormName: member.name, ...defaultMember.value});
}
}
async function innerLogout() {
logout();
resetSearchData();
membersAccepted.value = false;
membersCompleted.value = false;
inner.value = [];
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="relative">
<div class="grid grid-cols-1 gap-2">
<label :for="id" class="p-0 block leading-none relative flex items-start">
<input :id="id" v-model="inner" type="checkbox" :name="name" class="peer absolute invisible" />
<span
class="border-neutral-400 border-4 group-[.info]:border-2 border-solid peer-checked:border-primary absolute left-0 w-6 h-6 group-[.info]:w-4 group-[.info]:h-4 group-[.info]:top-[5px] rounded block top-0"
></span>
<span class="peer-checked:bg-primary left-[0.5rem] top-[0.5rem] group-[.info]:top-[0.58rem] group-[.info]:left-[0.25rem] w-2 h-2 absolute rounded block top-0"></span>
<span v-if="label" class="pl-8 group-[.info]:pl-6 pt-1 @sm:pt-0 @sm:group-[.info]:pt-1 text-gray-600 text-sm @sm:text-base @sm:group-[.info]:text-sm">
<span v-text="label"></span>
<span v-show="required" class="text-red-800">*</span>
</span>
</label>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
modelValue: {
required: true,
validator: (value) => value === true || value === false,
},
id: {
required: true,
type: String,
},
name: {
required: true,
type: String,
},
label: {
required: false,
type: String,
default: () => '',
},
required: {
required: false,
type: Boolean,
default: () => false,
},
});
const inner = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
</script>

View File

@ -0,0 +1,9 @@
<template>
<svg height="223.651" viewBox="0 0 105.911 55.913" width="423.642" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 4.66a4.57 4.57 0 0 1 1.41-3.295c1.882-1.82 4.928-1.82 6.808 0l44.737 43.3 44.738-43.3c1.881-1.82 4.927-1.82 6.807 0a4.552 4.552 0 0 1 0 6.589L56.36 54.547c-1.881 1.82-4.927 1.82-6.807 0L1.41 7.954A4.57 4.57 0 0 1 0 4.66Z"
style="stroke-width: 1.18403"
fill="currentColor"
/>
</svg>
</template>

View File

@ -5,27 +5,32 @@ import debounce from 'lodash/debounce';
const {success, errorFromResponse} = useToastify();
export default function useAdremaLogin() {
const loginData = ref({
mglnr: null,
password: null,
});
const defaultSearchData = {
vorname: null,
nachname: null,
untergliederungId: null,
};
const loginData = ref({
mglnr: null,
password: null,
});
const searching = ref(false);
const searchResults = ref([]);
const searchData = ref(JSON.parse(JSON.stringify(defaultSearchData)));
const loginToken = ref(window.localStorage.getItem('adrema_login_key') ? JSON.parse(window.localStorage.getItem('adrema_login_key')) : null);
const user = computed(() => {
if (loginToken.value === null) {
return null;
}
return loginToken.value.user;
});
function resetSearchData() {
searchData.value = JSON.parse(JSON.stringify(defaultSearchData));
}
const searchResults = ref([]);
const loginToken = ref(window.localStorage.getItem('adrema_login_key') ? JSON.parse(window.localStorage.getItem('adrema_login_key')) : null);
async function login() {
try {
const response = await axios.post('/remote/nami/token', loginData.value);
@ -38,33 +43,28 @@ export default function useAdremaLogin() {
}
}
const user = computed(() => {
if (loginToken.value === null) {
return null;
}
return loginToken.value.user;
});
function logout() {
window.localStorage.removeItem('adrema_login_key');
loginToken.value = null;
}
const searchForMember = debounce(async function () {
const searchForMember = debounce(async function (page = 1) {
searching.value = true;
const response = await axios.post(
'/remote/nami/search',
{
...searchData.value,
untergliederungId: searchData.value.untergliederungId ? [searchData.value.untergliederungId] : [],
page: page,
},
{
headers: {'X-Adrema-Token': loginToken.value.token},
},
);
searchResults.value = response.data.data;
}, 1000);
searchResults.value = response.data;
searching.value = false;
}, 500);
return {
searchResults,
@ -75,5 +75,6 @@ export default function useAdremaLogin() {
searchData,
searchForMember,
resetSearchData,
searching,
};
}