TorbatYar/frontend/components/accounting/AccountingShell.tsx
Mortezakoohjani 7d66932da8 Complete Accounting sidebar modules with ops CRUD, BFF proxy, and treasury APIs.
Wire nested nav pages for phases 5.1-5.11 to real list/create endpoints, fix COA import connectivity via same-origin proxy, and add operational migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 16:10:06 +03:30

187 lines
7.8 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, Lock } from "lucide-react";
import { cn } from "@/lib/utils";
import { useColorMode } from "@/components/providers/ColorModeProvider";
import { Button } from "@/components/ds";
import { ACCOUNTING_NAV, ACCOUNTING_QUICK_BAR } from "@/lib/accounting-nav";
function isActive(pathname: string, href: string) {
const base = href.split("?")[0];
if (base === "/accounting") return pathname === "/accounting";
return pathname === base || pathname.startsWith(`${base}/`);
}
export function AccountingShell({ 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 ACCOUNTING_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">
{ACCOUNTING_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-primary text-white 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",
group.blocked && "opacity-70"
)}
>
<Icon className="h-4 w-4 shrink-0" />
<span className="flex-1 text-right">{group.label}</span>
{group.blocked ? <Lock className="h-3.5 w-3.5" /> : null}
<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);
const blocked = item.blocked || group.blocked;
return (
<Link
key={item.href + item.label}
href={blocked ? "/accounting/ai" : item.href}
className={cn(
"flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium",
active
? "bg-primary text-white"
: "text-[var(--muted)] hover:bg-[var(--surface-muted)] hover:text-secondary",
blocked && "opacity-60"
)}
>
{blocked ? <Lock className="h-3 w-3" /> : null}
{item.label}
</Link>
);
})}
</div>
) : null}
</div>
);
})}
</nav>
<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">
{ACCOUNTING_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-primary 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>
{/* Quick access bar */}
<div className="fixed inset-x-0 bottom-0 z-40 border-t border-[var(--border)] bg-[var(--surface)]/95 backdrop-blur">
<div className="mx-auto flex max-w-5xl items-stretch justify-around gap-1 px-2 py-2 pe-16">
{ACCOUNTING_QUICK_BAR.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-primary" : "text-[var(--muted)]"
)}
>
<span className="truncate">{item.label}</span>
</Link>
))}
</div>
<Link
href="/accounting/vouchers/new"
className="absolute bottom-3 left-3 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-white shadow-lg"
aria-label="سند جدید"
>
<Plus className="h-6 w-6" />
</Link>
</div>
</div>
</div>
);
}