Files
frontend/app/(staff)/staff/schedule/page.tsx
T
Thanh Quy - wolf 54d5d6150c
Release package / release (pull_request) Successful in 3m23s
Refactor: Update Vietnamese text to English across multiple components
- Translated payment page text from Vietnamese to English.
- Updated staff creation page with English labels and messages.
- Changed staff schedule page labels and navigation items to English.
- Modified shift card and search bar components to use English text.
- Updated delete confirmation and product modal components with English messages.
- Translated product management tab and category sidebar to English.
- Changed shift schedule components to display English text for shifts and actions.
- Updated header logo alt text to improve accessibility.
2026-05-14 21:09:18 +07:00

401 lines
18 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 { 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>
);
}