TorbatYar/frontend/app/accounting/suppliers/page.tsx
Mortezakoohjani 7953e47f8b Ship accounting UX: Rial integers, auto doc numbers, reverse-and-edit, real reports, customers nav.
Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 15:52:06 +03:30

124 lines
4.1 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 { 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().optional(),
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")} placeholder="خودکار" />
<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>
);
}