TorbatYar/frontend/modules/beauty/components/BeautyBusinessShell.tsx
Mortezakoohjani 6f4a484051 Migrate Beauty, Healthcare, and Accounting frontend to modular src/modules architecture.
Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:28:27 +03:30

195 lines
8.2 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 { useMemo, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronDown, Moon, Sun, ChevronRight, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { useColorMode } from "@/components/providers/ColorModeProvider";
import { Button } from "@/components/ds";
import { OWNER_NAV } from "@/modules/beauty/constants/portals";
function isActive(pathname: string, href: string) {
const base = href.split("?")[0];
if (base === "/beauty") return pathname === "/beauty";
return pathname === base || pathname.startsWith(`${base}/`);
}
/** @deprecated Use BeautyPortalShell with portalId="owner" */
export function BeautyBusinessShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { mode, toggle } = useColorMode();
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
const autoOpen = useMemo(() => {
const map: Record<string, boolean> = {};
for (const g of OWNER_NAV) {
if (g.href && isActive(pathname, g.href)) map[g.id] = true;
if (g.items?.some((i) => isActive(pathname, i.href))) map[g.id] = true;
}
return map;
}, [pathname]);
const expanded = { ...autoOpen, ...openGroups };
const toggleGroup = (id: string) =>
setOpenGroups((prev) => ({ ...prev, [id]: !(expanded[id] ?? false) }));
return (
<div className="flex min-h-[calc(100vh-4rem)] bg-[var(--canvas)]">
<aside className="sticky top-16 hidden h-[calc(100vh-4rem)] w-72 shrink-0 overflow-y-auto border-l border-[var(--border)] bg-[var(--surface)] p-3 lg:block">
<div className="mb-3 flex items-center justify-between px-2">
<div>
<p className="text-xs text-[var(--muted)]">ماژول</p>
<p className="font-semibold text-secondary">زیبایی تربت بیوتی</p>
</div>
<Button type="button" variant="ghost" size="icon" onClick={toggle} aria-label="تغییر تم">
{mode === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
<nav className="space-y-0.5 pb-24">
{OWNER_NAV.map((group) => {
const Icon = group.icon;
const hasChildren = !!group.items?.length;
const groupActive = group.href
? isActive(pathname, group.href)
: group.items?.some((i) => isActive(pathname, i.href));
const isOpen = expanded[group.id] ?? false;
if (!hasChildren && group.href) {
return (
<Link
key={group.id}
href={group.href}
className={cn(
"flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors",
groupActive
? "bg-[var(--beauty-accent)] text-white shadow-[var(--shadow-sm)]"
: "text-[var(--muted)] hover:bg-[var(--surface-muted)] hover:text-secondary"
)}
>
<Icon className="h-4 w-4 shrink-0" />
{group.label}
</Link>
);
}
return (
<div key={group.id} className="rounded-xl">
<button
type="button"
onClick={() => toggleGroup(group.id)}
className={cn(
"flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors",
groupActive
? "bg-[var(--surface-muted)] text-secondary"
: "text-[var(--muted)] hover:bg-[var(--surface-muted)] hover:text-secondary"
)}
>
<Icon className="h-4 w-4 shrink-0" />
<span className="flex-1 text-right">{group.label}</span>
<ChevronDown
className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")}
/>
</button>
{isOpen ? (
<div className="mb-1 mr-3 space-y-0.5 border-r border-[var(--border)] pr-2">
{group.items?.map((item) => {
const active = isActive(pathname, item.href);
return (
<Link
key={item.href + item.label}
href={item.href}
className={cn(
"flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium",
active
? "bg-[var(--beauty-accent)] text-white"
: "text-[var(--muted)] hover:bg-[var(--surface-muted)] hover:text-secondary"
)}
>
{item.label}
</Link>
);
})}
</div>
) : null}
</div>
);
})}
</nav>
<Link
href="/beauty/hub"
className="mt-2 flex items-center gap-2 px-3 text-xs text-[var(--muted)] hover:text-secondary"
>
<ChevronRight className="h-3.5 w-3.5" />
تغییر پورتال
</Link>
<Link
href="/dashboard"
className="mt-2 flex items-center gap-2 px-3 text-xs text-[var(--muted)] hover:text-secondary"
>
<ChevronRight className="h-3.5 w-3.5" />
بازگشت به workspace
</Link>
</aside>
<div className="relative min-w-0 flex-1 pb-28 lg:pb-20">
<div className="flex gap-2 overflow-x-auto border-b border-[var(--border)] bg-[var(--surface)] px-3 py-2 lg:hidden">
{OWNER_NAV.flatMap((g) =>
g.href
? [{ href: g.href, label: g.label }]
: (g.items ?? []).slice(0, 1).map((i) => ({ href: i.href, label: g.label }))
).map((item) => (
<Link
key={item.href + item.label}
href={item.href}
className={cn(
"whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium",
isActive(pathname, item.href)
? "bg-[var(--beauty-accent)] text-white"
: "text-[var(--muted)] hover:bg-[var(--surface-muted)]"
)}
>
{item.label}
</Link>
))}
</div>
<div className="mx-auto max-w-7xl px-4 py-6 sm:px-6">{children}</div>
<div className="fixed inset-x-0 bottom-0 z-40 border-t border-[var(--border)] bg-[var(--surface)]/95 backdrop-blur lg:hidden">
<div className="mx-auto flex max-w-5xl items-stretch justify-around gap-1 px-2 py-2 pe-16">
{[
{ href: "/beauty/organizations", label: "سازمان" },
{ href: "/beauty/appointments", label: "نوبت" },
{ href: "/beauty/catalog/services", label: "خدمت" },
{ href: "/beauty/customers", label: "مشتری" },
{ href: "/beauty", label: "داشبورد" },
].map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"flex min-w-0 flex-1 flex-col items-center gap-0.5 rounded-lg px-1 py-1 text-[10px] font-medium sm:text-xs",
isActive(pathname, item.href) ? "text-[var(--beauty-accent)]" : "text-[var(--muted)]"
)}
>
<span className="truncate">{item.label}</span>
</Link>
))}
</div>
<Link
href="/beauty/appointments"
className="absolute bottom-3 left-3 flex h-12 w-12 items-center justify-center rounded-full bg-[var(--beauty-accent)] text-white shadow-[var(--shadow-lg)]"
aria-label="نوبت جدید"
>
<Plus className="h-6 w-6" />
</Link>
</div>
</div>
</div>
);
}