voucherQ.refetch()} />;
@@ -356,11 +357,49 @@ export default function VoucherDetailPage() {
-
+ (
+ {
+ const next =
+ typeof e === "object" && e && "target" in e
+ ? String((e as ChangeEvent).target.value)
+ : String(e ?? "0");
+ field.onChange(next);
+ }}
+ />
+ )}
+ />
-
+ (
+ {
+ const next =
+ typeof e === "object" && e && "target" in e
+ ? String((e as ChangeEvent).target.value)
+ : String(e ?? "0");
+ field.onChange(next);
+ }}
+ />
+ )}
+ />
@@ -419,9 +458,15 @@ export default function VoucherDetailPage() {
جمع بدهکار: {formatMoney(String(balance.debit))}
جمع بستانکار: {formatMoney(String(balance.credit))}
اختلاف: {formatMoney(String(Math.abs(balance.difference)))}
- {balance.isBalanced ? "متوازن ✓" : "نامتوازن — ذخیره مجاز نیست"}
+
+ {balance.isBalanced
+ ? "متوازن ✓"
+ : !balance.hasAmount
+ ? "مبالغ ردیفها را وارد کنید"
+ : "نامتوازن — ذخیره مجاز نیست"}
+
- {!balance.isBalanced ? (
+ {balance.hasAmount && !balance.isBalanced ? (
{unbalancedMessage(balance.debit, balance.credit)}
) : null}
diff --git a/frontend/app/accounting/vouchers/new/page.tsx b/frontend/app/accounting/vouchers/new/page.tsx
index bfeb481..6b52103 100644
--- a/frontend/app/accounting/vouchers/new/page.tsx
+++ b/frontend/app/accounting/vouchers/new/page.tsx
@@ -1,7 +1,8 @@
"use client";
-import { useEffect, useMemo } from "react";
-import { Controller, useFieldArray, useForm } from "react-hook-form";
+import { useEffect } from "react";
+import type { ChangeEvent } from "react";
+import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
@@ -130,8 +131,9 @@ export default function NewVoucherPage() {
onError: (e: Error) => toast.error(e.message),
});
- const watchedLines = form.watch("lines");
- const balance = useMemo(() => sumVoucherLines(watchedLines), [watchedLines]);
+ // useWatch (not useMemo on watch) — RHF mutates lines in place; memo would freeze at 0/0.
+ const watchedLines = useWatch({ control: form.control, name: "lines" });
+ const balance = sumVoucherLines(watchedLines);
if (!tenantId || periodsQ.isLoading || accountsQ.isLoading) return ;
if (periodsQ.error || accountsQ.error) {
@@ -258,11 +260,49 @@ export default function NewVoucherPage() {
-
+ (
+ {
+ const next =
+ typeof e === "object" && e && "target" in e
+ ? String((e as ChangeEvent).target.value)
+ : String(e ?? "0");
+ field.onChange(next);
+ }}
+ />
+ )}
+ />
-
+ (
+ {
+ const next =
+ typeof e === "object" && e && "target" in e
+ ? String((e as ChangeEvent).target.value)
+ : String(e ?? "0");
+ field.onChange(next);
+ }}
+ />
+ )}
+ />
@@ -320,9 +360,15 @@ export default function NewVoucherPage() {
جمع بدهکار: {formatMoney(String(balance.debit))}
جمع بستانکار: {formatMoney(String(balance.credit))}
اختلاف: {formatMoney(String(Math.abs(balance.difference)))}
- {balance.isBalanced ? "متوازن ✓" : "نامتوازن — ثبت مجاز نیست"}
+
+ {balance.isBalanced
+ ? "متوازن ✓"
+ : !balance.hasAmount
+ ? "مبالغ ردیفها را وارد کنید"
+ : "نامتوازن — ثبت مجاز نیست"}
+
- {!balance.isBalanced ? (
+ {balance.hasAmount && !balance.isBalanced ? (
{unbalancedMessage(balance.debit, balance.credit)}
) : null}
diff --git a/frontend/app/accounting/vouchers/page.tsx b/frontend/app/accounting/vouchers/page.tsx
index e84dd52..c10faea 100644
--- a/frontend/app/accounting/vouchers/page.tsx
+++ b/frontend/app/accounting/vouchers/page.tsx
@@ -2,7 +2,7 @@
import { Suspense, useMemo, useState } from "react";
import Link from "next/link";
-import { useSearchParams } from "next/navigation";
+import { useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { Plus } from "lucide-react";
import { accountingApi } from "@/lib/accounting-api";
@@ -18,6 +18,7 @@ import {
Badge,
JalaliDateText,
Pagination,
+ Select,
} from "@/components/ds";
const STATUS_FA: Record = {
@@ -28,41 +29,77 @@ const STATUS_FA: Record = {
cancelled: "لغو شده",
};
+const SOURCE_FA: Record = {
+ manual: "دستی",
+ sales: "فروش (اتومات)",
+ purchase: "خرید (اتومات)",
+ treasury: "خزانه (اتومات)",
+ payroll: "حقوق (اتومات)",
+ assets: "دارایی (اتومات)",
+};
+
+function sourceLabel(raw: unknown): string {
+ const s = String(raw ?? "").trim();
+ if (!s || s === "manual") return "دستی";
+ return SOURCE_FA[s] ?? `${s} (سیستمی)`;
+}
+
+function isAutoSource(raw: unknown): boolean {
+ const s = String(raw ?? "").trim();
+ return !!s && s !== "manual";
+}
+
const STATUS_TITLES: Record = {
draft: "اسناد پیشنویس",
reversed: "اسناد برگشتی",
- posted: "تأیید و ثبت اسناد",
+ posted: "اسناد ثبتقطعشده",
};
function VouchersList() {
const { tenantId } = useTenantId();
const sp = useSearchParams();
+ const router = useRouter();
const statusFilter = sp.get("status") || "";
+ const sourceFilter = sp.get("source") || "";
const [page, setPage] = useState(1);
- const pageSize = 50;
+ const pageSize = 100;
const listQ = useQuery({
- queryKey: ["accounting", tenantId, "vouchers", page, pageSize],
- queryFn: () => accountingApi.vouchers.list(tenantId!, page, pageSize),
+ queryKey: ["accounting", tenantId, "vouchers", page, pageSize, statusFilter, sourceFilter],
+ queryFn: () =>
+ accountingApi.vouchers.list(tenantId!, page, pageSize, {
+ status: statusFilter || undefined,
+ source: sourceFilter || undefined,
+ }),
enabled: !!tenantId,
});
- const rows = useMemo(() => {
- const all = listQ.data ?? [];
- if (!statusFilter) return all;
- return all.filter((v) => String(v.status) === statusFilter);
- }, [listQ.data, statusFilter]);
+ const rows = useMemo(() => listQ.data ?? [], [listQ.data]);
+
+ const setSource = (source: string) => {
+ const params = new URLSearchParams();
+ if (statusFilter) params.set("status", statusFilter);
+ if (source) params.set("source", source);
+ const q = params.toString();
+ router.push(q ? `/accounting/vouchers?${q}` : "/accounting/vouchers");
+ setPage(1);
+ };
if (!tenantId || listQ.isLoading) return ;
if (listQ.error) return listQ.refetch()} />;
- const title = STATUS_TITLES[statusFilter] || "لیست اسناد حسابداری";
+ const title =
+ sourceFilter === "auto"
+ ? "اسناد سیستمی / اتومات"
+ : sourceFilter === "manual"
+ ? "اسناد دستی"
+ : STATUS_TITLES[statusFilter] || "لیست اسناد حسابداری";
return (