"use client"; import { useState, useEffect, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { motion } from "motion/react"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Receipt, Download, Loader2, CheckCircle, FileText, Settings, Plus, Pencil, Trash2, } from "lucide-react"; import { api } from "@/lib/api/client"; interface VoucherEntry { account_code: string; account_name: string; debit_amount: number; credit_amount: number; summary: string; department?: string; } interface Voucher { id: number; voucher_number: string; voucher_date: string; period: string; summary: string; entries: VoucherEntry[]; total_debit: number; total_credit: number; status: string; confirmed_by?: number; confirmed_at?: string; created_at: string; } interface AccountMapping { id: number; standard_field: string; debit_account: string; debit_account_name: string; credit_account: string; credit_account_name: string; cost_center?: string; is_active: boolean; } const statusConfig: Record = { DRAFT: { label: "草稿", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" }, CONFIRMED: { label: "已确认", color: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300" }, EXPORTED: { label: "已导出", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" }, }; function formatCurrency(value: number): string { return new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY", minimumFractionDigits: 2, }).format(value); } export default function VouchersPage() { const searchParams = useSearchParams(); const [voucher, setVoucher] = useState(null); const [loading, setLoading] = useState(true); const [generating, setGenerating] = useState(false); const [taskId, setTaskId] = useState(searchParams.get("task_id") || ""); const [confirmOpen, setConfirmOpen] = useState(false); const [confirming, setConfirming] = useState(false); const [mappings, setMappings] = useState([]); const [mappingDialogOpen, setMappingDialogOpen] = useState(false); const [editingMapping, setEditingMapping] = useState(null); const [mappingForm, setMappingForm] = useState({ standard_field: "", debit_account: "", debit_account_name: "", credit_account: "", credit_account_name: "", cost_center: "", }); const loadVoucher = useCallback(async () => { if (!taskId) { setLoading(false); return; } setLoading(true); try { const res = await api.get(`/api/vouchers/task/${taskId}`); setVoucher(res.data); } catch (error) { console.error("加载凭证失败:", error); setVoucher(null); } finally { setLoading(false); } }, [taskId]); const loadMappings = useCallback(async () => { try { const res = await api.get(`/api/vouchers/account-mappings/list`); setMappings(res.data || []); } catch (error) { console.error("加载科目映射失败:", error); } }, []); useEffect(() => { loadVoucher(); loadMappings(); }, [loadVoucher, loadMappings]); async function handleGenerate() { if (!taskId) return; setGenerating(true); try { const res = await api.post(`/api/vouchers/generate`, { task_id: Number(taskId), }); setVoucher(res.data); } catch (error) { console.error("生成凭证失败:", error); } finally { setGenerating(false); } } async function handleConfirm() { if (!voucher) return; setConfirming(true); try { const userId = Number(localStorage.getItem("user_id") || "1"); const res = await api.post(`/api/vouchers/${voucher.id}/confirm`, { user_id: userId, }); setVoucher(res.data); setConfirmOpen(false); } catch (error) { console.error("确认凭证失败:", error); } finally { setConfirming(false); } } function handleExport(format: string) { if (!taskId) return; const token = localStorage.getItem("auth_token"); const companyId = localStorage.getItem("company_id"); const url = `${process.env.NEXT_PUBLIC_API_URL}/api/vouchers/task/${taskId}/export?format=${format}`; fetch(url, { headers: { Authorization: `Bearer ${token}`, "X-Company-ID": companyId || "", }, }) .then((res) => res.blob()) .then((blob) => { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `voucher_${taskId}.${format === "excel" ? "xlsx" : "csv"}`; a.click(); URL.revokeObjectURL(a.href); }); } function openMappingDialog(mapping?: AccountMapping) { if (mapping) { setEditingMapping(mapping); setMappingForm({ standard_field: mapping.standard_field, debit_account: mapping.debit_account, debit_account_name: mapping.debit_account_name, credit_account: mapping.credit_account, credit_account_name: mapping.credit_account_name, cost_center: mapping.cost_center || "", }); } else { setEditingMapping(null); setMappingForm({ standard_field: "", debit_account: "", debit_account_name: "", credit_account: "", credit_account_name: "", cost_center: "", }); } setMappingDialogOpen(true); } async function saveMapping() { try { if (editingMapping) { await api.put(`/api/vouchers/account-mappings/${editingMapping.id}`, mappingForm); } else { await api.post(`/api/vouchers/account-mappings`, mappingForm); } setMappingDialogOpen(false); loadMappings(); } catch (error) { console.error("保存科目映射失败:", error); } } async function deleteMapping(id: number) { try { await api.delete(`/api/vouchers/account-mappings/${id}`); loadMappings(); } catch (error) { console.error("删除科目映射失败:", error); } } return (

凭证管理

生成、预览和导出会计凭证

setTaskId(e.target.value)} className="h-9" />
{loading ? (
) : !voucher ? (

{taskId ? "该任务暂无凭证,请点击「生成凭证」" : "请输入任务ID查询凭证"}

) : ( <>
{voucher.voucher_number} {statusConfig[voucher.status]?.label || voucher.status}
{voucher.status === "DRAFT" && ( )}

凭证日期

{voucher.voucher_date}

会计期间

{voucher.period}

借方合计

{formatCurrency(voucher.total_debit)}

贷方合计

{formatCurrency(voucher.total_credit)}

摘要

{voucher.summary}

{voucher.confirmed_at && (
已于 {new Date(voucher.confirmed_at).toLocaleString("zh-CN")} 确认
)}
凭证分录 序号 科目代码 科目名称 摘要 借方金额 贷方金额 {voucher.entries.map((entry, i) => ( {i + 1} {entry.account_code} {entry.account_name} {entry.summary} {entry.debit_amount > 0 ? formatCurrency(entry.debit_amount) : "-"} {entry.credit_amount > 0 ? formatCurrency(entry.credit_amount) : "-"} ))} 合计 {formatCurrency(voucher.total_debit)} {formatCurrency(voucher.total_credit)}
)} {mappings.length > 0 && (
科目映射配置
标准字段 借方科目 贷方科目 成本中心 状态 操作 {mappings.map((m) => ( {m.standard_field} {m.debit_account} {m.debit_account_name} {m.credit_account} {m.credit_account_name} {m.cost_center || "-"} {m.is_active ? "启用" : "禁用"}
))}
)} {/* 确认对话框 */} 确认凭证

确认后凭证将不能修改。确认凭证后将可以导出金蝶格式文件。

{/* 科目映射对话框 */} {editingMapping ? "编辑科目映射" : "新增科目映射"}
setMappingForm({ ...mappingForm, standard_field: e.target.value })} placeholder="如:基本工资" />
setMappingForm({ ...mappingForm, debit_account: e.target.value })} placeholder="如:6601.01" />
setMappingForm({ ...mappingForm, debit_account_name: e.target.value })} placeholder="如:管理费用-工资" />
setMappingForm({ ...mappingForm, credit_account: e.target.value })} placeholder="如:2211.01" />
setMappingForm({ ...mappingForm, credit_account_name: e.target.value })} placeholder="如:应付职工薪酬-工资" />
setMappingForm({ ...mappingForm, cost_center: e.target.value })} placeholder="如:管理部" />
); }