feat: smart re-assignment with cancellation source tracking
Add cancelled_by, cancellation_source (organiser|volunteer|system), and cancelled_at columns to shift_assignments. Cancel flow now records who cancelled and why. Assign flow reactivates existing cancelled/rejected records instead of creating duplicates, preventing UNIQUE constraint violations. Assignable-persons endpoint returns previous_assignment data for contextual UI indicators. Frontend shows cancellation source labels, previous assignment history in assign dialog, and "Opnieuw toewijzen" buttons with volunteer-cancelled confirmation dialogs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
useRejectAssignment,
|
||||
useCancelAssignment,
|
||||
useBulkApproveAssignments,
|
||||
useAssignPersonToShift,
|
||||
} from '@/composables/api/useShiftAssignments'
|
||||
import AssignPersonDialog from '@/components/shifts/AssignPersonDialog.vue'
|
||||
import { useShiftDetailStore } from '@/stores/useShiftDetailStore'
|
||||
@@ -41,6 +42,50 @@ const { mutate: approveAssignment, isPending: isApproving } = useApproveAssignme
|
||||
const { mutate: rejectAssignment, isPending: isRejecting } = useRejectAssignment(eventIdRef)
|
||||
const { mutate: cancelAssignment, isPending: isCancelling } = useCancelAssignment(eventIdRef)
|
||||
const { mutate: bulkApprove, isPending: isBulkApproving } = useBulkApproveAssignments(eventIdRef)
|
||||
const { mutateAsync: assignPersonMutation } = useAssignPersonToShift(eventIdRef)
|
||||
|
||||
// Re-assign
|
||||
const reassigning = ref<string | null>(null)
|
||||
const showVolunteerReassignConfirm = ref(false)
|
||||
const reassigningAssignment = ref<ShiftAssignment | null>(null)
|
||||
|
||||
function onReassign(assignment: ShiftAssignment) {
|
||||
if (assignment.cancellation_source === 'volunteer') {
|
||||
reassigningAssignment.value = assignment
|
||||
showVolunteerReassignConfirm.value = true
|
||||
return
|
||||
}
|
||||
executeReassign(assignment)
|
||||
}
|
||||
|
||||
function confirmReassign() {
|
||||
if (reassigningAssignment.value) {
|
||||
executeReassign(reassigningAssignment.value)
|
||||
}
|
||||
showVolunteerReassignConfirm.value = false
|
||||
reassigningAssignment.value = null
|
||||
}
|
||||
|
||||
async function executeReassign(assignment: ShiftAssignment) {
|
||||
if (!store.selectedSectionId) return
|
||||
reassigning.value = assignment.id
|
||||
try {
|
||||
await assignPersonMutation({
|
||||
sectionId: store.selectedSectionId,
|
||||
shiftId: assignment.shift_id,
|
||||
personId: assignment.person_id,
|
||||
})
|
||||
successMessage.value = `${assignment.person?.name ?? 'Persoon'} opnieuw toegewezen`
|
||||
showSuccess.value = true
|
||||
}
|
||||
catch {
|
||||
successMessage.value = 'Fout bij opnieuw toewijzen'
|
||||
showSuccess.value = true
|
||||
}
|
||||
finally {
|
||||
reassigning.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// Status counts
|
||||
const pendingAssignments = computed(() =>
|
||||
@@ -642,12 +687,45 @@ function fillRateColor(): string {
|
||||
Auto
|
||||
</VChip>
|
||||
</div>
|
||||
<!-- Cancellation source -->
|
||||
<span
|
||||
v-if="assignment.status === ShiftAssignmentStatus.CANCELLED && assignment.cancellation_source === 'volunteer'"
|
||||
class="text-caption text-warning"
|
||||
>
|
||||
(afgemeld door vrijwilliger)
|
||||
</span>
|
||||
<span
|
||||
v-else-if="assignment.status === ShiftAssignmentStatus.CANCELLED && assignment.cancellation_source === 'organiser'"
|
||||
class="text-caption text-medium-emphasis"
|
||||
>
|
||||
(geannuleerd door organisator)
|
||||
</span>
|
||||
|
||||
<p class="text-caption text-disabled mb-0">
|
||||
{{ formatDateTime(assignment.created_at) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-assign button for cancelled/rejected -->
|
||||
<VBtn
|
||||
v-if="assignment.status === ShiftAssignmentStatus.CANCELLED || assignment.status === ShiftAssignmentStatus.REJECTED"
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:loading="reassigning === assignment.id"
|
||||
class="me-1"
|
||||
@click="onReassign(assignment)"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
size="14"
|
||||
>
|
||||
tabler-refresh
|
||||
</VIcon>
|
||||
Opnieuw toewijzen
|
||||
</VBtn>
|
||||
|
||||
<!-- Actions menu -->
|
||||
<VMenu v-if="assignment.is_approvable || assignment.is_cancellable">
|
||||
<template #activator="{ props: menuProps }">
|
||||
@@ -789,6 +867,40 @@ function fillRateColor(): string {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Volunteer re-assign confirmation -->
|
||||
<VDialog
|
||||
v-model="showVolunteerReassignConfirm"
|
||||
max-width="420"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle class="text-h6 pt-5 px-5">
|
||||
Vrijwilliger opnieuw toewijzen?
|
||||
</VCardTitle>
|
||||
<VCardText class="px-5">
|
||||
<strong>{{ reassigningAssignment?.person?.name ?? 'Deze persoon' }}</strong>
|
||||
heeft zichzelf afgemeld voor deze shift. Weet je zeker dat je deze
|
||||
persoon opnieuw wilt toewijzen?
|
||||
</VCardText>
|
||||
<VCardActions class="px-5 pb-5">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="showVolunteerReassignConfirm = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="warning"
|
||||
variant="flat"
|
||||
:loading="reassigning === reassigningAssignment?.id"
|
||||
@click="confirmReassign"
|
||||
>
|
||||
Toch toewijzen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Assign person dialog -->
|
||||
<AssignPersonDialog
|
||||
v-if="shift"
|
||||
|
||||
Reference in New Issue
Block a user