90 lines
3.8 KiB
TypeScript
90 lines
3.8 KiB
TypeScript
import type { ReactNode } from "react";
|
||
import { useLocation, useNavigate } from "react-router-dom";
|
||
import { ChevronRight, ClipboardList } from "lucide-react";
|
||
import { cn } from "../../lib/utils";
|
||
import { useAuthStore, type AuthUser } from "../../store/auth";
|
||
|
||
const ROLE_LABELS: Record<string, string> = {
|
||
admin: "ادمین",
|
||
procurement_manager: "مدیر تدارکات",
|
||
pm: "مدیر پروژه",
|
||
supervisor: "سرپرست پروژه",
|
||
};
|
||
|
||
export function userSubtitle(user: AuthUser | null | undefined): string {
|
||
if (!user) return "";
|
||
const fullName = `${user.first_name || ""} ${user.last_name || ""}`.trim() || user.display_name || "کاربر";
|
||
const username = user.username || user.casdoor_id || "-";
|
||
const role = user.role_label || (user.role ? ROLE_LABELS[user.role] : "بدون نقش");
|
||
const proj = user.role === "supervisor" && user.project_name ? ` – پروژه: ${user.project_name}` : "";
|
||
return `${fullName} | نام کاربری: ${username} | نقش: ${role}${proj}`;
|
||
}
|
||
|
||
interface HeaderProps {
|
||
title: string;
|
||
sub?: string;
|
||
backTo?: string; // مسیر بازگشتِ جایگزین (وقتی سابقهای در برنامه نیست)
|
||
backLabel?: string;
|
||
actions?: ReactNode; // دکمههای سمت چپ
|
||
brand?: boolean; // نمایش آیکون دفتر کنار عنوان
|
||
icon?: ReactNode; // آیکون سفارشی جعبهی کنار منو (پیشفرض: ClipboardList)
|
||
menu?: ReactNode; // منوی همبرگری بالای راست (ناوبری بین پنلها)
|
||
}
|
||
|
||
// نوار بالای هر صفحه: منو (همبرگری) + جعبهی آیکون + عنوان + اکشنها
|
||
export function Header({ title, sub, backTo, backLabel = "بازگشت", actions, brand, icon, menu }: HeaderProps) {
|
||
const navigate = useNavigate();
|
||
const location = useLocation();
|
||
const user = useAuthStore((s) => s.user);
|
||
|
||
const userSub = userSubtitle(user);
|
||
const displaySub = sub !== undefined ? sub : userSub;
|
||
|
||
// اگر کاربر از داخل برنامه به این صفحه آمده، یک قدم برمیگردیم؛
|
||
// اگر مستقیم باز شده (بدون سابقهی داخلی)، به backTo میرویم.
|
||
const goBack = () => {
|
||
if (location.key !== "default") navigate(-1);
|
||
else if (backTo) navigate(backTo);
|
||
};
|
||
|
||
return (
|
||
<header className="flex flex-wrap items-center gap-3 border-b border-line bg-panel px-5 py-2.5 max-md:sticky max-md:top-0 max-md:z-40">
|
||
{menu && <div className="flex shrink-0 items-center">{menu}</div>}
|
||
{(icon || brand) && (
|
||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-accent-bg text-accent">
|
||
{icon || <ClipboardList className="h-4.5 w-4.5" strokeWidth={2} />}
|
||
</span>
|
||
)}
|
||
{backTo && (
|
||
<button
|
||
type="button"
|
||
onClick={goBack}
|
||
className="hidden md:inline-flex cursor-pointer items-center gap-0.5 rounded-md px-2 py-1 text-[13px] text-muted transition-colors hover:bg-slate-bg hover:text-ink"
|
||
>
|
||
<ChevronRight className="h-4 w-4" />
|
||
{backLabel}
|
||
</button>
|
||
)}
|
||
<div className="flex min-w-0 items-center gap-2.5">
|
||
<div className="min-w-0">
|
||
<h1 className="text-[17px] font-extrabold leading-6">{title}</h1>
|
||
{displaySub && <div className="text-[11.5px] text-muted font-normal mt-0.5">{displaySub}</div>}
|
||
</div>
|
||
</div>
|
||
<div className="ms-auto flex flex-wrap items-center gap-2">{actions}</div>
|
||
</header>
|
||
);
|
||
}
|
||
|
||
// بدنهی صفحه با عرض محدود و چینش مرکزی
|
||
export function PageLayout({
|
||
children,
|
||
className,
|
||
}: {
|
||
children: ReactNode;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<div className={cn("mx-auto w-full max-w-[1200px] px-4 py-4 max-md:pb-24", className)}>{children}</div>
|
||
);
|
||
} |