440 lines
18 KiB
TypeScript
440 lines
18 KiB
TypeScript
"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 { ShiftEntity } from "@/lib/types";
|
||
import Link from "next/link";
|
||
import { useState } from "react";
|
||
|
||
const MONTH_NAMES = [
|
||
"January",
|
||
"February",
|
||
"March",
|
||
"April",
|
||
"May",
|
||
"June",
|
||
"July",
|
||
"August",
|
||
"September",
|
||
"October",
|
||
"November",
|
||
"December",
|
||
];
|
||
|
||
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,
|
||
shifts,
|
||
} = useShift();
|
||
|
||
const [selectedShift, setSelectedShift] = useState<ShiftEntity | null>(null);
|
||
const [detailOpen, setDetailOpen] = useState(false);
|
||
const [createOpen, setCreateOpen] = useState(false);
|
||
const [createDate, setCreateDate] = useState<Date | undefined>();
|
||
|
||
const isManager = user?.role === "manager";
|
||
|
||
const handleShiftClick = (shift: ShiftEntity) => {
|
||
setSelectedShift(shift);
|
||
setDetailOpen(true);
|
||
};
|
||
|
||
const handleCreateShift = (date: Date) => {
|
||
setCreateDate(date);
|
||
setCreateOpen(true);
|
||
};
|
||
|
||
const handleDateSelect = (date: Date) => {
|
||
if (isManager) {
|
||
setCreateDate(date);
|
||
setCreateOpen(true);
|
||
}
|
||
};
|
||
|
||
const monday = getMonday(currentDate);
|
||
const sunday = new Date(monday);
|
||
sunday.setDate(monday.getDate() + 6);
|
||
const weekLabel = `${formatDateShort(monday)} – ${formatDateShort(sunday)}`;
|
||
const weeklyBudget = getWeeklyBudget();
|
||
|
||
// Count shifts this week for stats
|
||
const totalShiftsThisWeek = shifts.filter((s) => {
|
||
if (!s.date) return false;
|
||
const d =
|
||
s.date instanceof Date ? s.date : new Date(s.date as unknown as string);
|
||
return d >= monday && d <= sunday;
|
||
}).length;
|
||
|
||
return (
|
||
<div className="flex min-h-screen bg-gray-50/50">
|
||
{/* ── Sidebar (Desktop) ─────────────────────────────────────────────────── */}
|
||
<aside className="hidden w-64 shrink-0 flex-col border-r border-(--color-border-light) bg-white shadow-sm lg:flex">
|
||
{/* Brand — gradient accent */}
|
||
<div className="bg-gradient-to-br from-(--color-primary) to-(--color-primary-dark) px-5 py-5">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/20 shadow-sm backdrop-blur-sm">
|
||
<i className="fa-solid fa-calendar-days text-base text-white"></i>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-bold text-white">Work Schedule</p>
|
||
<p className="text-xs text-white/70">
|
||
{isManager ? "Manager View" : "Staff View"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<nav className="flex-1 space-y-0.5 overflow-y-auto p-3">
|
||
{/* View toggle */}
|
||
<p className="mb-1 px-3 pt-2 text-[10px] font-bold tracking-widest text-(--color-text-muted) uppercase">
|
||
View
|
||
</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)/10 text-(--color-primary) shadow-sm"
|
||
: "bg-transparent text-(--color-text-secondary) hover:bg-gray-50 hover:text-(--color-primary-dark)"
|
||
}`}
|
||
>
|
||
<i
|
||
className={`fa-solid fa-table-columns w-4 text-center ${view === "week" ? "text-(--color-primary)" : ""}`}
|
||
></i>
|
||
<span className="flex-1 text-left">Weekly</span>
|
||
{view === "week" && (
|
||
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)"></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)/10 text-(--color-primary) shadow-sm"
|
||
: "bg-transparent text-(--color-text-secondary) hover:bg-gray-50 hover:text-(--color-primary-dark)"
|
||
}`}
|
||
>
|
||
<i
|
||
className={`fa-solid fa-calendar w-4 text-center ${view === "month" ? "text-(--color-primary)" : ""}`}
|
||
></i>
|
||
<span className="flex-1 text-left">Monthly</span>
|
||
{view === "month" && (
|
||
<span className="h-1.5 w-1.5 rounded-full bg-(--color-primary)"></span>
|
||
)}
|
||
</button>
|
||
|
||
{/* Navigation */}
|
||
<div className="mt-2 pt-2">
|
||
<p className="mb-1 px-3 text-[10px] font-bold tracking-widest text-(--color-text-muted) uppercase">
|
||
Navigation
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={goToToday}
|
||
className="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:bg-gray-50 hover:text-(--color-primary-dark)"
|
||
>
|
||
<i className="fa-solid fa-crosshairs w-4 text-center"></i>
|
||
<span className="flex-1 text-left">Today</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Manager link */}
|
||
{isManager && (
|
||
<div className="mt-2 pt-2">
|
||
<p className="mb-1 px-3 text-[10px] font-bold tracking-widest text-(--color-text-muted) uppercase">
|
||
Manager
|
||
</p>
|
||
<Link
|
||
href="/manager"
|
||
className="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:bg-gray-50 hover:text-(--color-primary-dark)"
|
||
>
|
||
<i className="fa-solid fa-store w-4 text-center"></i>
|
||
<span className="flex-1 text-left">Dashboard</span>
|
||
<i className="fa-solid fa-arrow-right text-xs opacity-40"></i>
|
||
</Link>
|
||
{/* Create shift button */}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setCreateDate(undefined);
|
||
setCreateOpen(true);
|
||
}}
|
||
className="mt-1 flex w-full cursor-pointer items-center gap-3 rounded-xl border-none bg-(--color-primary)/10 px-3 py-2.5 text-sm font-semibold text-(--color-primary) transition-all hover:bg-(--color-primary)/20"
|
||
>
|
||
<i className="fa-solid fa-plus w-4 text-center"></i>
|
||
<span className="flex-1 text-left">Create shift</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Stats cards */}
|
||
<div className="mt-3 space-y-2 border-t border-(--color-border-light) pt-3">
|
||
{isManager && (
|
||
<div className="rounded-xl bg-gradient-to-br from-(--color-primary)/10 to-(--color-primary)/5 p-3">
|
||
<p className="text-[10px] font-bold tracking-wider text-(--color-text-muted) uppercase">
|
||
Weekly budget
|
||
</p>
|
||
<p className="mt-1 text-xl font-bold text-(--color-primary)">
|
||
{weeklyBudget >= 1_000_000
|
||
? `${(weeklyBudget / 1_000_000).toFixed(1)}M`
|
||
: weeklyBudget.toLocaleString("vi-VN")}
|
||
</p>
|
||
<p className="text-[10px] text-(--color-text-muted)">VND</p>
|
||
</div>
|
||
)}
|
||
<div className="rounded-xl bg-gray-50 p-3">
|
||
<p className="text-[10px] font-bold tracking-wider text-(--color-text-muted) uppercase">
|
||
Shifts this week
|
||
</p>
|
||
<p className="mt-1 text-xl font-bold text-(--color-text-secondary)">
|
||
{totalShiftsThisWeek}
|
||
</p>
|
||
<p className="text-[10px] text-(--color-text-muted)">shifts</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 bg-gray-50 p-3">
|
||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-(--color-primary)/15">
|
||
<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 ?? "Staff"}
|
||
</p>
|
||
<span
|
||
className={`inline-block rounded-full px-2 py-px text-[10px] font-semibold ${
|
||
isManager
|
||
? "bg-(--color-primary)/10 text-(--color-primary)"
|
||
: "bg-gray-100 text-(--color-text-muted)"
|
||
}`}
|
||
>
|
||
{isManager ? "Manager" : "Staff"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-2 flex gap-2 px-1">
|
||
<Link
|
||
href="/"
|
||
className="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 hover:bg-gray-50"
|
||
>
|
||
<i className="fa-solid fa-house text-[10px]"></i>
|
||
Home
|
||
</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 text-[10px]"></i>
|
||
Logout
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* ── Main content ──────────────────────────────────────────────────────── */}
|
||
<div className="flex min-w-0 flex-1 flex-col">
|
||
{/* Header */}
|
||
<header className="sticky top-0 z-40 border-b border-(--color-border-light) bg-white/95 px-4 py-3 shadow-sm backdrop-blur-sm md:px-6 md:py-4">
|
||
<div className="flex items-center justify-between gap-3">
|
||
{/* Title */}
|
||
<div>
|
||
<h1 className="text-foreground text-base font-bold md:text-lg">
|
||
{isManager ? "Manage Shifts" : "Register Shifts"}
|
||
</h1>
|
||
<p className="text-xs text-(--color-text-muted)">
|
||
{view === "week"
|
||
? `Week: ${weekLabel}`
|
||
: `Month: ${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
{/* Mobile view toggle */}
|
||
<div className="flex items-center overflow-hidden 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-semibold transition ${
|
||
view === "week"
|
||
? "bg-(--color-primary) text-white"
|
||
: "bg-transparent text-(--color-text-secondary)"
|
||
}`}
|
||
>
|
||
Week
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setView("month")}
|
||
className={`cursor-pointer rounded-r-xl border-none px-3 py-2 text-xs font-semibold transition ${
|
||
view === "month"
|
||
? "bg-(--color-primary) text-white"
|
||
: "bg-transparent text-(--color-text-secondary)"
|
||
}`}
|
||
>
|
||
Month
|
||
</button>
|
||
</div>
|
||
|
||
{/* Desktop navigation arrows */}
|
||
<div className="hidden items-center gap-1 md:flex">
|
||
<button
|
||
title="Previous"
|
||
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-white text-(--color-text-muted) transition hover:border-(--color-primary)/30 hover:bg-(--color-primary)/5 hover:text-(--color-primary)"
|
||
>
|
||
<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-white px-3 py-1.5 text-xs font-semibold text-(--color-text-secondary) transition hover:border-(--color-primary)/30 hover:bg-(--color-primary)/5 hover:text-(--color-primary)"
|
||
>
|
||
Today
|
||
</button>
|
||
<button
|
||
title="Next"
|
||
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-white text-(--color-text-muted) transition hover:border-(--color-primary)/30 hover:bg-(--color-primary)/5 hover:text-(--color-primary)"
|
||
>
|
||
<i className="fa-solid fa-chevron-right text-xs"></i>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Create shift (manager, desktop) */}
|
||
{isManager && (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setCreateDate(undefined);
|
||
setCreateOpen(true);
|
||
}}
|
||
className="hidden cursor-pointer items-center gap-2 rounded-xl border-none bg-(--color-primary) px-4 py-2 text-xs font-bold text-white shadow-sm transition hover:opacity-90 md:flex"
|
||
>
|
||
<i className="fa-solid fa-plus"></i>
|
||
Create shift
|
||
</button>
|
||
)}
|
||
|
||
{/* Mobile navigation arrows */}
|
||
<div className="flex items-center gap-1 md:hidden">
|
||
<button
|
||
title="Previous"
|
||
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="Next"
|
||
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>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Content */}
|
||
<main className="flex-1 p-4 md:p-6">
|
||
{/* Desktop */}
|
||
<div className="hidden md:block">
|
||
{view === "week" ? (
|
||
<WeeklySchedule
|
||
onShiftClick={handleShiftClick}
|
||
onCreateShift={isManager ? handleCreateShift : undefined}
|
||
/>
|
||
) : (
|
||
<MonthlyCalendar
|
||
onShiftClick={handleShiftClick}
|
||
onDateSelect={isManager ? handleDateSelect : undefined}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* Mobile */}
|
||
<div className="md:hidden">
|
||
{view === "week" ? (
|
||
<WeeklySchedule
|
||
onShiftClick={handleShiftClick}
|
||
onCreateShift={isManager ? handleCreateShift : undefined}
|
||
mobileCalendarHeader
|
||
/>
|
||
) : (
|
||
<MobileShiftView onShiftClick={handleShiftClick} />
|
||
)}
|
||
</div>
|
||
|
||
{/* FAB for mobile manager */}
|
||
{isManager && (
|
||
<button
|
||
title="Create shift"
|
||
type="button"
|
||
onClick={() => {
|
||
setCreateDate(undefined);
|
||
setCreateOpen(true);
|
||
}}
|
||
className="fixed right-4 bottom-6 z-30 flex h-14 w-14 cursor-pointer items-center justify-center rounded-full border-none bg-(--color-primary) text-white shadow-xl 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>
|
||
);
|
||
}
|