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>
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
PageHeader,
|
|
LoadingState,
|
|
ErrorState,
|
|
Badge,
|
|
Card,
|
|
CardContent,
|
|
EmptyState,
|
|
} from "@/components/ds";
|
|
import { crmApi, CrmApiError } from "@/modules/crm/services/crm-api";
|
|
import { useTenantId } from "@/hooks/useTenantId";
|
|
import { CrmStatGrid } from "@/modules/crm/design-system";
|
|
|
|
export function CrmPageLoader({ label = "در حال بارگذاری…" }: { label?: string }) {
|
|
return <LoadingState label={label} />;
|
|
}
|
|
|
|
export function CrmPageError({
|
|
error,
|
|
onRetry,
|
|
}: {
|
|
error: unknown;
|
|
onRetry?: () => void;
|
|
}) {
|
|
if (error instanceof CrmApiError && error.status === 403) {
|
|
return (
|
|
<EmptyState
|
|
title="دسترسی محدود"
|
|
description={error.message || "مجوز لازم برای این عملیات وجود ندارد."}
|
|
/>
|
|
);
|
|
}
|
|
const message = error instanceof Error ? error.message : "خطا در دریافت دادهها";
|
|
return <ErrorState message={message} onRetry={onRetry} />;
|
|
}
|
|
|
|
export function useCrmHealth() {
|
|
return useQuery({
|
|
queryKey: ["crm", "health"],
|
|
queryFn: () => crmApi.health(),
|
|
});
|
|
}
|
|
|
|
export function CrmServiceBadge() {
|
|
const healthQ = useCrmHealth();
|
|
if (!healthQ.data) return null;
|
|
return (
|
|
<Badge tone={healthQ.data.status === "ok" ? "success" : "danger"}>
|
|
CRM v{healthQ.data.version}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
export function useCrmCounts(tenantId: string | null) {
|
|
return useQuery({
|
|
queryKey: ["crm", tenantId, "counts"],
|
|
queryFn: async () => {
|
|
const [leads, contacts, orgs, opps, activities, tasks, meetings, calls] =
|
|
await Promise.all([
|
|
crmApi.leads.list(tenantId!, { page: 1, page_size: 500 }),
|
|
crmApi.contacts.list(tenantId!, { page: 1, page_size: 500 }),
|
|
crmApi.organizations.list(tenantId!, { page: 1, page_size: 500 }),
|
|
crmApi.opportunities.list(tenantId!, { page: 1, page_size: 500 }),
|
|
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 }),
|
|
crmApi.calls.list(tenantId!, { page: 1, page_size: 500 }),
|
|
]);
|
|
return {
|
|
leads: leads.length,
|
|
contacts: contacts.length,
|
|
organizations: orgs.length,
|
|
opportunities: opps.length,
|
|
activities: activities.length,
|
|
tasks: tasks.length,
|
|
meetings: meetings.length,
|
|
calls: calls.length,
|
|
openDeals: opps.filter((o) => o.status === "open").length,
|
|
wonDeals: opps.filter((o) => o.status === "won").length,
|
|
};
|
|
},
|
|
enabled: !!tenantId,
|
|
});
|
|
}
|
|
|
|
export function CrmCountsGrid({ tenantId }: { tenantId: string }) {
|
|
const q = useCrmCounts(tenantId);
|
|
if (q.isLoading) return <CrmPageLoader />;
|
|
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
|
const c = q.data!;
|
|
return (
|
|
<CrmStatGrid
|
|
stats={[
|
|
{ label: "سرنخها", value: c.leads },
|
|
{ label: "مخاطبین", value: c.contacts },
|
|
{ label: "شرکتها", value: c.organizations },
|
|
{ label: "معاملات باز", value: c.openDeals },
|
|
{ label: "معاملات برنده", value: c.wonDeals },
|
|
{ label: "فعالیتها", value: c.activities },
|
|
{ label: "وظایف", value: c.tasks },
|
|
{ label: "جلسات", value: c.meetings },
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function exportToCsv(filename: string, rows: Record<string, unknown>[], columns: string[]) {
|
|
const header = columns.join(",");
|
|
const lines = rows.map((r) =>
|
|
columns.map((c) => JSON.stringify(r[c] ?? "")).join(",")
|
|
);
|
|
const blob = new Blob(["\uFEFF" + [header, ...lines].join("\n")], {
|
|
type: "text/csv;charset=utf-8;",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|