Files
frontend/lib/shift-context.tsx
T
TaNguyenThanhQuy 5307205977
Release package / release (push) Failing after 2m31s
develop (#29)
Co-authored-by: Thanh Quy - wolf <524H0124@student.tdtu.edu.vn>
Co-authored-by: Thanh Quy- wolf <524H0124@student.tdtu.edu.vn>
Co-authored-by: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com>
Reviewed-on: #29
Co-authored-by: TaNguyenThanhQuy <tanguyenthanhquy@noreply.localhost>
Co-committed-by: TaNguyenThanhQuy <tanguyenthanhquy@noreply.localhost>
2026-04-13 15:19:41 +00:00

293 lines
9.2 KiB
TypeScript

"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<ShiftSlot, "id">) => 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<ShiftContextType | undefined>(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<ShiftSlot[]>(MOCK_SHIFT_SLOTS);
const [view, setView] = useState<ScheduleView>("week");
const [currentDate, setCurrentDate] = useState<Date>(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<ShiftSlot, "id">) => {
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 (
<ShiftContext.Provider
value={{
shifts,
view,
setView,
currentDate,
goToNextWeek,
goToPrevWeek,
goToNextMonth,
goToPrevMonth,
goToToday,
registerShift,
unregisterShift,
createShift,
updateShift,
deleteShift,
getShiftsForDate,
getShiftsForWeek,
getWeekDates,
hasConflict,
getWeeklyBudget,
}}
>
{children}
</ShiftContext.Provider>
);
}
// ─── Hook ─────────────────────────────────────────────────────────────────────
export function useShift() {
const context = useContext(ShiftContext);
if (context === undefined) {
throw new Error("useShift must be used within a ShiftProvider");
}
return context;
}