TorbatYar/frontend/modules/crm/features/sales/dashboards.tsx
Mortezakoohjani e140908034 Ship enterprise CRM frontend with real API wiring and thin app routes.
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>
2026-07-26 20:58:17 +03:30

201 lines
7.0 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 { 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>
);
}