feat: enhance manager page with dropdown menu and improve product image handling
This commit is contained in:
+144
-11
@@ -4,10 +4,13 @@ import { ProductsTab } from "@/components/organisms/manager";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
import { useManager } from "@/lib/manager-context";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function ManagerPage() {
|
||||
const { user, logout } = useAuth();
|
||||
const { activeTab, setActiveTab, products } = useManager();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@@ -18,9 +21,22 @@ export default function ManagerPage() {
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
function handleOutsideClick(e: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleOutsideClick);
|
||||
return () => document.removeEventListener("mousedown", handleOutsideClick);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* ── Sidebar ── */}
|
||||
{/* ── Sidebar (lg+) ── */}
|
||||
<aside className="hidden w-64 shrink-0 flex-col border-r border-(--color-border-light) bg-white shadow-sm lg:flex">
|
||||
<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)">
|
||||
@@ -121,12 +137,13 @@ export default function ManagerPage() {
|
||||
|
||||
{/* ── Main content ── */}
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="sticky top-0 z-40 flex items-center justify-between border-b border-(--color-border-light) bg-white px-5 py-4 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-foreground text-lg font-bold">
|
||||
<header className="sticky top-0 z-40 flex items-center justify-between gap-3 border-b border-(--color-border-light) bg-white px-5 py-4 shadow-sm">
|
||||
{/* Title */}
|
||||
<div className="min-w-0 shrink">
|
||||
<h1 className="text-foreground truncate text-lg font-bold">
|
||||
{tabs.find((t) => t.id === activeTab)?.label ?? "Manager"}
|
||||
</h1>
|
||||
<p className="text-xs text-(--color-text-muted)">
|
||||
<p className="truncate text-xs text-(--color-text-muted)">
|
||||
Manage{" "}
|
||||
{activeTab === "products"
|
||||
? "menu items"
|
||||
@@ -139,8 +156,10 @@ export default function ManagerPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mobile tabs */}
|
||||
<div className="flex items-center gap-1 lg:hidden">
|
||||
{/* ── Actions: hidden on lg+ (sidebar handles nav there) ── */}
|
||||
<div className="flex shrink-0 items-center gap-1.5 lg:hidden">
|
||||
{/* sm–lg: inline icon+label buttons */}
|
||||
<div className="hidden items-center gap-1.5 sm:flex">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -152,19 +171,133 @@ export default function ManagerPage() {
|
||||
}`}
|
||||
>
|
||||
<i className={tab.icon}></i>
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<Link
|
||||
href="/manager/analytics"
|
||||
className="flex items-center gap-1.5 rounded-xl border-none bg-(--color-accent-light) px-3 py-2 text-xs font-medium text-(--color-primary) no-underline transition hover:bg-(--color-accent-light)/70"
|
||||
className="flex items-center gap-1.5 rounded-xl border-none bg-(--color-accent-light) px-3 py-2 text-xs font-medium text-(--color-primary) no-underline transition hover:opacity-80"
|
||||
>
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
<span className="hidden sm:inline">Finance</span>
|
||||
<span>Finance</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/staff/schedule"
|
||||
className="hover:bg-background flex items-center gap-1.5 rounded-xl border border-(--color-border-light) bg-transparent px-3 py-2 text-xs font-medium text-(--color-text-secondary) no-underline transition hover:text-(--color-primary-dark)"
|
||||
>
|
||||
<i className="fa-solid fa-calendar-days"></i>
|
||||
<span>Shifts</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:bg-background flex items-center gap-1.5 rounded-xl border border-(--color-border-light) bg-transparent px-3 py-2 text-xs font-medium text-(--color-text-secondary) no-underline transition"
|
||||
>
|
||||
<i className="fa-solid fa-house"></i>
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="flex cursor-pointer items-center gap-1.5 rounded-xl border border-red-200 bg-transparent px-3 py-2 text-xs font-medium text-red-500 transition hover:bg-red-50"
|
||||
>
|
||||
<i className="fa-solid fa-right-from-bracket"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Desktop actions */}
|
||||
{/* < sm: hamburger → dropdown */}
|
||||
<div className="relative sm:hidden" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setDropdownOpen((prev) => !prev)}
|
||||
aria-label="Open menu"
|
||||
className="flex cursor-pointer items-center justify-center rounded-xl border border-(--color-border-light) bg-transparent p-2 text-sm text-(--color-text-secondary) transition hover:bg-(--color-border-light)"
|
||||
>
|
||||
<i
|
||||
className={
|
||||
dropdownOpen ? "fa-solid fa-xmark" : "fa-solid fa-bars"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-52 overflow-hidden rounded-2xl border border-(--color-border-light) bg-white shadow-xl">
|
||||
<div className="p-1.5">
|
||||
{/* Tab buttons */}
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.id);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-xl border-none px-3 py-2.5 text-sm font-medium transition ${
|
||||
activeTab === tab.id
|
||||
? "bg-(--color-primary) text-white"
|
||||
: "bg-transparent text-(--color-text-secondary) hover:bg-(--color-border-light)"
|
||||
}`}
|
||||
>
|
||||
<i className={`${tab.icon} w-4 text-center`}></i>
|
||||
<span className="flex-1 text-left">{tab.label}</span>
|
||||
{tab.count !== null && (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
|
||||
activeTab === tab.id
|
||||
? "bg-white/20 text-white"
|
||||
: "bg-(--color-border-light) text-(--color-text-muted)"
|
||||
}`}
|
||||
>
|
||||
{tab.count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className="my-1.5 border-t border-(--color-border-light)"></div>
|
||||
|
||||
{/* Navigation links */}
|
||||
<Link
|
||||
href="/manager/analytics"
|
||||
onClick={() => setDropdownOpen(false)}
|
||||
className="flex items-center gap-2.5 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-primary) no-underline transition hover:bg-(--color-accent-light)"
|
||||
>
|
||||
<i className="fa-solid fa-chart-line w-4 text-center"></i>
|
||||
<span>Finance</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/staff/schedule"
|
||||
onClick={() => setDropdownOpen(false)}
|
||||
className="flex items-center gap-2.5 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition hover:bg-(--color-border-light)"
|
||||
>
|
||||
<i className="fa-solid fa-calendar-days w-4 text-center"></i>
|
||||
<span>Shifts</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => setDropdownOpen(false)}
|
||||
className="flex items-center gap-2.5 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition hover:bg-(--color-border-light)"
|
||||
>
|
||||
<i className="fa-solid fa-house w-4 text-center"></i>
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="my-1.5 border-t border-(--color-border-light)"></div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
logout();
|
||||
}}
|
||||
className="flex w-full cursor-pointer items-center gap-2.5 rounded-xl border-none bg-transparent px-3 py-2.5 text-sm font-medium text-red-500 transition hover:bg-red-50"
|
||||
>
|
||||
<i className="fa-solid fa-right-from-bracket w-4 text-center"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Desktop actions (lg+): sidebar handles nav, just show shortcut ── */}
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Link
|
||||
href="/manager/analytics"
|
||||
|
||||
@@ -4,6 +4,12 @@ import { ProductCard } from "@/components/molecules/cards";
|
||||
import { useCart } from "@/lib/cart-context";
|
||||
import { useManager } from "@/lib/manager-context";
|
||||
|
||||
function toDisplayUrl(filename: string) {
|
||||
if (!filename) return "/imgs/products/placeholder.jpg";
|
||||
if (filename.startsWith("/") || filename.startsWith("http")) return filename;
|
||||
return `/api/file/${filename}`;
|
||||
}
|
||||
|
||||
import type { ProductGridProps } from "./ProductGrid.types";
|
||||
|
||||
export default function ProductGrid({
|
||||
@@ -35,7 +41,7 @@ export default function ProductGrid({
|
||||
({ id, imageUrl, name, price, description }) => (
|
||||
<ProductCard
|
||||
key={id}
|
||||
image={imageUrl}
|
||||
image={toDisplayUrl(imageUrl)}
|
||||
imageAlt={name}
|
||||
productName={name}
|
||||
price={price}
|
||||
|
||||
@@ -50,7 +50,11 @@ export default function ShiftCreateModal({
|
||||
return;
|
||||
}
|
||||
|
||||
const deptName =
|
||||
DEPARTMENTS.find((d) => d.id === department)?.name ?? department;
|
||||
|
||||
createShift({
|
||||
name: `${deptName} - ${formatDateISO(date)}`,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
|
||||
+12
-13
@@ -107,14 +107,9 @@ const GET_ALL_SHIFTS = gql`
|
||||
query allShifts {
|
||||
allShifts {
|
||||
id
|
||||
date
|
||||
startTime
|
||||
endTime
|
||||
maxStaff
|
||||
wage
|
||||
registeredStaff {
|
||||
staffId
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -123,14 +118,9 @@ const CREATE_SHIFT = gql`
|
||||
mutation createShift($shiftInput: CreateShiftInput!) {
|
||||
createShift(shiftInput: $shiftInput) {
|
||||
id
|
||||
date
|
||||
startTime
|
||||
endTime
|
||||
maxStaff
|
||||
wage
|
||||
registeredStaff {
|
||||
staffId
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -323,12 +313,21 @@ export function ShiftProvider({ children }: { children: ReactNode }) {
|
||||
const createShift = useCallback(async (shift: Omit<ShiftEntity, "id">) => {
|
||||
const { data } = await mutateCreateShift({
|
||||
variables: {
|
||||
shiftInput: shift,
|
||||
shiftInput: {
|
||||
startTime: shift.startTime,
|
||||
endTime: shift.endTime,
|
||||
maxStaff: shift.maxStaff,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (data) setShifts((prev) => [...prev, data.createShift]);
|
||||
}, []);
|
||||
if (data) setShifts((prev) => [...prev, {
|
||||
...data.createShift,
|
||||
date: shift.date,
|
||||
wage: shift.wage,
|
||||
registeredStaff: [],
|
||||
}]);
|
||||
}, [mutateCreateShift]);
|
||||
|
||||
const updateShift = useCallback((shift: ShiftEntity) => {
|
||||
setShifts((prev) => prev.map((s) => (s.id === shift.id ? shift : s)));
|
||||
|
||||
+3
-2
@@ -96,10 +96,11 @@ export interface ShiftRegistrationEntity {
|
||||
|
||||
export interface ShiftEntity {
|
||||
id: string;
|
||||
date: Date;
|
||||
name?: string;
|
||||
date?: Date;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
wage: number;
|
||||
wage?: number;
|
||||
maxStaff: number;
|
||||
registeredStaff?: ShiftRegistrationEntity[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user