This commit is contained in:
@@ -37,7 +37,7 @@ function formatDateISO(d: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isToday(d: Date): boolean {
|
function isToday(d: Date): boolean {
|
||||||
const today = new Date(2026, 3, 10);
|
const today = Date.now();
|
||||||
return (
|
return (
|
||||||
d.getDate() === today.getDate() &&
|
d.getDate() === today.getDate() &&
|
||||||
d.getMonth() === today.getMonth() &&
|
d.getMonth() === today.getMonth() &&
|
||||||
@@ -60,35 +60,31 @@ export default function WeeklySchedule({
|
|||||||
const weekDates = getWeekDates();
|
const weekDates = getWeekDates();
|
||||||
|
|
||||||
const [selectedDate, setSelectedDate] = useState<string>(
|
const [selectedDate, setSelectedDate] = useState<string>(
|
||||||
formatDateISO(weekDates[0] ?? currentDate),
|
weekDates[0] ?? currentDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusDotsByDate = useMemo(() => {
|
const statusDotsByDate = useMemo(() => {
|
||||||
const map: Record<string, string[]> = {};
|
const map: Record<string, string[]> = {};
|
||||||
weekDates.forEach((date) => {
|
weekDates.forEach((date) => {
|
||||||
const dateStr = formatDateISO(date);
|
|
||||||
const dayShifts = getShiftsForDate(date);
|
|
||||||
const dots: string[] = [];
|
const dots: string[] = [];
|
||||||
if (dayShifts.some((s) => s.status === "available"))
|
// if (dayShifts.some((s) => s.status === "available"))
|
||||||
dots.push("bg-sky-300");
|
// dots.push("bg-sky-300");
|
||||||
if (dayShifts.some((s) => s.status === "registered"))
|
// if (dayShifts.some((s) => s.status === "registered"))
|
||||||
dots.push("bg-blue-600");
|
// dots.push("bg-blue-600");
|
||||||
if (dayShifts.some((s) => s.status === "approved_leave"))
|
// if (dayShifts.some((s) => s.status === "approved_leave"))
|
||||||
dots.push("bg-purple-400");
|
// dots.push("bg-purple-400");
|
||||||
if (dayShifts.some((s) => s.status === "absent"))
|
// if (dayShifts.some((s) => s.status === "absent"))
|
||||||
dots.push("bg-rose-400");
|
// dots.push("bg-rose-400");
|
||||||
map[dateStr] = dots.slice(0, 3);
|
map[date] = dots.slice(0, 3);
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, [weekDates, getShiftsForDate]);
|
}, [weekDates, getShiftsForDate]);
|
||||||
|
|
||||||
const selectedDateObj = useMemo(() => {
|
const selectedDateStr = useMemo(() => {
|
||||||
const inWeek = weekDates.find((d) => formatDateISO(d) === selectedDate);
|
const inWeek = weekDates.find((d) => formatDateISO(d) === selectedDate);
|
||||||
return inWeek ?? weekDates[0] ?? currentDate;
|
return inWeek ?? weekDates[0] ?? currentDate;
|
||||||
}, [selectedDate, weekDates, currentDate]);
|
}, [selectedDate, weekDates, currentDate]);
|
||||||
|
|
||||||
const selectedDateStr = formatDateISO(selectedDateObj);
|
|
||||||
|
|
||||||
const renderMobileDayView = mobileCalendarHeader && (
|
const renderMobileDayView = mobileCalendarHeader && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="rounded-xl border border-(--color-border-light) bg-white p-3">
|
<div className="rounded-xl border border-(--color-border-light) bg-white p-3">
|
||||||
@@ -116,14 +112,13 @@ export default function WeeklySchedule({
|
|||||||
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
<div className="grid grid-cols-7 gap-1">
|
||||||
{weekDates.map((date, i) => {
|
{weekDates.map((date, i) => {
|
||||||
const dateStr = formatDateISO(date);
|
const active = date === selectedDateStr;
|
||||||
const active = dateStr === selectedDateStr;
|
const dots = statusDotsByDate[date] ?? [];
|
||||||
const dots = statusDotsByDate[dateStr] ?? [];
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedDate(dateStr)}
|
onClick={() => setSelectedDate(date)}
|
||||||
className={`flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none p-1 ${
|
className={`flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none p-1 ${
|
||||||
active ? "bg-(--color-primary)/10" : "bg-transparent"
|
active ? "bg-(--color-primary)/10" : "bg-transparent"
|
||||||
}`}
|
}`}
|
||||||
@@ -135,14 +130,14 @@ export default function WeeklySchedule({
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold ${
|
className={`flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold ${
|
||||||
active || isToday(date)
|
active || isToday(new Date(date))
|
||||||
? "bg-indigo-500 text-white"
|
? "bg-indigo-500 text-white"
|
||||||
: i >= 5
|
: i >= 5
|
||||||
? "text-pink-500"
|
? "text-pink-500"
|
||||||
: "text-sky-500"
|
: "text-sky-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{date.getDate()}
|
{new Date(date).getDate()}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex min-h-2 items-center gap-0.5">
|
<div className="flex min-h-2 items-center gap-0.5">
|
||||||
{dots.map((dot, idx) => (
|
{dots.map((dot, idx) => (
|
||||||
@@ -160,7 +155,7 @@ export default function WeeklySchedule({
|
|||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{DEPARTMENTS.map((dept) => {
|
{DEPARTMENTS.map((dept) => {
|
||||||
const deptShifts = getShiftsForDate(selectedDateObj)
|
const deptShifts = getShiftsForDate(new Date(selectedDateStr));
|
||||||
// .filter(
|
// .filter(
|
||||||
// (s) => s.department === dept.id,
|
// (s) => s.department === dept.id,
|
||||||
// );
|
// );
|
||||||
@@ -248,7 +243,7 @@ export default function WeeklySchedule({
|
|||||||
</td>
|
</td>
|
||||||
{weekDates.map((date, i) => {
|
{weekDates.map((date, i) => {
|
||||||
const dateStr = formatDateISO(date);
|
const dateStr = formatDateISO(date);
|
||||||
const shifts = getShiftsForDate(date)
|
const shifts = getShiftsForDate(date);
|
||||||
// .filter(
|
// .filter(
|
||||||
// (s) => s.department === dept.id,
|
// (s) => s.department === dept.id,
|
||||||
// );
|
// );
|
||||||
|
|||||||
+12
-14
@@ -12,10 +12,8 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { eateryClient } from "./apollo-clients";
|
import { eateryClient } from "./apollo-clients";
|
||||||
import { MOCK_SHIFT_SLOTS } from "./constants";
|
|
||||||
import {
|
import {
|
||||||
type ShiftEntity,
|
type ShiftEntity,
|
||||||
type ShiftStatus,
|
|
||||||
type allShiftsQuery,
|
type allShiftsQuery,
|
||||||
createShiftMutation,
|
createShiftMutation,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -52,9 +50,9 @@ interface ShiftContextType {
|
|||||||
// Helpers
|
// Helpers
|
||||||
getShiftsForDate: (date: Date) => ShiftEntity[];
|
getShiftsForDate: (date: Date) => ShiftEntity[];
|
||||||
getShiftsForWeek: (weekStart: Date) => ShiftEntity[];
|
getShiftsForWeek: (weekStart: Date) => ShiftEntity[];
|
||||||
getWeekDates: () => Date[];
|
getWeekDates: () => string[];
|
||||||
hasConflict: (
|
hasConflict: (
|
||||||
date: Date,
|
date: string,
|
||||||
startTime: string,
|
startTime: string,
|
||||||
endTime: string,
|
endTime: string,
|
||||||
staffId: string,
|
staffId: string,
|
||||||
@@ -197,12 +195,12 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// ── Query helpers ───────────────────────────────────────────────────────
|
// ── Query helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const getWeekDates = useCallback((): Date[] => {
|
const getWeekDates = useCallback((): string[] => {
|
||||||
const monday = getMonday(currentDate);
|
const monday = getMonday(currentDate);
|
||||||
return Array.from({ length: 7 }, (_, i) => {
|
return Array.from({ length: 7 }, (_, i) => {
|
||||||
const d = new Date(monday);
|
const d = new Date(monday);
|
||||||
d.setDate(monday.getDate() + i);
|
d.setDate(monday.getDate() + i);
|
||||||
return d;
|
return d.toISOString();
|
||||||
});
|
});
|
||||||
}, [currentDate]);
|
}, [currentDate]);
|
||||||
|
|
||||||
@@ -220,7 +218,7 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
const dates = Array.from({ length: 7 }, (_, i) => {
|
const dates = Array.from({ length: 7 }, (_, i) => {
|
||||||
const d = new Date(weekStart);
|
const d = new Date(weekStart);
|
||||||
d.setDate(weekStart.getDate() + i);
|
d.setDate(weekStart.getDate() + i);
|
||||||
return d;
|
return d.toISOString();
|
||||||
});
|
});
|
||||||
return shifts.filter((s) => dates.includes(s.date));
|
return shifts.filter((s) => dates.includes(s.date));
|
||||||
},
|
},
|
||||||
@@ -229,7 +227,7 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const hasConflict = useCallback(
|
const hasConflict = useCallback(
|
||||||
(
|
(
|
||||||
date: Date,
|
date: string,
|
||||||
startTime: string,
|
startTime: string,
|
||||||
endTime: string,
|
endTime: string,
|
||||||
staffId: string,
|
staffId: string,
|
||||||
@@ -239,7 +237,7 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
(s) =>
|
(s) =>
|
||||||
s.date === date &&
|
s.date === date &&
|
||||||
s.id !== excludeShiftId &&
|
s.id !== excludeShiftId &&
|
||||||
s.registeredStaff.some((rs) => rs.id === staffId) &&
|
s.registeredStaff!.some((rs) => rs.id === staffId) &&
|
||||||
timesOverlap(startTime, endTime, s.startTime, s.endTime),
|
timesOverlap(startTime, endTime, s.startTime, s.endTime),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -249,8 +247,8 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
const getWeeklyBudget = useCallback((): number => {
|
const getWeeklyBudget = useCallback((): number => {
|
||||||
const weekDates = getWeekDates();
|
const weekDates = getWeekDates();
|
||||||
return shifts
|
return shifts
|
||||||
.filter((s) => weekDates.includes(s.date) && s.registeredStaff.length > 0)
|
.filter((s) => weekDates.includes(s.date) && s.registeredStaff!.length > 0)
|
||||||
.reduce((sum, s) => sum + s.wage * s.registeredStaff.length, 0);
|
.reduce((sum, s) => sum + s.wage * s.registeredStaff!.length, 0);
|
||||||
}, [shifts, getWeekDates]);
|
}, [shifts, getWeekDates]);
|
||||||
|
|
||||||
// ── Shift actions ───────────────────────────────────────────────────────
|
// ── Shift actions ───────────────────────────────────────────────────────
|
||||||
@@ -264,11 +262,11 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
const shift = shifts.find((s) => s.id === shiftId);
|
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) return { success: false, error: "Ca làm không tồn tại." };
|
||||||
|
|
||||||
if (shift.registeredStaff.length >= shift.maxStaff) {
|
if (shift.registeredStaff!.length >= shift.maxStaff) {
|
||||||
return { success: false, error: "Ca làm đã đủ người." };
|
return { success: false, error: "Ca làm đã đủ người." };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shift.registeredStaff.some((rs) => rs.id === staffId)) {
|
if (shift.registeredStaff!.some((rs) => rs.id === staffId)) {
|
||||||
return { success: false, error: "Bạn đã đăng ký ca này rồi." };
|
return { success: false, error: "Bạn đã đăng ký ca này rồi." };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +309,7 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
|||||||
setShifts((prev) =>
|
setShifts((prev) =>
|
||||||
prev.map((s) => {
|
prev.map((s) => {
|
||||||
if (s.id !== shiftId) return s;
|
if (s.id !== shiftId) return s;
|
||||||
const updated = s.registeredStaff.filter((rs) => rs.id !== staffId);
|
const updated = s.registeredStaff!.filter((rs) => rs.id !== staffId);
|
||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
registeredStaff: updated,
|
registeredStaff: updated,
|
||||||
|
|||||||
Reference in New Issue
Block a user