feat(portal): login, dashboard, event switcher, password reset flow

Made-with: Cursor
This commit is contained in:
2026-04-13 00:52:04 +02:00
parent ec4ba8733d
commit 34eb790b3e
16 changed files with 1151 additions and 394 deletions

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import { usePortalStore } from '@/stores/usePortalStore'
const portal = usePortalStore()
const menuOpen = ref(false)
function statusColor(status: string): string {
if (status === 'approved') return 'success'
if (status === 'pending' || status === 'applied' || status === 'invited') return 'warning'
if (status === 'rejected') return 'error'
return 'secondary'
}
function statusLabel(status: string): string {
const map: Record<string, string> = {
pending: 'In behandeling',
applied: 'In behandeling',
invited: 'Uitgenodigd',
approved: 'Goedgekeurd',
rejected: 'Afgewezen',
no_show: 'Niet verschenen',
}
return map[status] ?? status
}
function selectEvent(id: string) {
portal.setActiveEvent(id)
menuOpen.value = false
}
</script>
<template>
<div
v-if="portal.userEvents.length === 0"
class="text-body-2 text-medium-emphasis ms-2 ms-sm-4 d-flex align-center min-w-0"
>
Geen evenement
</div>
<div
v-else-if="portal.userEvents.length === 1 && portal.activeEvent"
class="ms-2 ms-sm-4 d-flex align-center gap-2 min-w-0 flex-grow-1 flex-sm-grow-0"
>
<VIcon
icon="tabler-calendar-event"
size="20"
class="flex-shrink-0"
/>
<span class="text-body-1 font-weight-medium text-truncate">{{ portal.activeEvent.event_name }}</span>
<VChip
:color="statusColor(portal.activeEvent.person_status)"
size="small"
label
class="flex-shrink-0"
>
{{ statusLabel(portal.activeEvent.person_status) }}
</VChip>
</div>
<VMenu
v-else
v-model="menuOpen"
location="bottom"
:close-on-content-click="true"
>
<template #activator="{ props: menuProps }">
<VBtn
v-bind="menuProps"
variant="text"
class="ms-2 ms-sm-4 text-none min-w-0"
rounded="lg"
>
<VIcon
icon="tabler-calendar-event"
start
size="20"
/>
<span class="text-truncate max-w-[200px] sm:max-w-[280px]">{{ portal.activeEvent?.event_name ?? 'Kies evenement' }}</span>
<VIcon
icon="tabler-chevron-down"
end
size="18"
/>
</VBtn>
</template>
<VList
density="compact"
min-width="280"
>
<VListSubheader class="text-caption">
Jouw evenementen
</VListSubheader>
<VListItem
v-for="ev in portal.userEvents"
:key="ev.event_id"
:active="ev.event_id === portal.activeEventId"
@click="selectEvent(ev.event_id)"
>
<VListItemTitle class="text-wrap">
{{ ev.event_name }}
</VListItemTitle>
<VListItemSubtitle class="d-flex flex-column gap-1 mt-1">
<div class="d-flex align-center gap-2 flex-wrap">
<VChip
:color="statusColor(ev.person_status)"
size="x-small"
label
>
{{ statusLabel(ev.person_status) }}
</VChip>
</div>
<span
v-if="ev.organisation_name"
class="text-caption text-medium-emphasis"
>{{ ev.organisation_name }}</span>
</VListItemSubtitle>
</VListItem>
</VList>
</VMenu>
</template>

View File

@@ -0,0 +1,161 @@
<script setup lang="ts">
const props = defineProps<{
variant: 'pending' | 'approved' | 'rejected'
eventName: string
registeredAt?: string | null
nextShiftSummary?: string | null
}>()
const registeredLabel = computed(() => {
if (!props.registeredAt) return null
try {
return new Date(props.registeredAt).toLocaleDateString('nl-NL', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
}
catch {
return null
}
})
</script>
<template>
<VCard
variant="tonal"
:color="variant === 'approved' ? 'success' : variant === 'pending' ? 'warning' : 'error'"
class="pa-6"
>
<template v-if="variant === 'pending'">
<div class="d-flex align-start gap-3">
<VIcon
icon="tabler-clock"
size="32"
/>
<div>
<h5 class="text-h5 mb-2">
Je registratie wordt beoordeeld
</h5>
<p class="text-body-1 mb-2">
Je hebt je aangemeld voor <strong>{{ eventName }}</strong>.
De organisatie beoordeelt je registratie.
</p>
<p class="text-body-1 mb-2">
Je ontvangt een e-mail zodra er een besluit is.
</p>
<p
v-if="registeredLabel"
class="text-body-2 text-medium-emphasis mb-0"
>
Aangemeld op: {{ registeredLabel }}
</p>
</div>
</div>
</template>
<template v-else-if="variant === 'rejected'">
<div class="d-flex align-start gap-3">
<VIcon
icon="tabler-circle-x"
size="32"
/>
<div>
<h5 class="text-h5 mb-2">
Je aanmelding is niet geselecteerd
</h5>
<p class="text-body-1 mb-0">
Helaas is je aanmelding voor <strong>{{ eventName }}</strong> niet geselecteerd.
Neem contact op met de organisatie als je vragen hebt.
</p>
</div>
</div>
</template>
<template v-else>
<div class="d-flex align-start gap-3 mb-6">
<VIcon
icon="tabler-circle-check"
size="32"
/>
<div>
<h5 class="text-h5 mb-1">
Welkom bij {{ eventName }}!
</h5>
<p class="text-body-2 text-medium-emphasis mb-0">
Je bent goedgekeurd als vrijwilliger.
</p>
</div>
</div>
<VRow class="mb-6">
<VCol
cols="12"
sm="4"
>
<VCard
:to="{ name: 'portal-shifts' }"
variant="outlined"
class="pa-4 h-100 text-decoration-none"
>
<div class="text-subtitle-2 text-medium-emphasis mb-1">
Mijn Shifts
</div>
<div class="text-body-2">
Rooster bekijken
</div>
</VCard>
</VCol>
<VCol
cols="12"
sm="4"
>
<VCard
variant="outlined"
class="pa-4 h-100 text-medium-emphasis"
>
<div class="text-subtitle-2 mb-1">
Shifts claimen
</div>
<div class="text-body-2">
Binnenkort beschikbaar
</div>
</VCard>
</VCol>
<VCol
cols="12"
sm="4"
>
<VCard
:to="{ name: 'portal-profile' }"
variant="outlined"
class="pa-4 h-100 text-decoration-none"
>
<div class="text-subtitle-2 text-medium-emphasis mb-1">
Profiel
</div>
<div class="text-body-2">
Gegevens bekijken
</div>
</VCard>
</VCol>
</VRow>
<div class="text-subtitle-1 font-weight-bold mb-2">
Komende shift
</div>
<p
v-if="nextShiftSummary"
class="text-body-1 mb-0"
>
{{ nextShiftSummary }}
</p>
<p
v-else
class="text-body-2 text-medium-emphasis mb-0"
>
Er is nog geen shift ingepland. Je coördinator houdt je op de hoogte.
</p>
</template>
</VCard>
</template>