Commit completed Torbat Pages admin frontend module, BFF proxy, routes, and validation artifacts for official platform deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
6.1 KiB
TypeScript
142 lines
6.1 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import Link from "next/link";
|
||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts";
|
||
import { experienceApi } from "@/modules/experience/services/experience-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { useExperienceCapabilities, useExperienceMetrics } from "@/modules/experience/hooks/useExperienceCapabilities";
|
||
import { ExperiencePageLoader, ExperiencePageError } from "@/modules/experience/pages/shared";
|
||
import { PageHeader, Card, CardContent, Button, Badge } from "@/components/ds";
|
||
import { ExperienceBreadcrumbs } from "@/modules/experience/components/ExperienceBreadcrumbs";
|
||
import { ExperienceStatGrid } from "@/modules/experience/design-system";
|
||
|
||
export const ExecutiveDashboard = function ExperienceExecutiveDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const caps = useExperienceCapabilities();
|
||
const metrics = useExperienceMetrics();
|
||
|
||
const countsQ = useQuery({
|
||
queryKey: ["experience", tenantId, "dashboard-counts"],
|
||
queryFn: async () => {
|
||
const [workspaces, sites, pages, forms, releases, audit] = await Promise.all([
|
||
experienceApi.workspaces.list(tenantId!),
|
||
experienceApi.sites.list(tenantId!),
|
||
experienceApi.pages.list(tenantId!),
|
||
experienceApi.forms.list(tenantId!),
|
||
experienceApi.publishReleases.list(tenantId!),
|
||
experienceApi.audit.list(tenantId!, { limit: 10 }),
|
||
]);
|
||
return {
|
||
workspaces: workspaces.length,
|
||
sites: sites.length,
|
||
pages: pages.length,
|
||
forms: forms.length,
|
||
releases: releases.length,
|
||
recentPages: pages.slice(0, 5),
|
||
recentSites: sites.slice(0, 5),
|
||
audit: audit.slice(0, 10),
|
||
};
|
||
},
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || countsQ.isLoading || caps.isLoading) return <ExperiencePageLoader />;
|
||
if (countsQ.error) return <ExperiencePageError error={countsQ.error} onRetry={() => countsQ.refetch()} />;
|
||
|
||
const c = countsQ.data!;
|
||
const chartData = [
|
||
{ name: "فضاها", value: c.workspaces },
|
||
{ name: "سایت", value: c.sites },
|
||
{ name: "صفحات", value: c.pages },
|
||
{ name: "فرم", value: c.forms },
|
||
{ name: "انتشار", value: c.releases },
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<ExperienceBreadcrumbs current="داشبورد اجرایی" />
|
||
<PageHeader
|
||
title="داشبورد اجرایی"
|
||
description="نمای کلی تربت پیجز — سایت، صفحه، فرم و انتشار."
|
||
actions={<Badge tone="success">Torbat Pages v{caps.data?.version}</Badge>}
|
||
/>
|
||
<ExperienceStatGrid
|
||
stats={[
|
||
{ label: "فضاهای کاری", value: String(c.workspaces) },
|
||
{ label: "سایتها", value: String(c.sites) },
|
||
{ label: "صفحات", value: String(c.pages) },
|
||
{ label: "فرمها", value: String(c.forms) },
|
||
]}
|
||
/>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-4 text-sm font-semibold text-secondary">توزیع منابع</h3>
|
||
<div className="h-48">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={chartData}>
|
||
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
|
||
<YAxis allowDecimals={false} tick={{ fontSize: 11 }} />
|
||
<Tooltip />
|
||
<Bar dataKey="value" fill="var(--experience-accent)" radius={[4, 4, 0, 0]} />
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold text-secondary">اقدامات سریع</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/experience/pages"><Button variant="outline" size="sm">صفحات</Button></Link>
|
||
<Link href="/experience/sites"><Button variant="outline" size="sm">سایتها</Button></Link>
|
||
<Link href="/experience/forms"><Button variant="outline" size="sm">فرمها</Button></Link>
|
||
<Link href="/experience/publish-releases"><Button variant="outline" size="sm">انتشار</Button></Link>
|
||
<Link href="/experience/analytics"><Button variant="outline" size="sm">آنالیتیکس</Button></Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold">آخرین صفحات</h3>
|
||
<ul className="space-y-1 text-sm">
|
||
{c.recentPages.map((p) => (
|
||
<li key={String(p.id)}>
|
||
<Link href={`/experience/pages/${p.id}/builder`} className="text-[var(--experience-accent)] hover:underline">
|
||
{String(p.title ?? p.slug)}
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold">فعالیت اخیر (ممیزی)</h3>
|
||
<ul className="space-y-1 text-xs text-[var(--muted)]">
|
||
{c.audit.map((a) => (
|
||
<li key={String(a.id)}>
|
||
{String(a.action)} — {String(a.entity_type)} — {new Date(String(a.created_at)).toLocaleString("fa-IR")}
|
||
</li>
|
||
))}
|
||
{c.audit.length === 0 ? <li>فعالیتی ثبت نشده</li> : null}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
{metrics.data ? (
|
||
<Card>
|
||
<CardContent className="p-4 text-xs text-[var(--muted)]">
|
||
فاز {metrics.data.phase} — {metrics.data.note}
|
||
</CardContent>
|
||
</Card>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ExecutiveDashboard;
|