"use client"; import { ReactNode, createContext, useCallback, useContext, useState, } from "react"; import { MOCK_SHIFT_SLOTS } from "./constants"; import type { ShiftSlot, ShiftStatus } from "./types"; // ─── Types ──────────────────────────────────────────────────────────────────── export type ScheduleView = "week" | "month"; interface ShiftContextType { // Data shifts: ShiftSlot[]; view: ScheduleView; setView: (view: ScheduleView) => void; // Current date navigation currentDate: Date; goToNextWeek: () => void; goToPrevWeek: () => void; goToNextMonth: () => void; goToPrevMonth: () => void; goToToday: () => void; // Shift actions registerShift: ( shiftId: string, staffId: number, staffName: string, ) => { success: boolean; error?: string }; unregisterShift: (shiftId: string, staffId: number) => void; createShift: (shift: Omit) => void; updateShift: (shift: ShiftSlot) => void; deleteShift: (shiftId: string) => void; // Helpers getShiftsForDate: (date: string) => ShiftSlot[]; getShiftsForWeek: (weekStart: Date) => ShiftSlot[]; getWeekDates: () => Date[]; hasConflict: ( date: string, startTime: string, endTime: string, staffId: number, excludeShiftId?: string, ) => boolean; getWeeklyBudget: () => number; } // ─── Context ────────────────────────────────────────────────────────────────── const ShiftContext = createContext(undefined); // ─── Helpers ────────────────────────────────────────────────────────────────── function getMonday(d: Date): Date { const date = new Date(d); const day = date.getDay(); const diff = date.getDate() - day + (day === 0 ? -6 : 1); date.setDate(diff); date.setHours(0, 0, 0, 0); return date; } function formatDate(d: Date): string { const y = d.getFullYear(); const m = (d.getMonth() + 1).toString().padStart(2, "0"); const day = d.getDate().toString().padStart(2, "0"); return `${y}-${m}-${day}`; } function timeToMinutes(time: string): number { const [h, m] = time.split(":").map(Number); return h * 60 + m; } function timesOverlap( start1: string, end1: string, start2: string, end2: string, ): boolean { const s1 = timeToMinutes(start1); const e1 = timeToMinutes(end1); const s2 = timeToMinutes(start2); const e2 = timeToMinutes(end2); return s1 < e2 && s2 < e1; } // ─── Provider ───────────────────────────────────────────────────────────────── export function ShiftProvider({ children }: { children: ReactNode }) { const [shifts, setShifts] = useState(MOCK_SHIFT_SLOTS); const [view, setView] = useState("week"); const [currentDate, setCurrentDate] = useState(new Date(2026, 3, 10)); // April 10, 2026 // ── Navigation ────────────────────────────────────────────────────────── const goToNextWeek = useCallback(() => { setCurrentDate((prev) => { const next = new Date(prev); next.setDate(next.getDate() + 7); return next; }); }, []); const goToPrevWeek = useCallback(() => { setCurrentDate((prev) => { const next = new Date(prev); next.setDate(next.getDate() - 7); return next; }); }, []); const goToNextMonth = useCallback(() => { setCurrentDate((prev) => { const next = new Date(prev); next.setMonth(next.getMonth() + 1); return next; }); }, []); const goToPrevMonth = useCallback(() => { setCurrentDate((prev) => { const next = new Date(prev); next.setMonth(next.getMonth() - 1); return next; }); }, []); const goToToday = useCallback(() => { setCurrentDate(new Date(2026, 3, 10)); }, []); // ── Query helpers ─────────────────────────────────────────────────────── const getWeekDates = useCallback((): Date[] => { const monday = getMonday(currentDate); return Array.from({ length: 7 }, (_, i) => { const d = new Date(monday); d.setDate(monday.getDate() + i); return d; }); }, [currentDate]); const getShiftsForDate = useCallback( (date: string): ShiftSlot[] => { return shifts.filter((s) => s.date === date); }, [shifts], ); const getShiftsForWeek = useCallback( (weekStart: Date): ShiftSlot[] => { const dates = Array.from({ length: 7 }, (_, i) => { const d = new Date(weekStart); d.setDate(weekStart.getDate() + i); return formatDate(d); }); return shifts.filter((s) => dates.includes(s.date)); }, [shifts], ); const hasConflict = useCallback( ( date: string, startTime: string, endTime: string, staffId: number, excludeShiftId?: string, ): boolean => { return shifts.some( (s) => s.date === date && s.id !== excludeShiftId && s.registeredStaff.some((rs) => rs.id === staffId) && timesOverlap(startTime, endTime, s.startTime, s.endTime), ); }, [shifts], ); const getWeeklyBudget = useCallback((): number => { const weekDates = getWeekDates().map(formatDate); return shifts .filter((s) => weekDates.includes(s.date) && s.registeredStaff.length > 0) .reduce((sum, s) => sum + s.wage * s.registeredStaff.length, 0); }, [shifts, getWeekDates]); // ── Shift actions ─────────────────────────────────────────────────────── const registerShift = useCallback( ( shiftId: string, staffId: number, staffName: string, ): { success: boolean; error?: string } => { const shift = shifts.find((s) => s.id === shiftId); if (!shift) return { success: false, error: "Ca làm không tồn tại." }; if (shift.registeredStaff.length >= shift.maxStaff) { return { success: false, error: "Ca làm đã đủ người." }; } if (shift.registeredStaff.some((rs) => rs.id === staffId)) { return { success: false, error: "Bạn đã đăng ký ca này rồi." }; } if ( hasConflict( shift.date, shift.startTime, shift.endTime, staffId, shiftId, ) ) { return { success: false, error: "Xung đột lịch! Bạn đã có ca làm trùng giờ trong ngày này.", }; } setShifts((prev) => prev.map((s) => s.id === shiftId ? { ...s, registeredStaff: [ ...s.registeredStaff, { id: staffId, name: staffName }, ], status: "registered" as ShiftStatus, } : s, ), ); return { success: true }; }, [shifts, hasConflict], ); const unregisterShift = useCallback((shiftId: string, staffId: number) => { setShifts((prev) => prev.map((s) => { if (s.id !== shiftId) return s; const updated = s.registeredStaff.filter((rs) => rs.id !== staffId); return { ...s, registeredStaff: updated, status: updated.length === 0 ? ("available" as ShiftStatus) : s.status, }; }), ); }, []); const createShift = useCallback((shift: Omit) => { const newShift: ShiftSlot = { ...shift, id: `shift_${Date.now()}`, }; setShifts((prev) => [...prev, newShift]); }, []); const updateShift = useCallback((shift: ShiftSlot) => { setShifts((prev) => prev.map((s) => (s.id === shift.id ? shift : s))); }, []); const deleteShift = useCallback((shiftId: string) => { setShifts((prev) => prev.filter((s) => s.id !== shiftId)); }, []); return ( {children} ); } // ─── Hook ───────────────────────────────────────────────────────────────────── export function useShift() { const context = useContext(ShiftContext); if (context === undefined) { throw new Error("useShift must be used within a ShiftProvider"); } return context; }