Multi-phase visual facelift (design tokens, dashboards, sidebar/navbar shell, per-page help guides, and a layout/content pass across every remaining page) plus backend fixes to the dashboard KPI stats: - Admin/Supervisor dashboard KPIs (revenue, donations, registrations, tickets sold) now use a rolling trailing-month window (today back one calendar month, e.g. 9 May - 8 June if today is 8 June) instead of calendar month-to-date, which under-counted for most of the month. The comparison window shifts the same way, so like is still compared with like. - Reports deep-links from those stat tiles now match the same window (range=trailing_month, replacing range=this_month). - Design tokens (brand-* Tailwind scale + shadcn CSS variables), a site-wide contextual help button, fixed dashboard sidebar/navbar, Admin/Supervisor/Staff/User dashboard rebuilds backed by a new GET /api/stats/overview endpoint, a dedicated Contact page, Site Settings restyle with WhatsApp config folded in, and an Account activity feed backed by a new SecurityEvent model. - Every remaining page (home, events, registration flow, auth, legal, payment results, and every Admin/Supervisor/Staff/User tool page) restyled onto the same design tokens, several with real layout upgrades (home hero, events list/detail, donate page, auth pages). - 20+ new dedicated help guides so the whole site has page-specific help content instead of falling back to a generic guide. - Assorted fixes surfaced along the way: donation-leg double-counting in payment stats, donations not counting toward revenue, refund netting in per-method report breakdowns, and donation over-allocation after a refund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import Link from "next/link";
|
|
import { Calendar, User, LayoutDashboard, ShieldCheck, Globe, Menu, type LucideIcon } from "lucide-react";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { RoleBadge, type Role } from "@/components/shared/RoleBadge";
|
|
import { UserAvatar } from "@/components/shared/UserAvatar";
|
|
import { usePathname } from "next/navigation";
|
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
|
|
|
const navLinks: { href: string; label: string; icon: LucideIcon; roles?: string[] }[] = [
|
|
{ href: "/dashboard/user", label: "My Events", icon: Calendar, roles: ["user", "staff", "supervisor", "admin"] },
|
|
{ href: "/dashboard/user/profile", label: "Profile & Security", icon: User, roles: ["user", "staff", "supervisor", "admin"] },
|
|
{ href: "/dashboard/staff", label: "Staff", icon: LayoutDashboard, roles: ["staff"] },
|
|
{ href: "/dashboard/supervisor", label: "Supervisor", icon: LayoutDashboard, roles: ["supervisor"] },
|
|
{ href: "/dashboard/admin", label: "Admin", icon: ShieldCheck, roles: ["admin"] },
|
|
{ href: "/dashboard/admin/settings", label: "Site Settings", icon: Globe, roles: ["admin"] },
|
|
];
|
|
|
|
function SidebarNavContent({ onNavigate }: { onNavigate?: () => void }) {
|
|
const { user } = useAuth();
|
|
const role = (user?.role || "user") as Role;
|
|
const pathname = usePathname();
|
|
const links = navLinks.filter(l => !l.roles || l.roles.includes(role));
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{user && (
|
|
<div className="mb-6 flex items-center gap-3">
|
|
<UserAvatar name={user.name} />
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold text-gray-900 truncate">{user.name}</div>
|
|
<RoleBadge role={role} className="mt-0.5" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
<nav className="space-y-1">
|
|
{links.map(l => {
|
|
const Icon = l.icon;
|
|
const active = pathname === l.href;
|
|
return (
|
|
<Link
|
|
key={l.href}
|
|
href={l.href}
|
|
onClick={onNavigate}
|
|
className={
|
|
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors " +
|
|
(active ? "bg-brand-50 text-brand-700 font-medium" : "text-gray-600 hover:bg-gray-50")
|
|
}
|
|
>
|
|
<Icon className="w-4 h-4 shrink-0" />
|
|
{l.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Desktop sidebar — fixed/pinned, does not scroll with page content. */
|
|
export function Sidebar() {
|
|
return (
|
|
<aside className="hidden md:flex md:flex-col w-64 shrink-0 h-full overflow-y-auto border-r bg-white p-4">
|
|
<SidebarNavContent />
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
/** Mobile equivalent — a slim top strip with a hamburger that opens the same nav in a drawer. */
|
|
export function MobileSidebar() {
|
|
const { user } = useAuth();
|
|
const role = (user?.role || "user") as Role;
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="md:hidden border-b bg-white px-4 py-3 shadow-sm flex items-center justify-between shrink-0">
|
|
{user && (
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<UserAvatar name={user.name} className="w-8 h-8 text-xs" />
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold text-gray-900 truncate">{user.name}</div>
|
|
<RoleBadge role={role} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
<SheetTrigger asChild>
|
|
<button type="button" aria-label="Open navigation" className="p-2 rounded-md hover:bg-gray-100 shrink-0">
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="w-72 p-4">
|
|
<SidebarNavContent onNavigate={() => setOpen(false)} />
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
);
|
|
}
|