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>
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import Link from "next/link";
|
||
import { PageHeader, Card, CardContent, Button, EmptyState } from "@/components/ds";
|
||
import { crmApi } from "@/modules/crm/services/crm-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { CrmPageLoader, CrmPageError } from "@/modules/crm/pages/shared";
|
||
import { CrmBreadcrumbs } from "@/modules/crm/components/CrmBreadcrumbs";
|
||
import { CrmStatusChip } from "@/modules/crm/design-system";
|
||
|
||
/** Customers = unified view of contacts + organizations (no separate backend entity). */
|
||
export default function CustomersPage() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["crm", tenantId, "customers-view"],
|
||
queryFn: async () => {
|
||
const [contacts, orgs] = await Promise.all([
|
||
crmApi.contacts.list(tenantId!, { page: 1, page_size: 500 }),
|
||
crmApi.organizations.list(tenantId!, { page: 1, page_size: 500 }),
|
||
]);
|
||
return { contacts: contacts.filter((c) => !c.is_deleted), orgs: orgs.filter((o) => !o.is_deleted) };
|
||
},
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <CrmPageLoader />;
|
||
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
const { contacts, orgs } = q.data!;
|
||
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "مشتریان" }]} />
|
||
<PageHeader
|
||
title="مشتریان"
|
||
description="نمای یکپارچه مخاطبین و شرکتها — منبع حقیقت در Contacts و Organizations است."
|
||
actions={
|
||
<div className="flex gap-2">
|
||
<Link href="/crm/sales/contacts">
|
||
<Button variant="outline">مخاطبین</Button>
|
||
</Link>
|
||
<Link href="/crm/sales/companies">
|
||
<Button variant="outline">شرکتها</Button>
|
||
</Link>
|
||
</div>
|
||
}
|
||
/>
|
||
<div className="grid gap-6 lg:grid-cols-2">
|
||
<section>
|
||
<h2 className="mb-3 font-semibold text-secondary">مخاطبین ({contacts.length})</h2>
|
||
<div className="space-y-2">
|
||
{contacts.slice(0, 12).map((c) => (
|
||
<Card key={c.id}>
|
||
<CardContent className="flex items-center justify-between py-4">
|
||
<div>
|
||
<p className="font-medium">{c.full_name}</p>
|
||
<p className="text-xs text-[var(--muted)]">{c.email ?? c.phone ?? "—"}</p>
|
||
</div>
|
||
<CrmStatusChip status={c.status} domain="contact" />
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
{contacts.length === 0 ? <EmptyState title="مخاطبی نیست" /> : null}
|
||
</div>
|
||
</section>
|
||
<section>
|
||
<h2 className="mb-3 font-semibold text-secondary">شرکتها ({orgs.length})</h2>
|
||
<div className="space-y-2">
|
||
{orgs.slice(0, 12).map((o) => (
|
||
<Card key={o.id}>
|
||
<CardContent className="flex items-center justify-between py-4">
|
||
<div>
|
||
<p className="font-medium">{o.name}</p>
|
||
<p className="text-xs text-[var(--muted)]">{o.organization_number}</p>
|
||
</div>
|
||
<CrmStatusChip status={o.status} domain="organization" />
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
{orgs.length === 0 ? <EmptyState title="شرکتی نیست" /> : null}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|