Vitron-Front/app/routes/admin.tsx
Arda Samadi cedeef4746 feat(admin): ledger UI — overview/reconciliation, payouts, transactions, per-seller (slice 2a)
Ledger section with sub-tabs: overview (money held/owed, payouts by status, and
a reconciliation warning card), payouts + transactions lists, per-seller balances
list, and a per-seller detail showing recorded vs derived balance + discrepancy,
bank accounts and payout history. Enabled the Ledger nav item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:28:11 +03:30

145 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

import { Link, Outlet, useLocation } from "@remix-run/react";
import {
LayoutDashboard,
ClipboardList,
Store,
Users,
Package,
Landmark,
ArrowRight,
ShieldCheck,
Loader2,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { useRequireAdmin } from "~/hooks/useRequireAdmin";
interface NavItem {
label: string;
to: string;
icon: LucideIcon;
exact?: boolean;
soon?: boolean; // planned but not built yet
}
const NAV: NavItem[] = [
{ label: "نمای کلی", to: "/admin", icon: LayoutDashboard, exact: true },
{ label: "سفارش‌ها", to: "/admin/orders", icon: ClipboardList },
{ label: "فروشگاه‌ها", to: "/admin/sellers", icon: Store },
{ label: "کاربران", to: "/admin/users", icon: Users },
{ label: "محصولات", to: "/admin/products", icon: Package },
{ label: "دفتر مالی", to: "/admin/ledger", icon: Landmark },
];
export default function AdminLayout() {
const location = useLocation();
const { isLoading, isAdmin } = useRequireAdmin();
const isActive = (item: NavItem) =>
item.exact
? location.pathname === item.to
: location.pathname.startsWith(item.to);
if (isLoading || !isAdmin) {
return (
<div className="flex min-h-[60vh] items-center justify-center">
<Loader2 className="h-7 w-7 animate-spin text-GRAY" />
</div>
);
}
const NavLinks = ({ onNavigate }: { onNavigate?: () => void }) => (
<>
{NAV.map((n) => {
const active = isActive(n);
const cls = `flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold transition-colors ${
active ? "bg-BLACK text-white" : "text-BLACK2 hover:bg-WHITE2"
} ${n.soon ? "opacity-45 cursor-not-allowed" : ""}`;
const inner = (
<>
<n.icon size={20} />
{n.label}
{n.soon && (
<span className="ms-auto text-[10px] text-GRAY font-normal">
بهزودی
</span>
)}
</>
);
return n.soon ? (
<div key={n.to} className={cls} aria-disabled>
{inner}
</div>
) : (
<Link
key={n.to}
to={n.to}
prefetch="intent"
onClick={onNavigate}
className={cls}
>
{inner}
</Link>
);
})}
<Link
to="/"
className="flex items-center gap-3 px-3.5 py-3 rounded-xl text-[14.5px] font-semibold text-BLACK2 hover:bg-WHITE2 transition-colors"
>
<ArrowRight size={20} />
بازگشت به سایت
</Link>
</>
);
return (
<div className="lg:max-w-[1440px] lg:mx-auto lg:w-full lg:px-8 lg:py-8 lg:grid lg:grid-cols-[248px_1fr] lg:gap-7 lg:items-start">
{/* Desktop sidebar */}
<aside className="hidden lg:block lg:sticky lg:top-6 border border-WHITE3 rounded-2xl overflow-hidden">
<div className="p-4 border-b border-WHITE3 flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-BLACK text-white grid place-items-center shrink-0">
<ShieldCheck size={20} />
</div>
<div className="min-w-0">
<p className="font-bold text-[15px] truncate">پنل مدیریت</p>
<p className="text-GRAY text-[12px]">ویترون</p>
</div>
</div>
<nav className="p-2.5 flex flex-col gap-0.5">
<NavLinks />
</nav>
</aside>
{/* Mobile top bar + horizontal nav */}
<div className="lg:hidden sticky top-0 z-30 bg-WHITE pt-[env(safe-area-inset-top)] border-b border-WHITE3">
<div className="flex items-center gap-2 px-4 h-[56px]">
<div className="h-8 w-8 rounded-lg bg-BLACK text-white grid place-items-center">
<ShieldCheck size={16} />
</div>
<p className="font-bold text-[15px]">پنل مدیریت</p>
</div>
<div className="flex gap-2 overflow-x-auto px-4 pb-2 hide-scrollbar">
{NAV.map((n) => {
const active = isActive(n);
const cls = `whitespace-nowrap px-3 py-1.5 rounded-full text-[13px] font-semibold ${
active ? "bg-BLACK text-white" : "bg-WHITE2 text-BLACK2"
} ${n.soon ? "opacity-45" : ""}`;
return n.soon ? (
<span key={n.to} className={cls}>
{n.label}
</span>
) : (
<Link key={n.to} to={n.to} prefetch="intent" className={cls}>
{n.label}
</Link>
);
})}
</div>
</div>
<div className="min-w-0 px-4 py-4 lg:p-0">
<Outlet />
</div>
</div>
);
}