"use client"; /** * Typed operational document workflows for domains without dedicated engines yet. * Still real /ops API — NOT the generic number/party/amount stub. */ import { useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { toast } from "sonner"; import { z } from "zod"; import { accountingApi } from "@/lib/accounting-api"; import { useTenantId } from "@/hooks/useTenantId"; import { formatMoney, parseMoneyInput } from "@/lib/utils"; import { Badge, Button, DataTable, DatePicker, Dialog, EmptyState, ErrorState, FormField, Input, JalaliDateText, LoadingState, MoneyInput, PageHeader, Select, } from "@/components/ds"; import { PartyCombobox } from "@/components/accounting/EntityCombobox"; type FieldDef = | { key: string; label: string; kind: "text" | "money" | "date" | "select"; options?: { value: string; label: string }[]; required?: boolean } | { key: string; label: string; kind: "party"; module?: string; required?: boolean }; export function SpecializedOpsPage({ title, description, module, docType, createLabel, fields, amountKey = "amount", }: { title: string; description: string; module: string; docType: string; createLabel: string; fields: FieldDef[]; amountKey?: string; }) { const { tenantId } = useTenantId(); const qc = useQueryClient(); const [open, setOpen] = useState(false); const shape: Record = { number: z.string().optional(), doc_date: z.string().min(1, "تاریخ الزامی است"), party_name: z.string().optional(), party_id: z.string().optional(), description: z.string().optional(), }; for (const f of fields) { if (f.kind === "party") continue; shape[f.key] = f.required === false ? z.string().optional() : z.string().min(1, `${f.label} الزامی است`); } const schema = z.object(shape); type Values = z.infer; const defaults = useMemo(() => { const d: Record = { number: "", doc_date: new Date().toISOString().slice(0, 10), party_name: "", party_id: "", description: "", }; for (const f of fields) { if (f.kind === "party") continue; d[f.key] = f.kind === "money" ? "0" : f.kind === "date" ? new Date().toISOString().slice(0, 10) : ""; } return d as Values; }, [fields]); const form = useForm({ resolver: zodResolver(schema), defaultValues: defaults }); const listQ = useQuery({ queryKey: ["accounting", tenantId, "ops-docs", module, docType], queryFn: () => accountingApi.ops.listDocuments(tenantId!, module, docType), enabled: !!tenantId, }); const createM = useMutation({ mutationFn: (v: Values) => { const meta: Record = {}; for (const f of fields) { if (f.kind === "party") continue; const raw = String((v as Record)[f.key] ?? ""); meta[f.key] = f.kind === "money" ? parseMoneyInput(raw) || "0" : raw; } const amount = parseMoneyInput(String((v as Record)[amountKey] ?? meta[amountKey] ?? "0")) || "0"; return accountingApi.ops.createDocument(tenantId!, { module, doc_type: docType, number: String(v.number || "").trim() || undefined, doc_date: String(v.doc_date), party_name: v.party_name ? String(v.party_name) : undefined, party_id: v.party_id ? String(v.party_id) : undefined, amount, description: v.description ? String(v.description) : undefined, status: "draft", meta_json: JSON.stringify(meta), }); }, onSuccess: async () => { toast.success(`${title} ثبت شد`); setOpen(false); form.reset(defaults); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] }); }, onError: (e: Error) => toast.error(e.message), }); const openCreate = async () => { form.reset(defaults); setOpen(true); try { const seq = await accountingApi.setup.peekNumber(tenantId!, `ops.${module}.${docType}`); form.setValue("number" as never, seq.preview as never); } catch { /* BE allocates on blank */ } }; const confirmM = useMutation({ mutationFn: (id: string) => accountingApi.ops.confirmDocument(tenantId!, id), onSuccess: async () => { toast.success("تأیید شد"); await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "ops-docs", module, docType] }); }, onError: (e: Error) => toast.error(e.message), }); if (!tenantId || listQ.isLoading) return ; if (listQ.error) return listQ.refetch()} />; return (
void openCreate()}> {createLabel} } /> }, { key: "party_name", header: "طرف حساب" }, { key: "amount", header: "مبلغ", render: (r) => formatMoney(String(r.amount ?? "0")) }, { key: "status", header: "وضعیت", render: (r) => {String(r.status)}, }, { key: "id", header: "عملیات", render: (r) => r.status === "draft" ? ( ) : null, }, ]} rows={(listQ.data ?? []) as unknown as Record[]} empty={ void openCreate()}> {createLabel} } /> } /> setOpen(false)} title={createLabel} size="lg">
createM.mutate(v))}> ( )} /> {fields.map((f) => { if (f.kind === "party") { return ( { form.setValue("party_name", party?.name || ""); form.setValue("party_id", party?.id || ""); }} /> ); } if (f.kind === "money") { return ( ); } if (f.kind === "date") { return ( ( )} /> ); } if (f.kind === "select") { return ( ); } return ( ); })}
); }