b37bf5d088
Release package / release (push) Successful in 4m38s
Co-authored-by: Thanh Quy - wolf <524H0124@student.tdtu.edu.vn> Co-authored-by: Thanh Quy- wolf <524H0124@student.tdtu.edu.vn> Reviewed-on: #43
239 lines
11 KiB
TypeScript
239 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import ShiftCard from "@/components/molecules/cards/ShiftCard";
|
|
import { DEPARTMENTS } from "@/lib/constants";
|
|
import { useShift } from "@/lib/shift-context";
|
|
import { useMemo, useState } from "react";
|
|
|
|
import type { WeeklyScheduleProps } from "./ShiftSchedule.types";
|
|
|
|
const MONTH_NAMES = [
|
|
"January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December",
|
|
];
|
|
|
|
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
|
|
function formatDateShort(d: Date): string {
|
|
return `${d.getDate().toString().padStart(2, "0")}/${(d.getMonth() + 1).toString().padStart(2, "0")}`;
|
|
}
|
|
|
|
function isToday(d: Date): boolean {
|
|
const today = new Date(Date.now());
|
|
return (
|
|
d.getDate() === today.getDate() &&
|
|
d.getMonth() === today.getMonth() &&
|
|
d.getFullYear() === today.getFullYear()
|
|
);
|
|
}
|
|
|
|
export default function WeeklySchedule({
|
|
onShiftClick,
|
|
onCreateShift,
|
|
mobileCalendarHeader = false,
|
|
}: WeeklyScheduleProps) {
|
|
const { currentDate, getWeekDates, getShiftsForDate, goToNextWeek, goToPrevWeek } = useShift();
|
|
const weekDates = getWeekDates();
|
|
|
|
const [selectedDate, setSelectedDate] = useState<Date>(weekDates[0] ?? currentDate);
|
|
|
|
const selectedDateRef = useMemo(() => {
|
|
return weekDates.find((d) => d === selectedDate) ?? weekDates[0] ?? currentDate;
|
|
}, [selectedDate, weekDates, currentDate]);
|
|
|
|
// ── Mobile calendar header view ────────────────────────────────────────────
|
|
|
|
if (mobileCalendarHeader) {
|
|
const dayShifts = getShiftsForDate(new Date(selectedDateRef));
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Week strip */}
|
|
<div className="overflow-hidden rounded-2xl border border-(--color-border-light) bg-white shadow-sm">
|
|
<div className="flex items-center justify-between border-b border-(--color-border-light) bg-gradient-to-r from-(--color-primary)/5 to-transparent px-4 py-3">
|
|
<button
|
|
title="Previous week"
|
|
type="button"
|
|
onClick={goToPrevWeek}
|
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-white/80 text-(--color-text-muted) shadow-sm transition hover:bg-white hover:text-(--color-primary)"
|
|
>
|
|
<i className="fa-solid fa-chevron-left text-xs"></i>
|
|
</button>
|
|
<h3 className="text-sm font-bold text-(--color-primary-dark)">
|
|
{MONTH_NAMES[currentDate.getMonth()]} {currentDate.getFullYear()}
|
|
</h3>
|
|
<button
|
|
title="Next week"
|
|
type="button"
|
|
onClick={goToNextWeek}
|
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border-none bg-white/80 text-(--color-text-muted) shadow-sm transition hover:bg-white hover:text-(--color-primary)"
|
|
>
|
|
<i className="fa-solid fa-chevron-right text-xs"></i>
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-7 gap-1 p-3">
|
|
{weekDates.map((date, i) => {
|
|
const active = date === selectedDateRef;
|
|
const today = isToday(new Date(date));
|
|
const shifts = getShiftsForDate(new Date(date));
|
|
return (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => setSelectedDate(date)}
|
|
className="flex cursor-pointer flex-col items-center gap-1 rounded-xl border-none bg-transparent py-1.5 transition"
|
|
>
|
|
<span className={`text-[10px] font-semibold uppercase ${i >= 5 ? "text-rose-400" : "text-(--color-text-muted)"}`}>
|
|
{DAY_LABELS[i]}
|
|
</span>
|
|
<span className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold transition ${
|
|
today
|
|
? "bg-(--color-primary) text-white shadow-sm"
|
|
: active
|
|
? "bg-(--color-primary)/15 text-(--color-primary)"
|
|
: i >= 5
|
|
? "text-rose-500"
|
|
: "text-(--color-text-secondary)"
|
|
}`}>
|
|
{new Date(date).getDate()}
|
|
</span>
|
|
<div className="flex min-h-2 items-center gap-px">
|
|
{shifts.length > 0 && (
|
|
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)/60" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Day shifts */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between px-1">
|
|
<h3 className="text-sm font-bold text-(--color-primary-dark)">
|
|
{DAY_LABELS[(new Date(selectedDateRef).getDay() + 6) % 7]},{" "}
|
|
{formatDateShort(new Date(selectedDateRef))}
|
|
</h3>
|
|
<span className="rounded-full bg-(--color-primary)/10 px-2.5 py-1 text-xs font-semibold text-(--color-primary)">
|
|
{dayShifts.length} shifts
|
|
</span>
|
|
</div>
|
|
|
|
{dayShifts.length === 0 && !onCreateShift ? (
|
|
<div className="rounded-2xl border border-dashed border-(--color-border-light) py-10 text-center">
|
|
<i className="fa-regular fa-calendar-xmark mb-2 text-3xl text-gray-200"></i>
|
|
<p className="text-sm text-(--color-text-muted)">No shifts today</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2.5">
|
|
{dayShifts.map((shift) => (
|
|
<ShiftCard key={shift.id} shift={shift} onClick={onShiftClick} />
|
|
))}
|
|
{onCreateShift && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onCreateShift(selectedDateRef)}
|
|
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-2xl border border-dashed border-(--color-border-light) bg-transparent py-3 text-sm font-medium text-(--color-text-muted) transition hover:border-(--color-primary) hover:text-(--color-primary)"
|
|
>
|
|
<i className="fa-solid fa-plus"></i>
|
|
Add shift
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Desktop table view ──────────────────────────────────────────────────────
|
|
|
|
return (
|
|
<div className="overflow-hidden rounded-2xl border border-(--color-border-light) shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[800px] border-collapse">
|
|
<thead>
|
|
<tr>
|
|
<th className="w-28 border-r border-b border-(--color-border-light) bg-gray-50/80 px-4 py-3 text-left text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
|
|
Department
|
|
</th>
|
|
{weekDates.map((date, i) => {
|
|
const today = isToday(new Date(date));
|
|
return (
|
|
<th
|
|
key={i}
|
|
className={`border-r border-b border-(--color-border-light) px-3 py-3 text-center text-xs transition ${
|
|
today
|
|
? "bg-gradient-to-b from-(--color-primary)/15 to-(--color-primary)/5 font-bold text-(--color-primary)"
|
|
: "bg-gray-50/80 font-semibold text-(--color-text-muted)"
|
|
}`}
|
|
>
|
|
<span className="block text-[11px] font-semibold uppercase tracking-wider">
|
|
{DAY_LABELS[i]}
|
|
</span>
|
|
<span className={`mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-full text-sm font-bold ${
|
|
today
|
|
? "bg-(--color-primary) text-white shadow-sm"
|
|
: "text-(--color-text-secondary)"
|
|
}`}>
|
|
{date.getDate()}
|
|
</span>
|
|
<span className={`mt-0.5 block text-[10px] font-normal ${today ? "text-(--color-primary)/70" : "text-(--color-text-muted)"}`}>
|
|
{(date.getMonth() + 1).toString().padStart(2, "0")}/{date.getFullYear()}
|
|
</span>
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{DEPARTMENTS.map((dept, deptIdx) => (
|
|
<tr key={dept.id} className={deptIdx % 2 === 0 ? "bg-white" : "bg-gray-50/40"}>
|
|
<td className="border-r border-b border-(--color-border-light) px-4 py-3 align-top">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-(--color-primary)/10">
|
|
<i className={`${dept.icon} text-xs text-(--color-primary)`}></i>
|
|
</div>
|
|
<span className="text-xs font-semibold text-(--color-text-secondary)">
|
|
{dept.name}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
{weekDates.map((date, i) => {
|
|
const today = isToday(new Date(date));
|
|
const shifts = getShiftsForDate(new Date(date));
|
|
return (
|
|
<td
|
|
key={i}
|
|
className={`border-r border-b border-(--color-border-light) p-2 align-top transition ${
|
|
today ? "bg-(--color-primary)/5" : ""
|
|
}`}
|
|
>
|
|
<div className="flex min-h-[88px] flex-col gap-1.5">
|
|
{shifts.map((shift) => (
|
|
<ShiftCard key={shift.id} shift={shift} compact onClick={onShiftClick} />
|
|
))}
|
|
{onCreateShift && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onCreateShift(date)}
|
|
className="mt-auto flex cursor-pointer items-center justify-center gap-1 rounded-lg border border-dashed border-gray-200 bg-transparent py-1.5 text-[10px] font-medium text-gray-300 transition hover:border-(--color-primary)/50 hover:text-(--color-primary)"
|
|
>
|
|
<i className="fa-solid fa-plus"></i>
|
|
Add
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|