Goods receipts and warehouse receipts now capture party vs warehouse separately; purchase returns link to invoices and reduce AP. Co-authored-by: Cursor <cursoragent@cursor.com>
645 lines
24 KiB
TypeScript
645 lines
24 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, 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, Archive, Pencil } from "lucide-react";
|
||
import { accountingApi, type Account, type ChartOfAccounts } from "@/lib/accounting-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import {
|
||
PageHeader,
|
||
Button,
|
||
Dialog,
|
||
Input,
|
||
Label,
|
||
FieldError,
|
||
Textarea,
|
||
DataTable,
|
||
EmptyState,
|
||
LoadingState,
|
||
ErrorState,
|
||
Badge,
|
||
Select,
|
||
Drawer,
|
||
TableToolbar,
|
||
Tabs,
|
||
Checkbox,
|
||
} from "@/components/ds";
|
||
|
||
const chartSchema = z.object({
|
||
code: z.string().min(1, "کد الزامی است"),
|
||
name: z.string().min(1, "نام الزامی است"),
|
||
description: z.string().optional(),
|
||
is_default: z.boolean().optional(),
|
||
});
|
||
|
||
const accountSchema = z.object({
|
||
chart_id: z.string().uuid("دفتر حساب را انتخاب کنید"),
|
||
code: z.string().min(1, "کد الزامی است"),
|
||
name: z.string().min(1, "نام الزامی است"),
|
||
account_type: z.string().min(1),
|
||
account_category: z.string().min(1),
|
||
parent_id: z.string().optional(),
|
||
is_group: z.boolean().optional(),
|
||
is_postable: z.boolean().optional(),
|
||
description: z.string().optional(),
|
||
});
|
||
|
||
type ChartForm = z.infer<typeof chartSchema>;
|
||
const editChartSchema = chartSchema.omit({ code: true }).extend({ code: z.string().optional() });
|
||
type EditChartForm = z.infer<typeof editChartSchema>;
|
||
type AccountForm = z.infer<typeof accountSchema>;
|
||
|
||
const ACCOUNT_TYPES = [
|
||
{ value: "asset", label: "دارایی" },
|
||
{ value: "liability", label: "بدهی" },
|
||
{ value: "equity", label: "حقوق صاحبان سهام" },
|
||
{ value: "revenue", label: "درآمد" },
|
||
{ value: "expense", label: "هزینه" },
|
||
];
|
||
|
||
const ACCOUNT_CATEGORIES = [
|
||
{ value: "current_asset", label: "دارایی جاری" },
|
||
{ value: "non_current_asset", label: "دارایی غیرجاری" },
|
||
{ value: "current_liability", label: "بدهی جاری" },
|
||
{ value: "non_current_liability", label: "بدهی غیرجاری" },
|
||
{ value: "equity", label: "حقوق صاحبان سهام" },
|
||
{ value: "operating_revenue", label: "درآمد عملیاتی" },
|
||
{ value: "other_revenue", label: "سایر درآمدها" },
|
||
{ value: "operating_expense", label: "هزینه عملیاتی" },
|
||
{ value: "other_expense", label: "سایر هزینهها" },
|
||
];
|
||
|
||
export default function ChartOfAccountsPage() {
|
||
const { tenantId } = useTenantId();
|
||
const qc = useQueryClient();
|
||
const [tab, setTab] = useState("accounts");
|
||
const [chartOpen, setChartOpen] = useState(false);
|
||
const [accountOpen, setAccountOpen] = useState(false);
|
||
const [editChart, setEditChart] = useState<ChartOfAccounts | null>(null);
|
||
const [selected, setSelected] = useState<Account | null>(null);
|
||
const [filterChart, setFilterChart] = useState<string>("");
|
||
const [search, setSearch] = useState("");
|
||
const [showInactive, setShowInactive] = useState(false);
|
||
|
||
const chartsQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "charts"],
|
||
queryFn: () => accountingApi.charts.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
const accountsQ = useQuery({
|
||
queryKey: ["accounting", tenantId, "accounts", filterChart],
|
||
queryFn: () => accountingApi.accounts.list(tenantId!, 1, 500, filterChart || undefined),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
const chartForm = useForm<ChartForm>({
|
||
resolver: zodResolver(chartSchema),
|
||
defaultValues: { code: "", name: "", description: "", is_default: false },
|
||
});
|
||
const accountForm = useForm<AccountForm>({
|
||
resolver: zodResolver(accountSchema),
|
||
defaultValues: {
|
||
chart_id: "",
|
||
code: "",
|
||
name: "",
|
||
account_type: "asset",
|
||
account_category: "current_asset",
|
||
parent_id: "",
|
||
is_group: false,
|
||
is_postable: true,
|
||
description: "",
|
||
},
|
||
});
|
||
const editChartForm = useForm<EditChartForm>({
|
||
resolver: zodResolver(editChartSchema),
|
||
});
|
||
const editAccountForm = useForm({
|
||
defaultValues: { name: "", status: "active", description: "", is_postable: true },
|
||
});
|
||
|
||
const invalidate = async () => {
|
||
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "charts"] });
|
||
await qc.invalidateQueries({ queryKey: ["accounting", tenantId, "accounts"] });
|
||
};
|
||
|
||
const createChart = useMutation({
|
||
mutationFn: (data: ChartForm) => accountingApi.charts.create(tenantId!, data),
|
||
onSuccess: async () => {
|
||
toast.success("دفتر حساب ایجاد شد");
|
||
setChartOpen(false);
|
||
chartForm.reset();
|
||
await invalidate();
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const updateChart = useMutation({
|
||
mutationFn: (data: ChartForm) =>
|
||
accountingApi.charts.update(tenantId!, editChart!.id, {
|
||
name: data.name,
|
||
description: data.description,
|
||
is_default: data.is_default,
|
||
}),
|
||
onSuccess: async () => {
|
||
toast.success("دفتر بهروز شد");
|
||
setEditChart(null);
|
||
await invalidate();
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const archiveChart = useMutation({
|
||
mutationFn: (id: string) => accountingApi.charts.archive(tenantId!, id),
|
||
onSuccess: async () => {
|
||
toast.success("دفتر آرشیو شد");
|
||
await invalidate();
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const createAccount = useMutation({
|
||
mutationFn: (data: AccountForm) =>
|
||
accountingApi.accounts.create(tenantId!, {
|
||
...data,
|
||
parent_id: data.parent_id || null,
|
||
is_postable: data.is_postable ?? true,
|
||
is_group: data.is_group ?? false,
|
||
}),
|
||
onSuccess: async () => {
|
||
toast.success("حساب ایجاد شد");
|
||
setAccountOpen(false);
|
||
await invalidate();
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const updateAccount = useMutation({
|
||
mutationFn: (body: { name: string; status: string; description?: string; is_postable: boolean }) =>
|
||
accountingApi.accounts.update(tenantId!, selected!.id, body),
|
||
onSuccess: async () => {
|
||
toast.success("حساب بهروز شد");
|
||
setSelected(null);
|
||
await invalidate();
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const archiveAccount = useMutation({
|
||
mutationFn: (id: string) => accountingApi.accounts.archive(tenantId!, id),
|
||
onSuccess: async () => {
|
||
toast.success("حساب آرشیو شد");
|
||
setSelected(null);
|
||
await invalidate();
|
||
},
|
||
onError: (e: Error) => toast.error(e.message),
|
||
});
|
||
|
||
const rows = useMemo(() => {
|
||
let list = accountsQ.data ?? [];
|
||
if (!showInactive) list = list.filter((a) => a.status === "active");
|
||
if (search.trim()) {
|
||
const q = search.trim().toLowerCase();
|
||
list = list.filter(
|
||
(a) => a.code.toLowerCase().includes(q) || a.name.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
return [...list].sort((a, b) => a.code.localeCompare(b.code, "fa"));
|
||
}, [accountsQ.data, search, showInactive]);
|
||
|
||
if (!tenantId) return <LoadingState />;
|
||
if (chartsQ.isLoading || accountsQ.isLoading) return <LoadingState />;
|
||
if (chartsQ.error || accountsQ.error) {
|
||
return (
|
||
<ErrorState
|
||
message={(chartsQ.error || accountsQ.error)?.message || "خطا"}
|
||
onRetry={() => {
|
||
void chartsQ.refetch();
|
||
void accountsQ.refetch();
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
const chartName = (id: string) => chartsQ.data?.find((c) => c.id === id)?.name ?? id;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="دفتر حسابها"
|
||
description="درخت حسابها، ویرایش، آرشیو و جزئیات — API واقعی."
|
||
actions={
|
||
<>
|
||
<Button type="button" variant="outline" onClick={() => setChartOpen(true)}>
|
||
<Plus className="h-4 w-4" />
|
||
دفتر جدید
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
if (chartsQ.data?.[0]) accountForm.setValue("chart_id", filterChart || chartsQ.data[0].id);
|
||
setAccountOpen(true);
|
||
}}
|
||
disabled={!chartsQ.data?.length}
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
حساب جدید
|
||
</Button>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
<Tabs
|
||
className="mb-4"
|
||
value={tab}
|
||
onChange={setTab}
|
||
items={[
|
||
{ id: "accounts", label: "حسابها" },
|
||
{ id: "charts", label: "دفاتر" },
|
||
]}
|
||
/>
|
||
|
||
{tab === "charts" ? (
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{chartsQ.data?.map((c) => (
|
||
<div key={c.id} className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-4">
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div>
|
||
<p className="font-semibold text-secondary">{c.name}</p>
|
||
<p className="mt-1 text-sm text-[var(--muted)]" dir="ltr">
|
||
{c.code}
|
||
</p>
|
||
</div>
|
||
<div className="flex flex-col items-end gap-1">
|
||
{c.is_default ? <Badge tone="primary">پیشفرض</Badge> : null}
|
||
<Badge tone={c.is_active ? "success" : "default"}>
|
||
{c.is_active ? "فعال" : "آرشیو"}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
<div className="mt-3 flex flex-wrap gap-2">
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => {
|
||
setEditChart(c);
|
||
editChartForm.reset({
|
||
code: c.code,
|
||
name: c.name,
|
||
description: c.description ?? "",
|
||
is_default: c.is_default,
|
||
});
|
||
}}
|
||
>
|
||
<Pencil className="h-3.5 w-3.5" />
|
||
ویرایش
|
||
</Button>
|
||
{c.is_active ? (
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
onClick={() => archiveChart.mutate(c.id)}
|
||
>
|
||
<Archive className="h-3.5 w-3.5" />
|
||
آرشیو
|
||
</Button>
|
||
) : null}
|
||
<Button type="button" size="sm" variant="ghost" onClick={() => { setFilterChart(c.id); setTab("accounts"); }}>
|
||
مشاهده حسابها
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{!chartsQ.data?.length ? (
|
||
<EmptyState
|
||
title="دفتری ثبت نشده"
|
||
description="ابتدا یک دفتر حساب بسازید یا از راهاندازی قالب وارد کنید."
|
||
action={
|
||
<Button type="button" onClick={() => setChartOpen(true)}>
|
||
ایجاد دفتر
|
||
</Button>
|
||
}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
) : (
|
||
<>
|
||
<TableToolbar
|
||
search={search}
|
||
onSearchChange={setSearch}
|
||
searchPlaceholder="جستجوی کد یا نام…"
|
||
actions={
|
||
<>
|
||
<Select
|
||
value={filterChart}
|
||
onChange={(e) => setFilterChart(e.target.value)}
|
||
className="w-44"
|
||
>
|
||
<option value="">همه دفاتر</option>
|
||
{chartsQ.data?.map((c) => (
|
||
<option key={c.id} value={c.id}>
|
||
{c.name}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
<Checkbox
|
||
label="نمایش غیرفعال"
|
||
checked={showInactive}
|
||
onChange={(e) => setShowInactive(e.target.checked)}
|
||
/>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
{/* Mobile cards */}
|
||
<div className="mb-4 space-y-2 md:hidden">
|
||
{rows.map((r) => (
|
||
<button
|
||
key={r.id}
|
||
type="button"
|
||
className="w-full rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-3 text-right"
|
||
onClick={() => {
|
||
setSelected(r);
|
||
editAccountForm.reset({
|
||
name: r.name,
|
||
status: r.status,
|
||
description: r.description ?? "",
|
||
is_postable: r.is_postable,
|
||
});
|
||
}}
|
||
>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="font-medium text-secondary">{r.name}</span>
|
||
<Badge tone={r.status === "active" ? "success" : "default"}>{r.status}</Badge>
|
||
</div>
|
||
<p className="mt-1 text-xs text-[var(--muted)]" dir="ltr">
|
||
{r.code} · {chartName(r.chart_id)}
|
||
</p>
|
||
</button>
|
||
))}
|
||
{!rows.length ? <EmptyState title="حسابی نیست" description="حساب جدید اضافه کنید." /> : null}
|
||
</div>
|
||
|
||
<div className="hidden md:block">
|
||
<DataTable
|
||
columns={[
|
||
{
|
||
key: "code",
|
||
header: "کد",
|
||
render: (r) => (
|
||
<span style={{ paddingInlineStart: `${(Number(r.level) - 1) * 12}px` }} dir="ltr">
|
||
{String(r.code)}
|
||
</span>
|
||
),
|
||
},
|
||
{ key: "name", header: "نام" },
|
||
{
|
||
key: "chart_id",
|
||
header: "دفتر",
|
||
render: (r) => chartName(String(r.chart_id)),
|
||
},
|
||
{
|
||
key: "account_type",
|
||
header: "نوع",
|
||
render: (r) =>
|
||
ACCOUNT_TYPES.find((t) => t.value === r.account_type)?.label ?? String(r.account_type),
|
||
},
|
||
{
|
||
key: "status",
|
||
header: "وضعیت",
|
||
render: (r) => (
|
||
<Badge tone={r.status === "active" ? "success" : "default"}>{String(r.status)}</Badge>
|
||
),
|
||
},
|
||
{
|
||
key: "id",
|
||
header: "",
|
||
render: (r) => (
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
onClick={() => {
|
||
const acc = r as unknown as Account;
|
||
setSelected(acc);
|
||
editAccountForm.reset({
|
||
name: acc.name,
|
||
status: acc.status,
|
||
description: acc.description ?? "",
|
||
is_postable: acc.is_postable,
|
||
});
|
||
}}
|
||
>
|
||
جزئیات
|
||
</Button>
|
||
),
|
||
},
|
||
]}
|
||
rows={rows as unknown as Record<string, unknown>[]}
|
||
empty={
|
||
<EmptyState
|
||
title="حسابی ثبت نشده"
|
||
description="ابتدا دفتر بسازید یا از قالب وارد کنید."
|
||
action={
|
||
<Button type="button" onClick={() => setChartOpen(true)}>
|
||
ایجاد دفتر حساب
|
||
</Button>
|
||
}
|
||
/>
|
||
}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
<Dialog open={chartOpen} onClose={() => setChartOpen(false)} title="ایجاد دفتر حساب">
|
||
<form className="space-y-3" onSubmit={chartForm.handleSubmit((d) => createChart.mutate(d))}>
|
||
<div>
|
||
<Label htmlFor="chart-code">کد</Label>
|
||
<Input id="chart-code" {...chartForm.register("code")} />
|
||
<FieldError>{chartForm.formState.errors.code?.message}</FieldError>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="chart-name">نام</Label>
|
||
<Input id="chart-name" {...chartForm.register("name")} />
|
||
<FieldError>{chartForm.formState.errors.name?.message}</FieldError>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="chart-desc">توضیح</Label>
|
||
<Textarea id="chart-desc" {...chartForm.register("description")} />
|
||
</div>
|
||
<Checkbox label="دفتر پیشفرض" {...chartForm.register("is_default")} />
|
||
<div className="flex justify-end gap-2 pt-2">
|
||
<Button type="button" variant="outline" onClick={() => setChartOpen(false)}>
|
||
انصراف
|
||
</Button>
|
||
<Button type="submit" disabled={createChart.isPending}>
|
||
ذخیره
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Dialog>
|
||
|
||
<Dialog open={!!editChart} onClose={() => setEditChart(null)} title="ویرایش دفتر حساب">
|
||
<form className="space-y-3" onSubmit={editChartForm.handleSubmit((d) => updateChart.mutate(d as ChartForm))}>
|
||
<div>
|
||
<Label>کد</Label>
|
||
<Input disabled value={editChart?.code ?? ""} />
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="edit-chart-name">نام</Label>
|
||
<Input id="edit-chart-name" {...editChartForm.register("name")} />
|
||
</div>
|
||
<div>
|
||
<Label>توضیح</Label>
|
||
<Textarea {...editChartForm.register("description")} />
|
||
</div>
|
||
<Checkbox label="دفتر پیشفرض" {...editChartForm.register("is_default")} />
|
||
<div className="flex justify-end gap-2">
|
||
<Button type="button" variant="outline" onClick={() => setEditChart(null)}>
|
||
انصراف
|
||
</Button>
|
||
<Button type="submit" disabled={updateChart.isPending}>
|
||
ذخیره
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Dialog>
|
||
|
||
<Dialog open={accountOpen} onClose={() => setAccountOpen(false)} title="ایجاد حساب" size="lg">
|
||
<form
|
||
className="grid gap-3 sm:grid-cols-2"
|
||
onSubmit={accountForm.handleSubmit((d) => createAccount.mutate(d))}
|
||
>
|
||
<div className="sm:col-span-2">
|
||
<Label>دفتر حساب</Label>
|
||
<Select {...accountForm.register("chart_id")}>
|
||
<option value="">انتخاب کنید</option>
|
||
{chartsQ.data?.filter((c) => c.is_active).map((c) => (
|
||
<option key={c.id} value={c.id}>
|
||
{c.code} — {c.name}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
<FieldError>{accountForm.formState.errors.chart_id?.message}</FieldError>
|
||
</div>
|
||
<div>
|
||
<Label>کد</Label>
|
||
<Input {...accountForm.register("code")} />
|
||
<FieldError>{accountForm.formState.errors.code?.message}</FieldError>
|
||
</div>
|
||
<div>
|
||
<Label>نام</Label>
|
||
<Input {...accountForm.register("name")} />
|
||
<FieldError>{accountForm.formState.errors.name?.message}</FieldError>
|
||
</div>
|
||
<div>
|
||
<Label>نوع</Label>
|
||
<Select {...accountForm.register("account_type")}>
|
||
{ACCOUNT_TYPES.map((t) => (
|
||
<option key={t.value} value={t.value}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label>دسته</Label>
|
||
<Select {...accountForm.register("account_category")}>
|
||
{ACCOUNT_CATEGORIES.map((t) => (
|
||
<option key={t.value} value={t.value}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
<div className="sm:col-span-2">
|
||
<Label>حساب والد</Label>
|
||
<Select {...accountForm.register("parent_id")}>
|
||
<option value="">بدون والد</option>
|
||
{(accountsQ.data ?? [])
|
||
.filter((a) => a.status === "active")
|
||
.map((a) => (
|
||
<option key={a.id} value={a.id}>
|
||
{a.code} — {a.name}
|
||
</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
<Checkbox label="گروه حساب" {...accountForm.register("is_group")} />
|
||
<Checkbox label="قابل ثبت سند" {...accountForm.register("is_postable")} />
|
||
<div className="flex justify-end gap-2 sm:col-span-2">
|
||
<Button type="button" variant="outline" onClick={() => setAccountOpen(false)}>
|
||
انصراف
|
||
</Button>
|
||
<Button type="submit" disabled={createAccount.isPending}>
|
||
ذخیره
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Dialog>
|
||
|
||
<Drawer
|
||
open={!!selected}
|
||
onClose={() => setSelected(null)}
|
||
title={selected ? `${selected.code} — ${selected.name}` : "حساب"}
|
||
description={selected ? chartName(selected.chart_id) : undefined}
|
||
>
|
||
{selected ? (
|
||
<form
|
||
className="space-y-3"
|
||
onSubmit={editAccountForm.handleSubmit((d) =>
|
||
updateAccount.mutate({
|
||
name: d.name,
|
||
status: d.status,
|
||
description: d.description,
|
||
is_postable: d.is_postable,
|
||
})
|
||
)}
|
||
>
|
||
<div>
|
||
<Label>نام</Label>
|
||
<Input {...editAccountForm.register("name")} />
|
||
</div>
|
||
<div>
|
||
<Label>وضعیت</Label>
|
||
<Select {...editAccountForm.register("status")}>
|
||
<option value="active">active</option>
|
||
<option value="inactive">inactive</option>
|
||
<option value="closed">closed</option>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label>توضیح</Label>
|
||
<Textarea {...editAccountForm.register("description")} />
|
||
</div>
|
||
<Checkbox label="قابل ثبت سند" {...editAccountForm.register("is_postable")} />
|
||
<p className="text-xs text-[var(--muted)]">
|
||
نوع: {selected.account_type} · دسته: {selected.account_category} · سطح: {selected.level}
|
||
</p>
|
||
<div className="flex flex-wrap gap-2 pt-2">
|
||
<Button type="submit" disabled={updateAccount.isPending}>
|
||
ذخیره تغییرات
|
||
</Button>
|
||
{selected.status === "active" ? (
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => archiveAccount.mutate(selected.id)}
|
||
>
|
||
آرشیو
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</form>
|
||
) : null}
|
||
</Drawer>
|
||
</div>
|
||
);
|
||
}
|