feat: smart assign person dialog with conflict details and assignable-persons endpoint
Add GET /events/{event}/shifts/{shift}/assignable-persons endpoint that
returns approved persons with availability status, conflict details, and
already-assigned flags. Improve ShiftAssignmentService conflict errors to
include section name, time slot, and time range. Replace both assign
dialogs with a new AssignPersonDialog featuring search, crowd type
filtering, availability toggle, and inline conflict warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,13 @@ import { useShiftList, useDeleteShift } from '@/composables/api/useShifts'
|
||||
import CreateSectionDialog from '@/components/sections/CreateSectionDialog.vue'
|
||||
import EditSectionDialog from '@/components/sections/EditSectionDialog.vue'
|
||||
import CreateShiftDialog from '@/components/sections/CreateShiftDialog.vue'
|
||||
import AssignShiftDialog from '@/components/sections/AssignShiftDialog.vue'
|
||||
import AssignPersonDialog from '@/components/shifts/AssignPersonDialog.vue'
|
||||
import ShiftDetailPanel from '@/components/shifts/ShiftDetailPanel.vue'
|
||||
import { useShiftDetailStore } from '@/stores/useShiftDetailStore'
|
||||
import type { FestivalSection, Shift, ShiftStatus } from '@/types/section'
|
||||
|
||||
const shiftDetailStore = useShiftDetailStore()
|
||||
|
||||
const props = defineProps<{
|
||||
eventId: string
|
||||
isSubEvent?: boolean
|
||||
@@ -179,9 +183,10 @@ const statusLabel: Record<ShiftStatus, string> = {
|
||||
cancelled: 'Geannuleerd',
|
||||
}
|
||||
|
||||
function fillRateColor(rate: number): string {
|
||||
if (rate >= 80) return 'success'
|
||||
if (rate >= 40) return 'warning'
|
||||
function fillRateColor(shift: Shift): string {
|
||||
if (shift.is_overbooked) return 'warning'
|
||||
if (shift.fill_rate >= 80) return 'success'
|
||||
if (shift.fill_rate >= 40) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
@@ -197,6 +202,12 @@ function formatDate(iso: string) {
|
||||
return dateFormatter.format(new Date(iso))
|
||||
}
|
||||
|
||||
// Selected shift for detail panel (resolved from store ID)
|
||||
const selectedShift = computed(() => {
|
||||
if (!shiftDetailStore.selectedShiftId || !shifts.value) return null
|
||||
return shifts.value.find(s => s.id === shiftDetailStore.selectedShiftId) ?? null
|
||||
})
|
||||
|
||||
// Success snackbar
|
||||
const showSuccess = ref(false)
|
||||
const successMessage = ref('')
|
||||
@@ -481,8 +492,8 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
<!-- Fill rate -->
|
||||
<div class="d-flex align-center gap-x-2" style="min-inline-size: 160px;">
|
||||
<VProgressLinear
|
||||
:model-value="shift.fill_rate"
|
||||
:color="fillRateColor(shift.fill_rate)"
|
||||
:model-value="shift.is_overbooked ? 100 : shift.fill_rate"
|
||||
:color="fillRateColor(shift)"
|
||||
height="8"
|
||||
rounded
|
||||
style="inline-size: 80px;"
|
||||
@@ -490,6 +501,12 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
<span class="text-body-2 text-no-wrap">
|
||||
{{ shift.filled_slots }}/{{ shift.slots_total }}
|
||||
</span>
|
||||
<VIcon
|
||||
v-if="shift.is_overbooked"
|
||||
icon="tabler-alert-triangle"
|
||||
size="16"
|
||||
color="warning"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
@@ -499,11 +516,32 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
>
|
||||
{{ statusLabel[shift.status] }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="shift.is_overbooked"
|
||||
color="warning"
|
||||
size="small"
|
||||
prepend-icon="tabler-alert-triangle"
|
||||
>
|
||||
Overbezet
|
||||
</VChip>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex gap-x-1">
|
||||
<VBtn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="shiftDetailStore.openPanel(shift.id, activeSection!.id)"
|
||||
>
|
||||
<VIcon size="18">
|
||||
tabler-eye
|
||||
</VIcon>
|
||||
<VTooltip activator="parent">
|
||||
Details bekijken
|
||||
</VTooltip>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
icon="tabler-user-plus"
|
||||
variant="text"
|
||||
@@ -563,7 +601,7 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
:is-sub-event="isSubEvent"
|
||||
/>
|
||||
|
||||
<AssignShiftDialog
|
||||
<AssignPersonDialog
|
||||
v-if="activeSection"
|
||||
v-model="isAssignShiftOpen"
|
||||
:event-id="activeSectionEventId"
|
||||
@@ -626,6 +664,13 @@ function onSectionCreated(payload: { name: string; redirectedToParent: boolean;
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Shift detail side panel -->
|
||||
<ShiftDetailPanel
|
||||
v-model="shiftDetailStore.isOpen"
|
||||
:event-id="activeSectionEventId"
|
||||
:shift="selectedShift"
|
||||
/>
|
||||
|
||||
<!-- Success snackbar -->
|
||||
<VSnackbar
|
||||
v-model="showSuccess"
|
||||
|
||||
Reference in New Issue
Block a user