Per WS-3 PR-B1 charter §4.2: portal pages relocate into the
single-SPA layout under apps/app/src/pages/portal/** (authenticated
portal context) and apps/app/src/pages/register/** (public
token-based form-fill / confirmation).
Updated meta blocks:
- Portal pages: layout: 'PortalLayout', context: 'portal'
(preserving original requiresAuth + nav fields)
- Register pages: layout: 'PublicLayout' (drop requiresAuth)
Skipped (apps/portal duplicates of pages already in apps/app):
index.vue, login.vue, wachtwoord-{vergeten,resetten}.vue,
verify-email-change.vue. Deleted: [...path].vue (apps/app already
has [...error].vue catch-all).
NOTE: Component/store/composable imports inside these files still
point at apps/portal-relative paths and will be rewritten in the
next commits. Build will not be green again until commit 6
(composables/lib).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
238 lines
6.6 KiB
Vue
238 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
import { useAllMyShifts } from '@/composables/api/usePortalShifts'
|
|
import { useAuthStore } from '@/stores/useAuthStore'
|
|
import type { AllMyShiftsAssignment } from '@/types/portal-shift'
|
|
|
|
definePage({
|
|
name: 'portal-shifts',
|
|
meta: {
|
|
layout: 'portal',
|
|
requiresAuth: true,
|
|
},
|
|
})
|
|
|
|
const auth = useAuthStore()
|
|
const { data: eventGroups, isLoading, isError, refetch } = useAllMyShifts()
|
|
|
|
const statusConfig: Record<string, { label: string; color: string }> = {
|
|
pending_approval: { label: 'In afwachting', color: 'warning' },
|
|
approved: { label: 'Bevestigd', color: 'success' },
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleDateString('nl-NL', {
|
|
weekday: 'long',
|
|
day: 'numeric',
|
|
month: 'long',
|
|
})
|
|
}
|
|
|
|
function getStatusConfig(assignment: AllMyShiftsAssignment) {
|
|
return statusConfig[assignment.status] ?? { label: assignment.status, color: 'default' }
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<VRow justify="center">
|
|
<VCol
|
|
cols="12"
|
|
md="8"
|
|
lg="6"
|
|
>
|
|
<h5 class="text-h5 mb-6">
|
|
Mijn diensten
|
|
</h5>
|
|
|
|
<!-- Not authenticated -->
|
|
<VAlert
|
|
v-if="!auth.isAuthenticated"
|
|
type="info"
|
|
variant="tonal"
|
|
>
|
|
<VIcon
|
|
start
|
|
icon="tabler-login"
|
|
/>
|
|
Log in om je diensten te bekijken.
|
|
</VAlert>
|
|
|
|
<template v-else>
|
|
<!-- 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>
|
|
|
|
<!-- Empty state -->
|
|
<VCard
|
|
v-else-if="!eventGroups?.length"
|
|
variant="flat"
|
|
class="text-center pa-8"
|
|
>
|
|
<VAvatar
|
|
size="64"
|
|
color="primary"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<VIcon
|
|
icon="tabler-calendar-off"
|
|
size="32"
|
|
/>
|
|
</VAvatar>
|
|
<p class="text-body-1 text-medium-emphasis mb-4">
|
|
Je hebt nog geen diensten toegewezen gekregen.
|
|
</p>
|
|
<VBtn
|
|
color="primary"
|
|
variant="tonal"
|
|
to="/evenementen"
|
|
>
|
|
Bekijk je evenementen
|
|
</VBtn>
|
|
</VCard>
|
|
|
|
<!-- Shift groups -->
|
|
<template v-else>
|
|
<div
|
|
v-for="eventGroup in eventGroups"
|
|
:key="eventGroup.event.id"
|
|
class="mb-8"
|
|
>
|
|
<!-- Event header -->
|
|
<div class="d-flex align-center gap-2 mb-4">
|
|
<VIcon
|
|
icon="tabler-calendar-event"
|
|
size="20"
|
|
color="primary"
|
|
/>
|
|
<span class="text-subtitle-1 font-weight-bold">
|
|
{{ eventGroup.event.name }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Date groups -->
|
|
<div
|
|
v-for="dateGroup in eventGroup.assignments"
|
|
:key="dateGroup.date"
|
|
class="mb-4"
|
|
>
|
|
<div class="text-subtitle-2 text-medium-emphasis mb-2">
|
|
{{ dateGroup.date_label ?? formatDate(dateGroup.date) }}
|
|
</div>
|
|
|
|
<VCard
|
|
v-for="assignment in dateGroup.shifts"
|
|
:key="assignment.id"
|
|
variant="outlined"
|
|
class="mb-2 shift-card"
|
|
:class="`shift-card--${assignment.status}`"
|
|
>
|
|
<VCardItem>
|
|
<template #prepend>
|
|
<VIcon
|
|
v-if="assignment.shift.section_icon"
|
|
:icon="assignment.shift.section_icon"
|
|
size="24"
|
|
color="primary"
|
|
/>
|
|
<VAvatar
|
|
v-else
|
|
size="32"
|
|
color="primary"
|
|
variant="tonal"
|
|
>
|
|
{{ assignment.shift.section_name[0] }}
|
|
</VAvatar>
|
|
</template>
|
|
|
|
<VCardTitle class="text-subtitle-1 font-weight-bold">
|
|
{{ assignment.shift.title }}
|
|
</VCardTitle>
|
|
<VCardSubtitle>{{ assignment.shift.section_name }}</VCardSubtitle>
|
|
|
|
<template #append>
|
|
<VChip
|
|
:color="getStatusConfig(assignment).color"
|
|
size="small"
|
|
variant="tonal"
|
|
>
|
|
{{ getStatusConfig(assignment).label }}
|
|
</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-clock"
|
|
size="14"
|
|
class="me-1"
|
|
/>
|
|
{{ assignment.shift.start_time }} - {{ assignment.shift.end_time }}
|
|
</span>
|
|
<span v-if="assignment.shift.report_time">
|
|
<VIcon
|
|
icon="tabler-alert-circle"
|
|
size="14"
|
|
class="me-1"
|
|
/>
|
|
Aanwezig: {{ assignment.shift.report_time }}
|
|
</span>
|
|
<span v-if="assignment.shift.location">
|
|
<VIcon
|
|
icon="tabler-map-pin"
|
|
size="14"
|
|
class="me-1"
|
|
/>
|
|
{{ assignment.shift.location.name }}
|
|
</span>
|
|
</div>
|
|
</VCardText>
|
|
</VCard>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</VCol>
|
|
</VRow>
|
|
</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));
|
|
}
|
|
</style>
|