TorbatYar/frontend/app/dashboard/page.tsx
Mortezakoohjani 12c8615615 Ship enterprise Accounting FE/API with CRUD parity and production wiring.
Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 15:26:43 +03:30

210 lines
7.7 KiB
TypeScript
Raw Permalink 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.

"use client";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { AuthGuard } from "@/components/AuthGuard";
import { TenantSwitcher } from "@/components/TenantSwitcher";
import { useAuth } from "@/hooks/useAuth";
import { useMe } from "@/hooks/useMe";
import { api, ApiError, type TenantContext } from "@/lib/api";
import { Badge, Button, Container, SectionTitle } from "@/components/ui";
const STATUS_LABELS: Record<string, string> = {
draft: "پیش‌نویس",
pending_activation: "در انتظار فعال‌سازی",
active: "فعال",
suspended: "معلق",
archived: "بایگانی‌شده",
};
const ROLE_LABELS: Record<string, string> = {
platform_admin: "مدیر پلتفرم",
tenant_owner: "مالک workspace",
tenant_admin: "مدیر workspace",
tenant_editor: "ویرایشگر",
tenant_viewer: "بیننده",
};
const QUICK_LINKS = [
{ title: "کاربران و دسترسی‌ها", desc: "مدیریت اعضای workspace (به‌زودی)" },
{ title: "برندینگ و ظاهر", desc: "شخصی‌سازی رنگ و لوگو (به‌زودی)" },
{ title: "دامنه‌ها", desc: "مدیریت زیردامنه و دامنه اختصاصی (به‌زودی)" },
{ title: "اشتراک و پلن", desc: "مشاهده و ارتقاء پلن (به‌زودی)" },
];
function DashboardContent() {
const router = useRouter();
const { user, signOut } = useAuth();
const { me, loading: meLoading, error: meError, reload: reloadMe } = useMe();
const [ctx, setCtx] = useState<TenantContext | null>(null);
const [loadingCtx, setLoadingCtx] = useState(true);
const [ctxError, setCtxError] = useState("");
const loadContext = useCallback(async () => {
setLoadingCtx(true);
setCtxError("");
try {
const data = await api.tenantContext.current();
setCtx(data);
} catch (err) {
setCtxError(
err instanceof ApiError ? err.message : "دریافت اطلاعات workspace ناموفق بود"
);
setCtx(null);
} finally {
setLoadingCtx(false);
}
}, []);
useEffect(() => {
if (meLoading) return;
if (!me) {
setLoadingCtx(false);
return;
}
if (me.onboarding_required) {
router.replace("/onboarding");
return;
}
void loadContext();
}, [me, meLoading, router, loadContext]);
const handleSwitched = async () => {
await reloadMe();
await loadContext();
};
if (meLoading || (me && me.onboarding_required)) {
return (
<Container className="py-16 text-center">
<p className="text-sm text-gray-500">در حال بارگذاری...</p>
</Container>
);
}
return (
<Container className="py-10">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-secondary">داشبورد workspace</h1>
<p className="mt-1 text-sm text-gray-600">
خوش آمدید {user?.username || user?.email || "کاربر"}
</p>
</div>
<div className="flex flex-wrap items-end gap-4">
{me && (
<TenantSwitcher
memberships={me.memberships}
currentTenantId={me.current_tenant_id}
onSwitched={handleSwitched}
/>
)}
<Link href="/dashboard/settings">
<Button variant="outline">تنظیمات حساب</Button>
</Link>
<Button variant="outline" onClick={() => signOut()}>
خروج
</Button>
</div>
</div>
{(meError || ctxError) && (
<p className="mt-6 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-600">
{meError || ctxError}
</p>
)}
{loadingCtx ? (
<p className="mt-8 text-sm text-gray-500">در حال بارگذاری workspace...</p>
) : ctx ? (
<div className="mt-8 space-y-8">
<section className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-xl font-bold text-secondary">{ctx.tenant.name}</h2>
<p dir="ltr" className="mt-1 font-mono text-sm text-gray-500">
{ctx.primary_domain || ctx.tenant.slug}
</p>
</div>
<Badge variant={ctx.tenant.status === "active" ? "primary" : "default"}>
{STATUS_LABELS[ctx.tenant.status] || ctx.tenant.status}
</Badge>
</div>
<dl className="mt-6 grid grid-cols-1 gap-4 text-sm sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-xl bg-gray-50 p-4">
<dt className="text-xs text-gray-500">پلن فعلی</dt>
<dd className="mt-1 font-semibold text-secondary">
{ctx.plan_name || "—"}
</dd>
</div>
<div className="rounded-xl bg-gray-50 p-4">
<dt className="text-xs text-gray-500">وضعیت اشتراک</dt>
<dd className="mt-1 font-semibold text-secondary">
{ctx.subscription_status || "—"}
</dd>
</div>
<div className="rounded-xl bg-gray-50 p-4">
<dt className="text-xs text-gray-500">نقش شما</dt>
<dd className="mt-1 font-semibold text-secondary">
{(ctx.role && ROLE_LABELS[ctx.role]) || ctx.role || "—"}
</dd>
</div>
<div className="rounded-xl bg-gray-50 p-4">
<dt className="text-xs text-gray-500">وضعیت onboarding</dt>
<dd className="mt-1 font-semibold text-secondary">
{ctx.tenant.onboarding_completed ? "تکمیل‌شده" : "ناتمام"}
</dd>
</div>
</dl>
{ctx.domains.length > 0 && (
<div className="mt-6">
<p className="text-xs font-medium text-gray-500">دامنهها</p>
<div className="mt-2 flex flex-wrap gap-2">
{ctx.domains.map((d) => (
<Badge key={d.id} variant={d.is_verified ? "primary" : "default"}>
<span dir="ltr" className="font-mono">
{d.domain}
</span>
</Badge>
))}
</div>
</div>
)}
</section>
<section>
<SectionTitle title="ماژول‌ها" subtitle="دسترسی سریع به بخش‌های workspace" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{QUICK_LINKS.map((link) => (
<div
key={link.title}
className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm"
>
<p className="font-semibold text-secondary">{link.title}</p>
<p className="mt-1 text-xs text-gray-500">{link.desc}</p>
</div>
))}
</div>
</section>
</div>
) : (
!ctxError && (
<p className="mt-8 text-sm text-gray-500">اطلاعات workspace در دسترس نیست.</p>
)
)}
</Container>
);
}
export default function DashboardPage() {
return (
<AuthGuard>
<DashboardContent />
</AuthGuard>
);
}