Connect all CRM pages to the BFF and CRM service, add module docs, and mark CRM available in the SuperApp catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
7.0 KiB
TypeScript
201 lines
7.0 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { PageHeader } from "@/components/ds";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import {
|
||
CrmCountsGrid,
|
||
CrmPageLoader,
|
||
CrmPageError,
|
||
CrmServiceBadge,
|
||
useCrmCounts,
|
||
} from "@/modules/crm/pages/shared";
|
||
import { CrmBreadcrumbs } from "@/modules/crm/components/CrmBreadcrumbs";
|
||
import { CrmStatGrid } from "@/modules/crm/design-system";
|
||
import { crmApi } from "@/modules/crm/services/crm-api";
|
||
|
||
function DashboardShell({
|
||
title,
|
||
description,
|
||
children,
|
||
}: {
|
||
title: string;
|
||
description: string;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "داشبورد", href: "/crm/sales/dashboard/overview" }, { label: title }]} />
|
||
<PageHeader title={title} description={description} actions={<CrmServiceBadge />} />
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function OverviewDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
if (!tenantId) return <CrmPageLoader />;
|
||
return (
|
||
<DashboardShell title="نمای کلی" description="خلاصه وضعیت CRM tenant — داده واقعی از API.">
|
||
<CrmCountsGrid tenantId={tenantId} />
|
||
</DashboardShell>
|
||
);
|
||
}
|
||
|
||
export function SalesDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useCrmCounts(tenantId);
|
||
const goalsQ = useQuery({
|
||
queryKey: ["crm", tenantId, "goals"],
|
||
queryFn: () => crmApi.goals.list(tenantId!, { page: 1, page_size: 50 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
const targetsQ = useQuery({
|
||
queryKey: ["crm", tenantId, "targets"],
|
||
queryFn: () => crmApi.targets.list(tenantId!, { page: 1, page_size: 50 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <CrmPageLoader />;
|
||
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
return (
|
||
<DashboardShell title="داشبورد فروش" description="معاملات، اهداف و اهداف کمی فروش.">
|
||
<CrmStatGrid
|
||
stats={[
|
||
{ label: "معاملات باز", value: q.data!.openDeals },
|
||
{ label: "معاملات برنده", value: q.data!.wonDeals },
|
||
{ label: "اهداف فروش", value: goalsQ.data?.length ?? 0 },
|
||
{ label: "تارگتها", value: targetsQ.data?.length ?? 0 },
|
||
]}
|
||
/>
|
||
</DashboardShell>
|
||
);
|
||
}
|
||
|
||
export function PipelineDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const pipelinesQ = useQuery({
|
||
queryKey: ["crm", tenantId, "pipelines"],
|
||
queryFn: () => crmApi.pipelines.list(tenantId!, { page: 1, page_size: 100 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
const oppsQ = useQuery({
|
||
queryKey: ["crm", tenantId, "opportunities"],
|
||
queryFn: () => crmApi.opportunities.list(tenantId!, { page: 1, page_size: 500 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || pipelinesQ.isLoading || oppsQ.isLoading) return <CrmPageLoader />;
|
||
if (pipelinesQ.error) return <CrmPageError error={pipelinesQ.error} onRetry={() => pipelinesQ.refetch()} />;
|
||
|
||
const opps = oppsQ.data ?? [];
|
||
const byPipeline = (pipelinesQ.data ?? []).map((p) => ({
|
||
label: p.name,
|
||
value: opps.filter((o) => o.pipeline_id === p.id).length,
|
||
}));
|
||
|
||
return (
|
||
<DashboardShell title="داشبورد قیف" description="توزیع معاملات در قیفهای فروش.">
|
||
<CrmStatGrid stats={byPipeline.length ? byPipeline : [{ label: "قیف", value: 0 }]} />
|
||
</DashboardShell>
|
||
);
|
||
}
|
||
|
||
export function LeadDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["crm", tenantId, "leads"],
|
||
queryFn: () => crmApi.leads.list(tenantId!, { page: 1, page_size: 500 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <CrmPageLoader />;
|
||
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
const leads = q.data ?? [];
|
||
const byStatus = ["new", "contacted", "qualified", "converted", "lost"].map((s) => ({
|
||
label: s,
|
||
value: leads.filter((l) => l.status === s && !l.is_deleted).length,
|
||
}));
|
||
|
||
return (
|
||
<DashboardShell title="داشبورد سرنخ" description="وضعیت سرنخها بر اساس lifecycle.">
|
||
<CrmStatGrid stats={[{ label: "کل سرنخ", value: leads.length }, ...byStatus]} />
|
||
</DashboardShell>
|
||
);
|
||
}
|
||
|
||
export function ActivityDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["crm", tenantId, "activity-dash"],
|
||
queryFn: async () => {
|
||
const [activities, tasks, meetings] = await Promise.all([
|
||
crmApi.activities.list(tenantId!, { page: 1, page_size: 500 }),
|
||
crmApi.tasks.list(tenantId!, { page: 1, page_size: 500 }),
|
||
crmApi.meetings.list(tenantId!, { page: 1, page_size: 500 }),
|
||
]);
|
||
return { activities, tasks, meetings };
|
||
},
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <CrmPageLoader />;
|
||
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
return (
|
||
<DashboardShell title="داشبورد فعالیت" description="فعالیتها، وظایف و جلسات.">
|
||
<CrmStatGrid
|
||
stats={[
|
||
{ label: "فعالیتها", value: q.data!.activities.length },
|
||
{ label: "وظایف", value: q.data!.tasks.length },
|
||
{ label: "جلسات", value: q.data!.meetings.length },
|
||
{
|
||
label: "فعالیت تکمیلشده",
|
||
value: q.data!.activities.filter((a) => a.status === "completed").length,
|
||
},
|
||
]}
|
||
/>
|
||
</DashboardShell>
|
||
);
|
||
}
|
||
|
||
export function CalendarDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["crm", tenantId, "meetings"],
|
||
queryFn: () => crmApi.meetings.list(tenantId!, { page: 1, page_size: 500 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <CrmPageLoader />;
|
||
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
const upcoming = (q.data ?? [])
|
||
.filter((m) => new Date(m.starts_at) >= new Date())
|
||
.slice(0, 10);
|
||
|
||
return (
|
||
<DashboardShell title="تقویم" description="جلسات آینده — بر اساس API meetings.">
|
||
<div className="space-y-2">
|
||
{upcoming.map((m) => (
|
||
<div
|
||
key={m.id}
|
||
className="rounded-xl border border-[var(--border)] bg-[var(--surface)] px-4 py-3 shadow-[var(--shadow-sm)]"
|
||
>
|
||
<p className="font-medium text-secondary">{m.subject}</p>
|
||
<p className="text-xs text-[var(--muted)]">
|
||
{new Date(m.starts_at).toLocaleString("fa-IR")}
|
||
{m.location ? ` — ${m.location}` : ""}
|
||
</p>
|
||
</div>
|
||
))}
|
||
{upcoming.length === 0 ? (
|
||
<p className="text-sm text-[var(--muted)]">جلسه آیندهای ثبت نشده.</p>
|
||
) : null}
|
||
</div>
|
||
</DashboardShell>
|
||
);
|
||
}
|