feat: portal cross-event my-shifts endpoint and dashboard page
GET /portal/my-shifts aggregates shift assignments across all events the logged-in user is linked to via Person records. Groups by event then date, showing only active assignments (approved/pending_approval) for approved/pending persons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,25 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { AvailableShiftsDay, MyShiftsResponse } from '@/types/portal-shift'
|
||||
import type { AllMyShiftsEventGroup, AvailableShiftsDay, MyShiftsResponse } from '@/types/portal-shift'
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export function useAllMyShifts() {
|
||||
return useQuery({
|
||||
queryKey: ['portal-all-my-shifts'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<AllMyShiftsEventGroup[]>>(
|
||||
'/portal/my-shifts',
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAvailableShifts(eventId: Ref<string | null>) {
|
||||
return useQuery({
|
||||
queryKey: ['available-shifts', eventId],
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<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: {
|
||||
@@ -6,6 +10,26 @@ definePage({
|
||||
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>
|
||||
@@ -15,14 +39,199 @@ definePage({
|
||||
md="8"
|
||||
lg="6"
|
||||
>
|
||||
<VCard class="text-center pa-6">
|
||||
<VCardTitle class="text-h5">
|
||||
Mijn Shifts
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
Overzicht van je ingeplande shifts
|
||||
</VCardSubtitle>
|
||||
</VCard>
|
||||
<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>
|
||||
|
||||
@@ -48,3 +48,40 @@ export interface MyShiftAssignment {
|
||||
report_time: string | null
|
||||
can_cancel: boolean
|
||||
}
|
||||
|
||||
// Cross-event "all my shifts" types
|
||||
export interface AllMyShiftsAssignment {
|
||||
id: string
|
||||
status: string
|
||||
shift: {
|
||||
id: string
|
||||
title: string
|
||||
section_name: string
|
||||
section_icon: string | null
|
||||
time_slot_name: string
|
||||
date: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
report_time: string | null
|
||||
location: {
|
||||
name: string
|
||||
address: string | null
|
||||
} | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface AllMyShiftsDateGroup {
|
||||
date: string
|
||||
date_label: string
|
||||
shifts: AllMyShiftsAssignment[]
|
||||
}
|
||||
|
||||
export interface AllMyShiftsEventGroup {
|
||||
event: {
|
||||
id: string
|
||||
name: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
assignments: AllMyShiftsDateGroup[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user