Files
frontend/app/(staff)/staff/schedule/page.tsx
T
2026-04-14 03:05:52 +00:00

387 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import MobileShiftView from "@/components/organisms/shift-schedule/MobileShiftView";
import MonthlyCalendar from "@/components/organisms/shift-schedule/MonthlyCalendar";
import ShiftCreateModal from "@/components/organisms/shift-schedule/ShiftCreateModal";
import ShiftDetailModal from "@/components/organisms/shift-schedule/ShiftDetailModal";
import WeeklySchedule from "@/components/organisms/shift-schedule/WeeklySchedule";
import { useAuth } from "@/lib/auth-context";
import { useShift } from "@/lib/shift-context";
import type { ShiftSlot } from "@/lib/types";
import Link from "next/link";
import { useState } from "react";
const MONTH_NAMES = [
"Tháng 1",
"Tháng 2",
"Tháng 3",
"Tháng 4",
"Tháng 5",
"Tháng 6",
"Tháng 7",
"Tháng 8",
"Tháng 9",
"Tháng 10",
"Tháng 11",
"Tháng 12",
];
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);
return date;
}
function formatDateShort(d: Date): string {
return `${d.getDate().toString().padStart(2, "0")}/${(d.getMonth() + 1).toString().padStart(2, "0")}`;
}
export default function StaffSchedulePage() {
const { user, logout } = useAuth();
const {
view,
setView,
currentDate,
goToNextWeek,
goToPrevWeek,
goToNextMonth,
goToPrevMonth,
goToToday,
getWeeklyBudget,
} = useShift();
const [selectedShift, setSelectedShift] = useState<ShiftSlot | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [createDate, setCreateDate] = useState<string | undefined>();
const isManager = user?.role === "manager";
const handleShiftClick = (shift: ShiftSlot) => {
setSelectedShift(shift);
setDetailOpen(true);
};
const handleCreateShift = (date: string) => {
setCreateDate(date);
setCreateOpen(true);
};
const handleDateSelect = (date: string) => {
// In month view on desktop, clicking a date could open create modal for managers
if (isManager) {
setCreateDate(date);
setCreateOpen(true);
}
};
// Week range label
const monday = getMonday(currentDate);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
const weekLabel = `${formatDateShort(monday)} ${formatDateShort(sunday)}`;
const weeklyBudget = getWeeklyBudget();
return (
<div className="flex min-h-screen">
{/* ── Sidebar (Desktop) ── */}
<aside className="hidden w-64 shrink-0 flex-col border-r border-(--color-border-light) bg-white shadow-sm lg:flex">
{/* Brand */}
<div className="flex items-center gap-3 border-b border-(--color-border-light) px-5 py-5">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-(--color-primary)">
<i className="fa-solid fa-calendar-days text-sm text-white"></i>
</div>
<div>
<p className="text-foreground text-sm font-bold">Lịch làm việc</p>
<p className="text-xs text-(--color-text-muted)">
{isManager ? "Manager" : "Staff"}
</p>
</div>
</div>
{/* View toggle */}
<nav className="flex-1 space-y-1 p-3">
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
Chế độ xem
</p>
<button
type="button"
onClick={() => setView("week")}
className={`flex w-full cursor-pointer items-center gap-3 rounded-xl border-none px-3 py-2.5 text-sm font-medium transition-all ${
view === "week"
? "bg-(--color-primary) text-white shadow-sm"
: "hover:bg-background bg-transparent text-(--color-text-secondary) hover:text-(--color-primary-dark)"
}`}
>
<i className="fa-solid fa-table-columns w-4 text-center"></i>
<span className="flex-1 text-left">Theo tuần</span>
</button>
<button
type="button"
onClick={() => setView("month")}
className={`flex w-full cursor-pointer items-center gap-3 rounded-xl border-none px-3 py-2.5 text-sm font-medium transition-all ${
view === "month"
? "bg-(--color-primary) text-white shadow-sm"
: "hover:bg-background bg-transparent text-(--color-text-secondary) hover:text-(--color-primary-dark)"
}`}
>
<i className="fa-solid fa-calendar w-4 text-center"></i>
<span className="flex-1 text-left">Theo tháng</span>
</button>
{/* Quick nav */}
<div className="mt-3 border-t border-(--color-border-light) pt-3">
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
Điều hướng
</p>
<button
type="button"
onClick={goToToday}
className="hover:bg-background flex w-full cursor-pointer items-center gap-3 rounded-xl border-none bg-transparent px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) transition-all hover:text-(--color-primary-dark)"
>
<i className="fa-solid fa-crosshairs w-4 text-center"></i>
<span className="flex-1 text-left">Hôm nay</span>
</button>
</div>
{/* Manager link */}
{isManager && (
<div className="mt-3 border-t border-(--color-border-light) pt-3">
<p className="mb-2 px-3 text-[11px] font-semibold tracking-wider text-(--color-text-muted) uppercase">
Quản
</p>
<Link
href="/manager"
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark)"
>
<i className="fa-solid fa-store w-4 text-center"></i>
<span className="flex-1 text-left">Dashboard</span>
</Link>
</div>
)}
{/* Weekly budget */}
<div className="mt-3 border-t border-(--color-border-light) pt-3">
<div className="rounded-xl bg-(--color-primary)/5 p-3">
<p className="text-[10px] font-semibold text-(--color-text-muted) uppercase">
Ngân sách tuần
</p>
<p className="mt-1 text-lg font-bold text-(--color-primary)">
{weeklyBudget.toLocaleString("vi-VN")}
</p>
<p className="text-[10px] text-(--color-text-muted)">VND</p>
</div>
</div>
</nav>
{/* User info */}
<div className="border-t border-(--color-border-light) p-3">
<div className="flex items-center gap-3 rounded-xl p-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-(--color-accent-light)">
<i
className={`fa-solid ${isManager ? "fa-user-tie" : "fa-user"} text-sm text-(--color-primary)`}
></i>
</div>
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-semibold">
{user?.name ?? "Nhân viên"}
</p>
<p className="text-xs text-(--color-text-muted)">
{isManager ? "Quản lý" : "Nhân viên"}
</p>
</div>
</div>
<div className="mt-1 flex gap-2 px-1">
<Link
href="/"
className="hover:bg-background flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-(--color-border-light) bg-transparent py-2 text-xs font-medium text-(--color-text-secondary) no-underline transition"
>
<i className="fa-solid fa-house"></i>
Trang chủ
</Link>
<button
type="button"
onClick={logout}
className="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-xl border-none bg-transparent py-2 text-xs font-medium text-red-500 transition hover:bg-red-50"
>
<i className="fa-solid fa-right-from-bracket"></i>
Đăng xuất
</button>
</div>
</div>
</aside>
{/* ── Main content ── */}
<div className="flex min-w-0 flex-1 flex-col">
{/* Header */}
<header className="sticky top-0 z-40 flex items-center justify-between border-b border-(--color-border-light) bg-white px-4 py-3 shadow-sm md:px-5 md:py-4">
<div>
<h1 className="text-foreground text-base font-bold md:text-lg">
Đăng ca làm
</h1>
<p className="text-xs text-(--color-text-muted)">
{view === "week"
? weekLabel
: `${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`}
</p>
</div>
<div className="flex items-center gap-2">
{/* View toggle (mobile) */}
<div className="flex items-center rounded-xl border border-(--color-border-light) lg:hidden">
<button
type="button"
onClick={() => setView("week")}
className={`cursor-pointer rounded-l-xl border-none px-3 py-2 text-xs font-medium transition ${
view === "week"
? "bg-(--color-primary) text-white"
: "bg-transparent text-(--color-text-secondary)"
}`}
>
Tuần
</button>
<button
type="button"
onClick={() => setView("month")}
className={`cursor-pointer rounded-r-xl border-none px-3 py-2 text-xs font-medium transition ${
view === "month"
? "bg-(--color-primary) text-white"
: "bg-transparent text-(--color-text-secondary)"
}`}
>
Tháng
</button>
</div>
{/* Navigation arrows */}
<div className="hidden items-center gap-1 md:flex">
<button
title="Về trước"
type="button"
onClick={view === "week" ? goToPrevWeek : goToPrevMonth}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:bg-gray-50"
>
<i className="fa-solid fa-chevron-left text-xs"></i>
</button>
<button
type="button"
onClick={goToToday}
className="cursor-pointer rounded-lg border border-(--color-border-light) bg-transparent px-3 py-1.5 text-xs font-medium text-(--color-text-secondary) transition hover:bg-gray-50"
>
Hôm nay
</button>
<button
title="Tiếp theo"
type="button"
onClick={view === "week" ? goToNextWeek : goToNextMonth}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:bg-gray-50"
>
<i className="fa-solid fa-chevron-right text-xs"></i>
</button>
</div>
{/* Create shift button (manager only) */}
{isManager && (
<button
type="button"
onClick={() => {
setCreateDate(undefined);
setCreateOpen(true);
}}
className="hidden cursor-pointer items-center gap-1.5 rounded-xl border-none bg-(--color-primary) px-3 py-2 text-xs font-semibold text-white transition hover:opacity-90 md:flex"
>
<i className="fa-solid fa-plus"></i>
Tạo ca
</button>
)}
{/* Mobile nav */}
<div className="flex items-center gap-1 md:hidden">
<button
title="Về trước"
type="button"
onClick={view === "week" ? goToPrevWeek : goToPrevMonth}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--color-text-muted)"
>
<i className="fa-solid fa-chevron-left text-xs"></i>
</button>
<button
title="Tiếp theo"
type="button"
onClick={view === "week" ? goToNextWeek : goToNextMonth}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--color-text-muted)"
>
<i className="fa-solid fa-chevron-right text-xs"></i>
</button>
</div>
</div>
</header>
{/* Content */}
<main className="flex-1 p-4 md:p-6">
{/* Desktop views */}
<div className="hidden md:block">
{view === "week" ? (
<WeeklySchedule
onShiftClick={handleShiftClick}
onCreateShift={isManager ? handleCreateShift : undefined}
/>
) : (
<MonthlyCalendar
onShiftClick={handleShiftClick}
onDateSelect={isManager ? handleDateSelect : undefined}
/>
)}
</div>
{/* Mobile view */}
<div className="md:hidden">
{view === "week" ? (
<WeeklySchedule
onShiftClick={handleShiftClick}
onCreateShift={isManager ? handleCreateShift : undefined}
mobileCalendarHeader
/>
) : (
<MobileShiftView onShiftClick={handleShiftClick} />
)}
</div>
{/* Mobile FAB for manager */}
{isManager && (
<button
title="Tạo ca"
type="button"
onClick={() => {
setCreateDate(undefined);
setCreateOpen(true);
}}
className="fixed right-4 bottom-4 z-30 flex h-14 w-14 cursor-pointer items-center justify-center rounded-full border-none bg-(--color-primary) text-white shadow-lg transition hover:opacity-90 md:hidden"
>
<i className="fa-solid fa-plus text-lg"></i>
</button>
)}
</main>
</div>
{/* Modals */}
<ShiftDetailModal
shift={selectedShift}
isOpen={detailOpen}
onClose={() => {
setDetailOpen(false);
setSelectedShift(null);
}}
/>
<ShiftCreateModal
isOpen={createOpen}
onClose={() => setCreateOpen(false)}
defaultDate={createDate}
/>
</div>
);
}