Files
crewli/apps/app/src/pages/portal/shifts/index.vue
bert.hausmans 4cfcd5306a refactor(portal): move pages from apps/portal to apps/app
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>
2026-05-05 18:58:06 +02:00

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>