Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useForm } from "react-hook-form";
|
||
import { z } from "zod";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { toast } from "sonner";
|
||
import { Plus } from "lucide-react";
|
||
import { accountingApi } from "@/lib/accounting-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { formatMoney } from "@/lib/utils";
|
||
import {
|
||
PageHeader,
|
||
Button,
|
||
Dialog,
|
||
Input,
|
||
Label,
|
||
FieldError,
|
||
DataTable,
|
||
EmptyState,
|
||
LoadingState,
|
||
ErrorState,
|
||
Badge,
|
||
} from "@/components/ds";
|
||
|
||
const schema = z.object({
|
||
code: z.string().min(1, "کد الزامی است"),
|
||
name: z.string().min(1, "نام الزامی است"),
|
||
});
|
||
|
||
export default function SuppliersPage() {
|
||
const { tenantId } = useTenantId();
|
||
const qc = useQueryClient();
|
||
const [open, setOpen] = useState(false);
|
||
const form = useForm<z.infer<typeof schema>>({
|
||
resolver: zodResolver(schema),
|
||
defaultValues: { code: "", name: "" },
|
||
});
|
||
|
||
const listQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "suppliers"],
|
||
queryFn: () => accountingApi.suppliers.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
const createM = useMutation({
|
||
mutationFn: (d: z.infer<typeof schema>) => accountingApi.suppliers.create(tenantId!, d),
|
||
onSuccess: async () => {
|
||
toast.success("تأمینکننده ایجاد شد");
|
||
setOpen(false);
|
||
form.reset();
|
||
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "suppliers"] });
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
if (!tenantId || listQ.isLoading) return <LoadingState />;
|
||
if (listQ.error) return <ErrorState message={listQ.error.message} onRetry={() => listQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="تأمینکنندگان"
|
||
description="مدیریت تأمینکنندگان و مانده بدهی — API واقعی AR/AP."
|
||
actions={
|
||
<Button onClick={() => setOpen(true)}>
|
||
<Plus className="h-4 w-4" />
|
||
تأمینکننده جدید
|
||
</Button>
|
||
}
|
||
/>
|
||
|
||
<DataTable
|
||
columns={[
|
||
{ key: "code", header: "کد" },
|
||
{ key: "name", header: "نام" },
|
||
{
|
||
key: "outstanding_balance",
|
||
header: "مانده بدهی",
|
||
render: (r) => formatMoney(String(r.outstanding_balance ?? "0")),
|
||
},
|
||
{
|
||
key: "is_active",
|
||
header: "وضعیت",
|
||
render: (r) => (
|
||
<Badge tone={r.is_active ? "success" : "default"}>
|
||
{r.is_active ? "فعال" : "غیرفعال"}
|
||
</Badge>
|
||
),
|
||
},
|
||
]}
|
||
rows={(listQ.data ?? []) as unknown as Record<string, unknown>[]}
|
||
empty={
|
||
<EmptyState title="تأمینکنندهای ثبت نشده" action={<Button onClick={() => setOpen(true)}>ایجاد</Button>} />
|
||
}
|
||
/>
|
||
|
||
<Dialog open={open} onClose={() => setOpen(false)} title="تأمینکننده جدید">
|
||
<form className="space-y-3" onSubmit={form.handleSubmit((d) => createM.mutate(d))}>
|
||
<div>
|
||
<Label>کد</Label>
|
||
<Input {...form.register("code")} />
|
||
<FieldError>{form.formState.errors.code?.message}</FieldError>
|
||
</div>
|
||
<div>
|
||
<Label>نام</Label>
|
||
<Input {...form.register("name")} />
|
||
<FieldError>{form.formState.errors.name?.message}</FieldError>
|
||
</div>
|
||
<div className="flex justify-end gap-2">
|
||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||
انصراف
|
||
</Button>
|
||
<Button type="submit" disabled={createM.isPending}>
|
||
ذخیره
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|