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>
297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useForm } from "react-hook-form";
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { toast } from "sonner";
|
||
import {
|
||
Button,
|
||
Dialog,
|
||
FormField,
|
||
Input,
|
||
PageHeader,
|
||
Card,
|
||
CardContent,
|
||
Badge,
|
||
EmptyState,
|
||
} from "@/components/ds";
|
||
import { crmApi } from "@/modules/crm/services/crm-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { useMe } from "@/hooks/useMe";
|
||
import {
|
||
CRM_PERMISSIONS,
|
||
CRM_PERMISSION_GROUPS,
|
||
} from "@/modules/crm/constants/permissions";
|
||
import { CrmPageLoader, CrmPageError, useCrmHealth } from "@/modules/crm/pages/shared";
|
||
import { CrmBreadcrumbs } from "@/modules/crm/components/CrmBreadcrumbs";
|
||
import { CrmScopeNotice } from "@/modules/crm/components/CrmScopeNotice";
|
||
import { CrmTablePage } from "@/modules/crm/design-system";
|
||
|
||
export function NotificationsPage() {
|
||
const { tenantId } = useTenantId();
|
||
const qc = useQueryClient();
|
||
const q = useQuery({
|
||
queryKey: ["crm", tenantId, "mentions-unread"],
|
||
queryFn: () => crmApi.mentions.unread(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
const readM = useMutation({
|
||
mutationFn: (id: string) => crmApi.mentions.markRead(tenantId!, id),
|
||
onSuccess: () => qc.invalidateQueries({ queryKey: ["crm", tenantId, "mentions-unread"] }),
|
||
});
|
||
|
||
if (!tenantId || q.isLoading) return <CrmPageLoader />;
|
||
if (q.error) return <CrmPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "اعلانها" }]} />
|
||
<PageHeader title="اعلانها" description="Mentionهای خواندهنشده — delivery از Notification Platform." />
|
||
<div className="space-y-2">
|
||
{(q.data ?? []).map((m) => (
|
||
<Card key={m.id}>
|
||
<CardContent className="flex items-center justify-between py-4">
|
||
<span className="text-sm">Mention #{m.id.slice(0, 8)}</span>
|
||
<Button size="sm" variant="outline" onClick={() => readM.mutate(m.id)}>
|
||
خوانده شد
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
{(q.data ?? []).length === 0 ? <EmptyState title="اعلان جدیدی نیست" /> : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function RolesPage() {
|
||
const { tenantId } = useTenantId();
|
||
const [open, setOpen] = useState(false);
|
||
const form = useForm({ defaultValues: { code: "", name: "" } });
|
||
const createM = useMutation({
|
||
mutationFn: (d: Record<string, string>) => crmApi.team.roles.create(tenantId!, d),
|
||
onSuccess: () => {
|
||
toast.success("نقش تیم ثبت شد");
|
||
setOpen(false);
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const membersQ = useQuery({
|
||
queryKey: ["crm", tenantId, "team-members"],
|
||
queryFn: () => crmApi.team.members.list(tenantId!, { page: 1, page_size: 500 }),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || membersQ.isLoading) return <CrmPageLoader />;
|
||
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "نقشها" }]} />
|
||
<PageHeader
|
||
title="نقش تیم فروش"
|
||
description="Sales team roles — فقط create API؛ list roles موجود نیست."
|
||
actions={<Button onClick={() => setOpen(true)}>نقش جدید</Button>}
|
||
/>
|
||
<CrmTablePage
|
||
title="اعضای تیم"
|
||
columns={[
|
||
{ key: "user_id", header: "User ID" },
|
||
{ key: "display_name", header: "نام" },
|
||
{
|
||
key: "is_active",
|
||
header: "فعال",
|
||
render: (r) => (r.is_active ? "بله" : "خیر"),
|
||
},
|
||
]}
|
||
rows={(membersQ.data ?? []) as unknown as Record<string, unknown>[]}
|
||
/>
|
||
<Dialog open={open} onClose={() => setOpen(false)} title="نقش تیم">
|
||
<form className="space-y-3" onSubmit={form.handleSubmit((d) => createM.mutate(d))}>
|
||
<FormField label="کد">
|
||
<Input {...form.register("code")} />
|
||
</FormField>
|
||
<FormField label="نام">
|
||
<Input {...form.register("name")} />
|
||
</FormField>
|
||
<Button type="submit" disabled={createM.isPending}>
|
||
ذخیره
|
||
</Button>
|
||
</form>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function PermissionsPage() {
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "دسترسیها" }]} />
|
||
<PageHeader
|
||
title="درخت دسترسی CRM"
|
||
description="مرجع static از definitions.py — RBAC API در CRM وجود ندارد."
|
||
/>
|
||
<div className="space-y-4">
|
||
{CRM_PERMISSION_GROUPS.map((g) => (
|
||
<Card key={g.label}>
|
||
<CardContent className="pt-5">
|
||
<h3 className="mb-2 font-semibold">{g.label}</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
{g.permissions.map((p) => (
|
||
<Badge key={p} tone="default">
|
||
{p}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
<Card>
|
||
<CardContent className="pt-5">
|
||
<h3 className="mb-2 font-semibold">همه ({CRM_PERMISSIONS.length})</h3>
|
||
<div className="flex flex-wrap gap-1">
|
||
{CRM_PERMISSIONS.map((p) => (
|
||
<Badge key={p} tone="default">
|
||
{p}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function AuditLogsPage() {
|
||
const { tenantId } = useTenantId();
|
||
const [entityType, setEntityType] = useState("lead");
|
||
const [entityId, setEntityId] = useState("");
|
||
const q = useQuery({
|
||
queryKey: ["crm", tenantId, "audit", entityType, entityId],
|
||
queryFn: () => crmApi.audit.list(tenantId!, entityType, entityId),
|
||
enabled: !!tenantId && !!entityId,
|
||
});
|
||
|
||
if (!tenantId) return <CrmPageLoader />;
|
||
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "لاگ ممیزی" }]} />
|
||
<PageHeader title="ممیزی per-entity" description="GET /audit?entity_type&entity_id — بدون global audit log." />
|
||
<div className="mb-4 flex gap-3">
|
||
<Input placeholder="entity_type" value={entityType} onChange={(e) => setEntityType(e.target.value)} />
|
||
<Input placeholder="entity UUID" value={entityId} onChange={(e) => setEntityId(e.target.value)} className="max-w-md" />
|
||
</div>
|
||
{entityId && q.isLoading ? <CrmPageLoader /> : null}
|
||
{q.error ? <CrmPageError error={q.error} onRetry={() => q.refetch()} /> : null}
|
||
{entityId && q.data ? (
|
||
<CrmTablePage
|
||
title="رویدادهای ممیزی"
|
||
columns={[
|
||
{ key: "action", header: "عمل" },
|
||
{ key: "actor_user_id", header: "کاربر" },
|
||
{ key: "created_at", header: "زمان" },
|
||
]}
|
||
rows={q.data as unknown as Record<string, unknown>[]}
|
||
/>
|
||
) : (
|
||
!entityId && <EmptyState title="UUID موجودیت را وارد کنید" />
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function SettingsPage() {
|
||
const { tenantId } = useTenantId();
|
||
const { me } = useMe();
|
||
const form = useForm({
|
||
defaultValues: {
|
||
email_mentions: true,
|
||
in_app_mentions: true,
|
||
task_reminders: true,
|
||
},
|
||
});
|
||
const saveM = useMutation({
|
||
mutationFn: (d: Record<string, boolean>) =>
|
||
crmApi.collaborationPreferences.upsert(tenantId!, me!.user_id, d),
|
||
onSuccess: () => toast.success("تنظیمات ذخیره شد"),
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
if (!tenantId || !me) return <CrmPageLoader />;
|
||
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "تنظیمات" }]} />
|
||
<PageHeader title="تنظیمات همکاری" description="Notification preferences per user." />
|
||
<form className="max-w-md space-y-4" onSubmit={form.handleSubmit((d) => saveM.mutate(d))}>
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" {...form.register("email_mentions")} />
|
||
ایمیل mention
|
||
</label>
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" {...form.register("in_app_mentions")} />
|
||
in-app mention
|
||
</label>
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input type="checkbox" {...form.register("task_reminders")} />
|
||
یادآور وظایف
|
||
</label>
|
||
<Button type="submit" disabled={saveM.isPending}>
|
||
ذخیره
|
||
</Button>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function HealthPage() {
|
||
const healthQ = useCrmHealth();
|
||
if (healthQ.isLoading) return <CrmPageLoader />;
|
||
if (healthQ.error) return <CrmPageError error={healthQ.error} onRetry={() => healthQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "سلامت" }]} />
|
||
<PageHeader title="سلامت سرویس CRM" description="GET /health" />
|
||
<Card>
|
||
<CardContent className="space-y-2 pt-5">
|
||
<p>
|
||
<strong>Status:</strong> {healthQ.data?.status}
|
||
</p>
|
||
<p>
|
||
<strong>Service:</strong> {healthQ.data?.service}
|
||
</p>
|
||
<p>
|
||
<strong>Version:</strong> {healthQ.data?.version}
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function CapabilitiesPage() {
|
||
const healthQ = useCrmHealth();
|
||
return (
|
||
<div>
|
||
<CrmBreadcrumbs items={[{ label: "قابلیتها" }]} />
|
||
<PageHeader title="قابلیتهای CRM" description="/capabilities endpoint وجود ندارد." />
|
||
<CrmScopeNotice
|
||
title="Capability discovery unavailable"
|
||
description="CRM 6.3 فقط /health دارد. قابلیتهای فاز از docs/crm-phase-6-*.md استخراج میشود."
|
||
alternatives={[{ label: "Health", href: "/crm/sales/health" }]}
|
||
/>
|
||
{healthQ.data ? (
|
||
<Card className="mt-4">
|
||
<CardContent className="pt-5 text-sm">
|
||
نسخه فعلی: <strong>{healthQ.data.version}</strong> — Phase 6.3 Collaboration complete
|
||
</CardContent>
|
||
</Card>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|