TorbatYar/frontend/modules/crm/features/sales/customers.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

88 lines
3.6 KiB
TypeScript
Raw 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 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>
);
}