feat(portal): restructure into three-screen architecture with event tabs
Replace scattered dashboard pages with a three-screen volunteer portal: 1. Mijn evenementen (/evenementen) - landing page with visual event cards in a responsive grid, sorted upcoming-first 2. Event-pagina (/evenementen/:eventId) - single page with hash-based tabs (Overzicht, Mijn rooster, Diensten claimen, Informatie) replacing the old separate dashboard/my-shifts/claim-shifts pages 3. Mijn profiel (/profiel) - unchanged, platform-level settings Key changes: - Extract page content into tab components (RoosterTab, ClaimenTab, OverzichtTab, InformatieTab) that receive eventId as prop - Dual-mode navbar: platform mode (Crewli logo) vs event mode (org name + event name + back link) - StatusCard now emits switchTab events instead of route navigation - Smart login redirect: 1 event → direct to event, 2+ → overview - Backward-compat redirects for /dashboard/* → /evenementen - Delete EventSwitcher (replaced by events overview page) - Update UserAvatarMenu with "Mijn evenementen" link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
436
apps/portal/src/components/event/RoosterTab.vue
Normal file
436
apps/portal/src/components/event/RoosterTab.vue
Normal file
@@ -0,0 +1,436 @@
|
||||
<script setup lang="ts">
|
||||
import { useMyShifts, useCancelAssignment } from '@/composables/api/usePortalShifts'
|
||||
import type { MyShiftAssignment } from '@/types/portal-shift'
|
||||
|
||||
const props = defineProps<{
|
||||
eventId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
switchTab: [tab: string]
|
||||
}>()
|
||||
|
||||
const eventIdRef = computed(() => props.eventId as string | null)
|
||||
|
||||
const { data: shifts, isLoading, isError, refetch } = useMyShifts(eventIdRef)
|
||||
const cancelMutation = useCancelAssignment(eventIdRef)
|
||||
|
||||
const showCancelDialog = ref(false)
|
||||
const cancelTarget = ref<MyShiftAssignment | null>(null)
|
||||
const cancelReason = ref('')
|
||||
const cancelError = ref<string | null>(null)
|
||||
const snackbar = ref(false)
|
||||
const snackbarMessage = ref('')
|
||||
|
||||
const statusConfig: Record<string, { label: string; color: string }> = {
|
||||
pending_approval: { label: 'Wacht op goedkeuring', color: 'warning' },
|
||||
approved: { label: 'Goedgekeurd', color: 'success' },
|
||||
rejected: { label: 'Afgewezen', color: 'error' },
|
||||
cancelled: { label: 'Geannuleerd', color: 'default' },
|
||||
completed: { label: 'Afgerond', color: 'info' },
|
||||
}
|
||||
|
||||
function openCancelDialog(assignment: MyShiftAssignment) {
|
||||
cancelTarget.value = assignment
|
||||
cancelReason.value = ''
|
||||
cancelError.value = null
|
||||
showCancelDialog.value = true
|
||||
}
|
||||
|
||||
async function confirmCancel() {
|
||||
if (!cancelTarget.value) return
|
||||
|
||||
cancelError.value = null
|
||||
|
||||
try {
|
||||
const result = await cancelMutation.mutateAsync({
|
||||
assignmentId: cancelTarget.value.assignment_id,
|
||||
reason: cancelReason.value || undefined,
|
||||
})
|
||||
showCancelDialog.value = false
|
||||
snackbarMessage.value = result.message
|
||||
snackbar.value = true
|
||||
}
|
||||
catch (err: any) {
|
||||
cancelError.value = err?.response?.data?.message ?? 'Er is een fout opgetreden.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<h5 class="text-h5">
|
||||
Mijn diensten
|
||||
</h5>
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="emit('switchTab', 'claimen')"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
icon="tabler-calendar-plus"
|
||||
size="18"
|
||||
/>
|
||||
Diensten claimen
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<template v-if="isLoading">
|
||||
<VSkeletonLoader
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
type="card"
|
||||
class="mb-4"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Error -->
|
||||
<VAlert
|
||||
v-else-if="isError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Er ging iets mis bij het ophalen van je diensten.
|
||||
<template #append>
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="refetch()"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
</template>
|
||||
</VAlert>
|
||||
|
||||
<template v-else-if="shifts">
|
||||
<!-- Upcoming -->
|
||||
<div class="mb-6">
|
||||
<h6 class="text-h6 mb-3">
|
||||
Komende diensten
|
||||
</h6>
|
||||
|
||||
<template v-if="shifts.upcoming.length">
|
||||
<VCard
|
||||
v-for="assignment in shifts.upcoming"
|
||||
:key="assignment.assignment_id"
|
||||
variant="outlined"
|
||||
class="mb-3 shift-card"
|
||||
:class="`shift-card--${assignment.status}`"
|
||||
>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
v-if="assignment.section_icon"
|
||||
:icon="assignment.section_icon"
|
||||
size="24"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle class="text-subtitle-1 font-weight-bold">
|
||||
{{ assignment.shift_title }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ assignment.section_name }}</VCardSubtitle>
|
||||
<template #append>
|
||||
<VChip
|
||||
:color="statusConfig[assignment.status]?.color ?? 'default'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ statusConfig[assignment.status]?.label ?? assignment.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-0">
|
||||
<div class="d-flex flex-wrap gap-x-4 gap-y-1 text-body-2">
|
||||
<span>
|
||||
<VIcon
|
||||
icon="tabler-calendar"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.date_label }}
|
||||
</span>
|
||||
<span>
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.start_time }} - {{ assignment.end_time }}
|
||||
</span>
|
||||
<span v-if="assignment.location_name">
|
||||
<VIcon
|
||||
icon="tabler-map-pin"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.location_name }}
|
||||
</span>
|
||||
<span v-if="assignment.report_time">
|
||||
<VIcon
|
||||
icon="tabler-alert-circle"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
Aanwezig: {{ assignment.report_time }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions v-if="assignment.can_cancel">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
color="error"
|
||||
size="small"
|
||||
:disabled="cancelMutation.isPending.value"
|
||||
@click="openCancelDialog(assignment)"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<VAlert
|
||||
v-else
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
Je hebt nog geen diensten.
|
||||
<a
|
||||
href="#"
|
||||
class="text-primary font-weight-medium"
|
||||
@click.prevent="emit('switchTab', 'claimen')"
|
||||
>
|
||||
Diensten claimen →
|
||||
</a>
|
||||
</VAlert>
|
||||
</div>
|
||||
|
||||
<!-- Past -->
|
||||
<div
|
||||
v-if="shifts.past.length"
|
||||
class="mb-6"
|
||||
>
|
||||
<h6 class="text-h6 mb-3">
|
||||
Afgelopen diensten
|
||||
</h6>
|
||||
|
||||
<VCard
|
||||
v-for="assignment in shifts.past"
|
||||
:key="assignment.assignment_id"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
v-if="assignment.section_icon"
|
||||
:icon="assignment.section_icon"
|
||||
size="24"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle class="text-subtitle-1 font-weight-bold">
|
||||
{{ assignment.shift_title }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ assignment.section_name }}</VCardSubtitle>
|
||||
<template #append>
|
||||
<VChip
|
||||
:color="statusConfig[assignment.status]?.color ?? 'default'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ statusConfig[assignment.status]?.label ?? assignment.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-0">
|
||||
<div class="d-flex flex-wrap gap-x-4 gap-y-1 text-body-2">
|
||||
<span>
|
||||
<VIcon
|
||||
icon="tabler-calendar"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.date_label }}
|
||||
</span>
|
||||
<span>
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.start_time }} - {{ assignment.end_time }}
|
||||
</span>
|
||||
<span v-if="assignment.location_name">
|
||||
<VIcon
|
||||
icon="tabler-map-pin"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.location_name }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<!-- Cancelled / Rejected -->
|
||||
<div v-if="shifts.cancelled.length">
|
||||
<h6 class="text-h6 mb-3">
|
||||
Geannuleerd / Afgewezen
|
||||
</h6>
|
||||
|
||||
<VCard
|
||||
v-for="assignment in shifts.cancelled"
|
||||
:key="assignment.assignment_id"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
v-if="assignment.section_icon"
|
||||
:icon="assignment.section_icon"
|
||||
size="24"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle class="text-subtitle-1 font-weight-bold">
|
||||
{{ assignment.shift_title }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ assignment.section_name }}</VCardSubtitle>
|
||||
<template #append>
|
||||
<VChip
|
||||
:color="statusConfig[assignment.status]?.color ?? 'default'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ statusConfig[assignment.status]?.label ?? assignment.status }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-0">
|
||||
<div class="d-flex flex-wrap gap-x-4 gap-y-1 text-body-2">
|
||||
<span>
|
||||
<VIcon
|
||||
icon="tabler-calendar"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.date_label }}
|
||||
</span>
|
||||
<span>
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
size="14"
|
||||
class="me-1"
|
||||
/>
|
||||
{{ assignment.start_time }} - {{ assignment.end_time }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Cancel confirmation dialog -->
|
||||
<VDialog
|
||||
v-model="showCancelDialog"
|
||||
max-width="480"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle>Dienst annuleren</VCardTitle>
|
||||
<VCardText>
|
||||
<p>
|
||||
Weet je zeker dat je deze dienst wilt annuleren?
|
||||
</p>
|
||||
<p class="text-body-2 text-medium-emphasis mb-3">
|
||||
<strong>{{ cancelTarget?.shift_title }}</strong> —
|
||||
{{ cancelTarget?.date_label }} ({{ cancelTarget?.start_time }} - {{ cancelTarget?.end_time }})
|
||||
</p>
|
||||
|
||||
<VTextarea
|
||||
v-model="cancelReason"
|
||||
label="Reden (optioneel)"
|
||||
rows="2"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
|
||||
<VAlert
|
||||
v-if="cancelError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-3"
|
||||
>
|
||||
{{ cancelError }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
:disabled="cancelMutation.isPending.value"
|
||||
@click="showCancelDialog = false"
|
||||
>
|
||||
Terug
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
:loading="cancelMutation.isPending.value"
|
||||
@click="confirmCancel"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<VSnackbar
|
||||
v-model="snackbar"
|
||||
color="success"
|
||||
:timeout="4000"
|
||||
>
|
||||
{{ snackbarMessage }}
|
||||
</VSnackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shift-card {
|
||||
border-inline-start: 3px solid transparent;
|
||||
}
|
||||
|
||||
.shift-card--approved {
|
||||
border-inline-start-color: rgb(var(--v-theme-success));
|
||||
}
|
||||
|
||||
.shift-card--pending_approval {
|
||||
border-inline-start-color: rgb(var(--v-theme-warning));
|
||||
}
|
||||
|
||||
.shift-card--rejected {
|
||||
border-inline-start-color: rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
.shift-card--cancelled {
|
||||
border-inline-start-color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
|
||||
.shift-card--completed {
|
||||
border-inline-start-color: rgb(var(--v-theme-info));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user