import { useMemo, useState } from 'react'; import { useCreatePosition, useCreateShift, useDeletePosition, useDeleteShift, useEdition, useGenerateShifts, usePositions, useShifts, useUpdatePosition, useUpdateShift, } from '@/api/hooks'; import type { PositionCategory, PositionRead, ShiftRead } from '@/api/schema'; import { ErrorNote, Loading } from '@/components/state'; import { Btn } from '@/components/ui'; import { categoryLabel } from '@/lib/planning'; import { hourRangeLabel, hoursBetween, longDayLabel } from '@/lib/time'; import { t } from '@/i18n'; import { cx } from '@/lib/cx'; const CATEGORIES: PositionCategory[] = [ 'BAR', 'TICKETING', 'WELCOME', 'PARKING', 'CLEANING', 'CATERING', 'SETUP', 'STAGE', 'OTHER', ]; function enumerateDays(start: string, end: string): string[] { const out: string[] = []; const cursor = new Date(`${start}T00:00:00Z`); const last = new Date(`${end}T00:00:00Z`); while (cursor <= last) { out.push(cursor.toISOString().slice(0, 10)); cursor.setUTCDate(cursor.getUTCDate() + 1); } return out; } export function ConfigGrid() { const editionQuery = useEdition(); const positions = usePositions(); const allShifts = useShifts(); const createPosition = useCreatePosition(); const [selectedId, setSelectedId] = useState(null); const position = (positions.data ?? []).find((p) => p.id === selectedId) ?? (positions.data ?? [])[0]; const positionShifts = useShifts(position?.id); const updateShift = useUpdateShift(); const deleteShift = useDeleteShift(); const createShift = useCreateShift(); const totals = useMemo(() => { let min = 0; let ideal = 0; for (const s of allShifts.data ?? []) { const h = hoursBetween(s.starts_at, s.ends_at); min += s.min_staff * h; ideal += s.ideal_staff * h; } return { min: Math.round(min), ideal: Math.round(ideal), count: (allShifts.data ?? []).length }; }, [allShifts.data]); const addPosition = () => createPosition.mutate( { name: t('config.grid.newPositionName'), category: 'OTHER' }, { onSuccess: (created) => setSelectedId(created.id) }, ); if (positions.isLoading || editionQuery.isLoading) return ; if (positions.error) return ; if (editionQuery.error) return ; if (!position) { return (
{createPosition.isPending ? t('app.loading') : t('config.grid.addPosition')} {createPosition.isError ? : null}
); } const shiftsByDay = groupByDay(positionShifts.data ?? []); const days = editionQuery.data ? enumerateDays(editionQuery.data.start_date, editionQuery.data.end_date) : [...shiftsByDay.keys()]; return (
{t('config.grid.positionsHeader', { count: totals.count })}
{(positions.data ?? []).map((entry) => { const active = entry.id === position.id; const n = (allShifts.data ?? []).filter((s) => s.position_id === entry.id).length; return ( ); })} {createPosition.isError ? : null}
{positionShifts.isLoading ? : null} {createShift.isError ? : null} {updateShift.isError ? : null}
{days.map((day) => { const shifts = shiftsByDay.get(day) ?? []; return (
{longDayLabel(`${day}T12:00`)}
{shifts.map((shift) => (
{hourRangeLabel(shift.starts_at, shift.ends_at)} updateShift.mutate({ id: shift.id, body: { min_staff: Math.max(0, shift.min_staff + delta) }, }) } /> updateShift.mutate({ id: shift.id, body: { ideal_staff: Math.max(0, shift.ideal_staff + delta) }, }) } />
))} {shifts.length === 0 ? (
{t('config.grid.emptyDay')}
) : null}
); })}
{t('config.grid.note')}
); } function PositionHeader({ position, totals, }: { position: PositionRead; totals: { min: number; ideal: number }; }) { const updatePosition = useUpdatePosition(); const deletePosition = useDeletePosition(); const generate = useGenerateShifts(); const [name, setName] = useState(position.name); const commitName = () => { const trimmed = name.trim(); if (trimmed && trimmed !== position.name) { updatePosition.mutate({ id: position.id, body: { name: trimmed } }); } }; return (
setName(e.target.value)} onBlur={commitName} onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()} className="border-2 border-ink bg-yellow px-2 py-1 font-display text-[22px] font-black tracking-tight" />
{generate.isSuccess ? (
{t('config.grid.generated', { count: generate.data.created })}
) : null} {updatePosition.isError ? : null} {generate.isError ? : null}
); } function groupByDay(shifts: ShiftRead[]): Map { const map = new Map(); for (const s of [...shifts].sort((a, b) => a.starts_at.localeCompare(b.starts_at))) { const day = s.starts_at.slice(0, 10); map.set(day, [...(map.get(day) ?? []), s]); } return map; } function MiniStat({ value, label, accent = false }: { value: string; label: string; accent?: boolean }) { return (
{value}
{label}
); } function Stepper({ label, value, accent = false, onChange, }: { label: string; value: number; accent?: boolean; onChange: (delta: number) => void; }) { return ( {label} {value} ); }