fix: time slot dropdown group headers and dimming via boundary detection

VAutocomplete ignores interleaved fake header items — they were filtered
out before reaching the template. Replace with Approach A: keep only
real selectable items sorted by group, detect group boundaries in the
#item template by comparing adjacent groupName values, and render
VListSubheader before each new group.

- Remove _isGroupHeader from TimeSlotDropdownItem interface
- Rename groupTimeSlots → sortedItems (returns only selectable items)
- Add hasGroups computed for conditional header rendering
- Add isNewGroup(index) boundary detection in CreateShiftDialog
- Add scoped .time-slot-dimmed CSS class (opacity: 0.65)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 22:31:32 +02:00
parent 5bd028f408
commit 948965e664
3 changed files with 56 additions and 44 deletions

View File

@@ -5,12 +5,11 @@ import type { TimeSlot } from '@/types/timeSlot'
export type DropdownScenario = 'flat' | 'sub_event_standard' | 'cross_event' | 'festival_standard'
interface TimeSlotDropdownItem {
export interface TimeSlotDropdownItem {
id: string
name: string
timeRange: string
displayLabel: string
_isGroupHeader: boolean
_isDimmed: boolean
groupName: string
}
@@ -46,6 +45,8 @@ export function useTimeSlotDropdown(
const showInfoTooltip = computed(() => scenario.value !== 'flat')
const hasGroups = computed(() => scenario.value !== 'flat')
const tooltipText = computed<TooltipContent | null>(() => {
const eventName = event.value?.name ?? ''
const sectionName = section.value?.name ?? ''
@@ -86,52 +87,41 @@ export function useTimeSlotDropdown(
}
})
function groupTimeSlots(timeSlots: TimeSlot[]): TimeSlotDropdownItem[] {
/**
* Returns a flat array of selectable items sorted by group
* (own event first, then others). Group boundaries are detected
* in the template by comparing adjacent items' groupName.
*/
function sortedItems(timeSlots: TimeSlot[]): TimeSlotDropdownItem[] {
if (scenario.value === 'flat') {
return timeSlots.map(ts => toDropdownItem(ts, false))
return timeSlots.map(ts => toDropdownItem(ts, false, ''))
}
// Group by event_name
// Classify each slot into a group and determine isOwn per group
const groups = new Map<string, { slots: TimeSlot[]; isOwn: boolean }>()
for (const ts of timeSlots) {
const key = ts.event_name ?? 'Onbekend'
if (!groups.has(key)) {
const isOwn = ts.source === 'own' || ts.source === 'sub_event' || ts.source === 'festival'
? (scenario.value === 'sub_event_standard'
? ts.source === 'sub_event'
: ts.source === 'own')
: false
const isOwn = scenario.value === 'sub_event_standard'
? ts.source === 'sub_event'
: ts.source === 'own'
groups.set(key, { slots: [], isOwn })
}
groups.get(key)!.slots.push(ts)
}
const items: TimeSlotDropdownItem[] = []
// Own group first, then others
const sortedGroups = [...groups.entries()].sort(([, a], [, b]) => {
// Own group first, then others alphabetically
const sorted = [...groups.entries()].sort(([nameA, a], [nameB, b]) => {
if (a.isOwn && !b.isOwn) return -1
if (!a.isOwn && b.isOwn) return 1
return 0
return nameA.localeCompare(nameB)
})
for (const [groupName, { slots, isOwn }] of sortedGroups) {
// Add group header
items.push({
id: `header-${groupName}`,
name: groupName,
timeRange: '',
displayLabel: groupName,
_isGroupHeader: true,
_isDimmed: false,
groupName,
})
// Determine if slots should be dimmed
const items: TimeSlotDropdownItem[] = []
for (const [groupName, { slots, isOwn }] of sorted) {
const isDimmed = scenario.value === 'sub_event_standard' && !isOwn
for (const ts of slots) {
items.push(toDropdownItem(ts, isDimmed))
items.push(toDropdownItem(ts, isDimmed, groupName))
}
}
@@ -141,21 +131,21 @@ export function useTimeSlotDropdown(
return {
scenario,
showInfoTooltip,
hasGroups,
tooltipText,
fetchParams,
groupTimeSlots,
sortedItems,
}
}
function toDropdownItem(ts: TimeSlot, isDimmed: boolean): TimeSlotDropdownItem {
function toDropdownItem(ts: TimeSlot, isDimmed: boolean, groupName: string): TimeSlotDropdownItem {
const timeRange = `${ts.start_time} ${ts.end_time}`
return {
id: ts.id,
name: ts.name,
timeRange,
displayLabel: ts.name,
_isGroupHeader: false,
_isDimmed: isDimmed,
groupName: ts.event_name ?? '',
groupName,
}
}