Add search and steps

This commit is contained in:
philipp lang 2024-02-29 02:01:28 +01:00
parent ab7b99495e
commit 980f539a0b
8 changed files with 239 additions and 112 deletions

View File

@ -1,16 +1,59 @@
<template>
<div class="text-sm relative">
<div class="flex items-center justify-center w-6 h-6 rounded-full border-blue-600 border-solid border-2 absolute -top-2 -left-2 bg-blue-200">
<info-icon class="w-3 h-3 text-blue-700"></info-icon>
</div>
<div class="flex flex-row items-center p-4 border-2 rounded form-group shadow-sm bg-blue-200 border-blue-600">
<div class="flex-grow text-sm text-blue-900 group info">
<slot></slot>
<div
class="flex items-center justify-center w-6 h-6 rounded-full border-solid border-2 absolute -top-2 -left-2 font-arvo font-bold"
:class="[colors[type].container, colors[type].lightText]"
v-text="step"
></div>
<div class="flex flex-row items-center p-4 border-2 rounded form-group shadow-sm group" :class="[colors[type].container, type]">
<div class="flex-grow text-sm" :class="colors[type].text">
<slot name="current" v-if="modelValue === step"></slot>
<slot name="finished" v-if="modelValue > step"></slot>
<slot name="due" v-if="modelValue < step"></slot>
</div>
</div>
</div>
</template>
<script setup>
import InfoIcon from './icons/InfoIcon.vue';
import {ref, computed} from 'vue';
const colors = ref({
success: {
container: 'bg-green-200 border-green-600',
lightText: 'text-green-700',
text: 'text-green-900',
},
info: {
container: 'bg-blue-200 border-blue-600',
lightText: 'text-blue-700',
text: 'text-blue-900',
},
default: {
container: 'bg-neutral-200 border-neutral-600',
lightText: 'text-neutral-700',
text: 'text-neutral-900',
},
});
const type = computed(() => {
if (props.modelValue === props.step) {
return 'info';
}
if (props.modelValue > props.step) {
return 'success';
}
return 'default';
});
const props = defineProps({
modelValue: {
required: true,
type: Number,
},
step: {
required: true,
type: Number,
},
});
</script>

View File

@ -1,16 +0,0 @@
<template>
<div class="text-sm relative">
<div class="flex items-center justify-center w-6 h-6 rounded-full border-green-600 border-solid border-2 absolute -top-2 -left-2 bg-green-200">
<info-icon class="w-3 h-3 text-green-700"></info-icon>
</div>
<div class="flex flex-row items-center p-4 border-2 rounded form-group shadow-sm bg-green-200 border-green-600">
<div class="flex-grow text-sm text-green-900">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup>
import InfoIcon from './icons/InfoIcon.vue';
</script>

13
src/components/VBtn.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<button
type="button"
@click.prevent="$emit('click', $event)"
class="px-4 h-[28px] rounded flex items-center justify-center shadow group-[.info]:bg-blue-500 group-[.info]:hover:bg-blue-700 group-[.info]:text-blue-100 group-[.info]:hover:text-blue-200 group-[.success]:bg-green-500 group-[.success]:hover:bg-green-700 group-[.success]:text-green-100 group-[.success]:hover:text-green-200 group-[.default]:bg-neutral-500 group-[.default]:hover:bg-neutral-700 group-[.default]:text-neutral-100 group-[.default]:hover:text-neutral-200 transition duration-200"
>
<slot></slot>
</button>
</template>
<script setup>
defineEmits(['click']);
</script>

View File

@ -1,16 +1,10 @@
<template>
<label class="w-full border border-solid border-gray-500 focus-within:border-primary rounded-lg relative flex" :for="field.key">
<select :name="field.key" :id="innerId" class="bg-white rounded-lg focus:outline-none text-gray-600 text-left peer py-1 px-2 @sm:py-2 text-sm @sm:text-base @sm:px-3 w-full" v-model="inner">
<option :value="null">-- kein --</option>
<option v-for="(option, index) in field.options" :key="index" :value="option" v-text="option"></option>
</select>
<field-label :name="field.name" :required="field.required"></field-label>
</label>
<v-dropdown v-model="inner" :label="field.name" :id="innerId" :name="innerId" :options="innerOptions" :required="field.required"></v-dropdown>
</template>
<script setup>
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
import VDropdown from './VDropdown.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
@ -35,6 +29,12 @@ const props = defineProps({
},
});
const innerOptions = computed(() => {
return props.field.options.map((option) => {
return {id: option, name: option};
});
});
const innerId = computed(() => (props.id ? props.id : props.field.key));
const inner = computed({

View File

@ -1,77 +1,83 @@
<template>
<div class="relative w-full flex flex-col">
<success v-if="user !== null">
<div class="flex items-center justify-between">
<div>Erfolgreich eingeloggt als {{ user }}.</div>
<button
type="button"
@click.prevent="logout"
class="self-end px-4 h-[28px] rounded flex items-center justify-center bg-green-500 hover:bg-green-700 shadow text-green-100 hover:text-green-200 transition duration-200"
>
Abmelden
</button>
</div>
</success>
<info v-else>
<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>
<p>
Falls du noch keine NaMi Zugangsdaten hast, kannst du sie
<a href="https://nami.dpsg.de/ica/pages/requestLogin.jsp" target="_BLANK" class="font-semibold">hier</a>
beantragen.
</p>
<p>
Bitte achte außerdem darauf, dass du mindestens <span class="font-semibold">Leserechte</span> auf deine Gruppierung hast. Diese kann dir i.d.R. dein
<span class="font-semibold">StaVo</span> erteilen.
</p>
</div>
<div class="flex mt-4 space-x-3">
<v-text name="nami_mglnr" label="Mitgliedsnummer" id="nami_mglnr" v-model="loginData.mglnr" required></v-text>
<v-text name="nami_password" label="Passwort" id="nami_password" v-model="loginData.password" type="password" required></v-text>
<button
type="button"
@click.prevent="login"
class="self-end px-4 h-[28px] rounded flex items-center justify-center bg-blue-500 hover:bg-blue-700 shadow text-blue-100 hover:text-blue-200 transition duration-200"
>
Anmelden
</button>
</div>
<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>
<p>
Falls du noch keine NaMi Zugangsdaten hast, kannst du sie
<a href="https://nami.dpsg.de/ica/pages/requestLogin.jsp" target="_BLANK" class="font-semibold">hier</a>
beantragen.
</p>
<p>
Bitte achte außerdem darauf, dass du mindestens <span class="font-semibold">Leserechte</span> auf deine Gruppierung hast. Diese kann dir i.d.R. dein
<span class="font-semibold">StaVo</span> erteilen.
</p>
</div>
<div class="flex mt-4 space-x-3">
<v-text @keypress.enter="innerLogin" name="nami_mglnr" label="Mitgliedsnummer" id="nami_mglnr" v-model="loginData.mglnr" required></v-text>
<v-text @keypress.enter="innerLogin" name="nami_password" label="Passwort" id="nami_password" v-model="loginData.password" type="password" required></v-text>
<v-btn class="self-end" @click.prevent="innerLogin">Anmelden</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.
</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>
<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>
</div>
</div>
</template>
</info>
<div v-if="user !== null">
<label class="w-full border border-solid border-gray-500 focus-within:border-primary rounded-lg relative flex mt-2">
<input
id="search_firstname"
v-model="searchData.firstname"
name="search_firstname"
type="text"
placeholder=""
class="bg-white rounded-lg focus:outline-none text-gray-600 text-left py-1 px-2 text-sm w-full"
@input="searchForMember"
/>
<field-label name="Vorname"></field-label>
</label>
<div v-for="member in searchResults" :id="member.id">
{{ member.name }}
</div>
</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>
</div>
</div>
<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>
</div>
</template>
@ -80,14 +86,24 @@
import {computed, ref} 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 Success from '../Success.vue';
import VBtn from '../VBtn.vue';
import useAdremaLogin from '../../composables/useAdremaLogin.js';
import useEventMeta from '../../composables/useEventMeta.js';
const {login, logout, user, loginData, searchData, searchForMember, searchResults} = useAdremaLogin();
const eventMeta = useEventMeta();
const {login, logout, user, loginData, searchData, searchForMember, resetSearchData, searchResults} = useAdremaLogin();
const {resolveComponentName} = useFields();
const step = computed(() => {
if (user.value === null) {
return 1;
}
return 2;
});
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
@ -126,4 +142,15 @@ const inner = computed(
},
{deep: true},
);
async function innerLogin() {
await login();
resetSearchData();
searchForMember();
}
async function innerLogout() {
logout();
resetSearchData();
inner.value = [];
}
</script>

View File

@ -0,0 +1,53 @@
<template>
<label class="w-full border border-solid border-gray-500 focus-within:border-primary rounded-lg relative flex" :for="id">
<select
:name="name"
:id="id"
class="bg-white group-[.info]:bg-blue-200 rounded-lg focus:outline-none text-gray-600 text-left peer py-1 px-2 @sm:py-2 @sm:group-[.info]:py-1 text-sm @sm:text-base @sm:group-[.info]:text-sm @sm:px-3 @sm:group-[.info]:px-2 w-full"
v-model="inner"
>
<option :value="null">-- kein --</option>
<option v-for="(option, index) in options" :key="index" :value="option.id" v-text="option.name"></option>
</select>
<field-label :name="label" class="group-[.info]:bg-blue-200" :required="required"></field-label>
</label>
</template>
<script setup>
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
modelValue: {
required: true,
validator: (value) => value === null || typeof value === 'string',
},
required: {
required: false,
type: Boolean,
default: () => false,
},
name: {
required: true,
type: String,
},
id: {
required: true,
type: String,
},
label: {
required: true,
type: String,
},
options: {
required: true,
type: Array,
},
});
const inner = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
</script>

View File

@ -6,6 +6,7 @@
:name="name"
:type="type"
placeholder=""
@keypress="$emit('keypress', $event)"
class="bg-white group-[.info]:bg-blue-200 rounded-lg focus:outline-none text-gray-600 text-left w-full py-1 @sm:py-2 @sm:group-[.info]:py-1 px-2 @sm:px-3 @sm:group-[.info]:px-2 text-sm @sm:text-base @sm:group-[.info]:text-sm"
/>
<field-label :name="label" class="group-[.info]:bg-blue-200" :required="required"></field-label>
@ -16,7 +17,7 @@
import {computed} from 'vue';
import FieldLabel from '../FieldLabel.vue';
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(['update:modelValue', 'keypress']);
const props = defineProps({
modelValue: {
required: true,

View File

@ -1,10 +1,8 @@
import {ref, computed} from 'vue';
import useEventMeta from './useEventMeta.js';
import useToastify from './useToastify.js';
import debounce from 'lodash/debounce';
const {success, errorFromResponse} = useToastify();
const eventMeta = useEventMeta();
export default function useAdremaLogin() {
const loginData = ref({
@ -12,10 +10,17 @@ export default function useAdremaLogin() {
password: null,
});
const searchData = ref({
firstname: null,
lastname: null,
});
const defaultSearchData = {
vorname: null,
nachname: null,
untergliederungId: null,
};
const searchData = ref(JSON.parse(JSON.stringify(defaultSearchData)));
function resetSearchData() {
searchData.value = JSON.parse(JSON.stringify(defaultSearchData));
}
const searchResults = ref([]);
@ -47,11 +52,11 @@ export default function useAdremaLogin() {
}
const searchForMember = debounce(async function () {
console.log(loginToken.value.token);
const response = await axios.post(
'/remote/nami/search',
{
vorname: searchData.value.firstname,
...searchData.value,
untergliederungId: searchData.value.untergliederungId ? [searchData.value.untergliederungId] : [],
},
{
headers: {'X-Adrema-Token': loginToken.value.token},
@ -69,5 +74,6 @@ export default function useAdremaLogin() {
loginData,
searchData,
searchForMember,
resetSearchData,
};
}