Replace 24 `err: any` error handler types with proper `AxiosError<ApiErrorResponse>` typing. Fix additional `as any` casts and `Record<string, any>` patterns in registration field components, event settings, and portal layout. Create shared `ApiErrorResponse` type for portal app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
441 lines
12 KiB
Vue
441 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { useMyShifts, useCancelAssignment } from '@/composables/api/usePortalShifts'
|
|
import type { MyShiftAssignment } from '@/types/portal-shift'
|
|
import axios from 'axios'
|
|
import type { ApiErrorResponse } from '@/types/api'
|
|
|
|
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: unknown) {
|
|
cancelError.value = axios.isAxiosError<ApiErrorResponse>(err)
|
|
? err.response?.data?.message ?? 'Er is een fout opgetreden.'
|
|
: '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>
|