All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m5s
289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
import { useState, useCallback } from 'react';
|
||
import { Head, Link, router } from '@inertiajs/react';
|
||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||
import {
|
||
Search,
|
||
Wallet,
|
||
Eye,
|
||
X,
|
||
} from "lucide-react";
|
||
import { StatusBadge } from '@/Components/shared/StatusBadge';
|
||
import { formatDate } from '@/lib/date';
|
||
import Pagination from '@/Components/shared/Pagination';
|
||
import { Can } from '@/Components/Permission/Can';
|
||
import { Button } from '@/Components/ui/button';
|
||
import { Input } from '@/Components/ui/input';
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
|
||
import { SearchableSelect } from '@/Components/ui/searchable-select';
|
||
import { debounce } from "lodash";
|
||
|
||
const STATUS_OPTIONS = [
|
||
{ value: 'all', label: '所有狀態' },
|
||
{ value: 'pending', label: '待處理' },
|
||
{ value: 'posted', label: '已入帳' },
|
||
{ value: 'paid', label: '已支付' },
|
||
{ value: 'voided', label: '已作廢' },
|
||
];
|
||
|
||
const getStatusBadgeVariant = (status: string) => {
|
||
switch (status) {
|
||
case 'pending': return 'warning';
|
||
case 'posted': return 'info';
|
||
case 'paid': return 'success';
|
||
case 'voided': return 'destructive';
|
||
default: return 'neutral';
|
||
}
|
||
};
|
||
|
||
const getStatusLabel = (status: string) => {
|
||
const found = STATUS_OPTIONS.find(opt => opt.value === status);
|
||
return found ? found.label : status;
|
||
};
|
||
|
||
export default function AccountPayableIndex({ payables, filters, vendors }: any) {
|
||
const [searchTerm, setSearchTerm] = useState(filters.search || "");
|
||
const [statusFilter, setStatusFilter] = useState(filters.status || "all");
|
||
const [vendorFilter, setVendorFilter] = useState(filters.vendor_id || "all");
|
||
const [perPage, setPerPage] = useState(filters.per_page || "10");
|
||
|
||
// 穩定的防抖過濾函式
|
||
const debouncedFilter = useCallback(
|
||
debounce((params: any) => {
|
||
router.get(route('account-payables.index'), params, {
|
||
preserveState: true,
|
||
replace: true,
|
||
preserveScroll: true,
|
||
});
|
||
}, 500),
|
||
[]
|
||
);
|
||
|
||
const handleSearchChange = (term: string) => {
|
||
setSearchTerm(term);
|
||
debouncedFilter({
|
||
...filters,
|
||
search: term,
|
||
status: statusFilter === "all" ? "" : statusFilter,
|
||
vendor_id: vendorFilter === "all" ? "" : vendorFilter,
|
||
page: 1
|
||
});
|
||
};
|
||
|
||
const handleClearSearch = () => {
|
||
setSearchTerm("");
|
||
debouncedFilter({
|
||
...filters,
|
||
search: "",
|
||
status: statusFilter === "all" ? "" : statusFilter,
|
||
vendor_id: vendorFilter === "all" ? "" : vendorFilter,
|
||
page: 1
|
||
});
|
||
};
|
||
|
||
const handleStatusChange = (value: string) => {
|
||
setStatusFilter(value);
|
||
debouncedFilter({
|
||
...filters,
|
||
search: searchTerm,
|
||
status: value === "all" ? "" : value,
|
||
vendor_id: vendorFilter === "all" ? "" : vendorFilter,
|
||
page: 1
|
||
});
|
||
};
|
||
|
||
const handleVendorChange = (value: string) => {
|
||
setVendorFilter(value);
|
||
debouncedFilter({
|
||
...filters,
|
||
search: searchTerm,
|
||
status: statusFilter === "all" ? "" : statusFilter,
|
||
vendor_id: value === "all" ? "" : value,
|
||
page: 1
|
||
});
|
||
};
|
||
|
||
const handlePerPageChange = (value: string) => {
|
||
setPerPage(value);
|
||
debouncedFilter({
|
||
...filters,
|
||
per_page: value,
|
||
page: 1
|
||
});
|
||
};
|
||
|
||
return (
|
||
<AuthenticatedLayout
|
||
breadcrumbs={[
|
||
{ label: '財務管理', href: '#' },
|
||
{ label: '應付帳款管理' }
|
||
]}
|
||
>
|
||
<Head title="應付帳款管理" />
|
||
|
||
<div className="container mx-auto p-6 max-w-7xl">
|
||
{/* 頁面標題 */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||
<Wallet className="h-6 w-6 text-primary-main" />
|
||
應付帳款管理
|
||
</h1>
|
||
<p className="text-gray-500 mt-1">
|
||
追蹤與供應商的應付帳項,管理採購入庫後的結算與付款狀態。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 篩選工具列 */}
|
||
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
|
||
<div className="flex flex-col md:flex-row gap-4">
|
||
{/* 搜尋 */}
|
||
<div className="flex-1 relative">
|
||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||
<Input
|
||
placeholder="搜尋單號或備註..."
|
||
value={searchTerm}
|
||
onChange={(e) => handleSearchChange(e.target.value)}
|
||
className="pl-10 pr-10 h-9"
|
||
/>
|
||
{searchTerm && (
|
||
<button
|
||
onClick={handleClearSearch}
|
||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 狀態篩選 */}
|
||
<SearchableSelect
|
||
value={statusFilter}
|
||
onValueChange={handleStatusChange}
|
||
options={[
|
||
{ label: "所有狀態", value: "all" },
|
||
{ label: "待處理", value: "pending" },
|
||
{ label: "已入帳", value: "posted" },
|
||
{ label: "已支付", value: "paid" },
|
||
{ label: "已作廢", value: "voided" }
|
||
]}
|
||
placeholder="選擇狀態"
|
||
className="w-full md:w-[160px] h-9"
|
||
showSearch={false}
|
||
/>
|
||
|
||
{/* 供應商篩選 */}
|
||
<SearchableSelect
|
||
value={vendorFilter}
|
||
onValueChange={handleVendorChange}
|
||
options={[
|
||
{ label: "所有供應商", value: "all" },
|
||
...vendors.map((v: any) => ({ label: v.name, value: v.id.toString() }))
|
||
]}
|
||
placeholder="選擇供應商"
|
||
className="w-full md:w-[200px] h-9"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||
<Table>
|
||
<TableHeader className="bg-gray-50">
|
||
<TableRow>
|
||
<TableHead className="w-[50px] text-center font-medium text-gray-600">#</TableHead>
|
||
<TableHead className="font-medium text-gray-600">應付單號</TableHead>
|
||
<TableHead className="font-medium text-gray-600">供應商</TableHead>
|
||
<TableHead className="font-medium text-gray-600 text-right">金額</TableHead>
|
||
<TableHead className="font-medium text-gray-600">到期日</TableHead>
|
||
<TableHead className="text-center font-medium text-gray-600">狀態</TableHead>
|
||
<TableHead className="text-center font-medium text-gray-600">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{payables.data.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className="text-center py-10 text-gray-500">
|
||
尚無應付帳款資料
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
payables.data.map((payable: any, index: number) => (
|
||
<TableRow
|
||
key={payable.id}
|
||
className="hover:bg-gray-50/50 transition-colors cursor-pointer group"
|
||
onClick={() => router.visit(route('account-payables.show', [payable.id]))}
|
||
>
|
||
<TableCell className="text-center text-gray-500 font-medium">
|
||
{(payables.current_page - 1) * payables.per_page + index + 1}
|
||
</TableCell>
|
||
<TableCell className="font-medium text-primary-main">
|
||
{payable.document_number}
|
||
</TableCell>
|
||
<TableCell className="text-gray-700">
|
||
{payable.vendor?.name}
|
||
</TableCell>
|
||
<TableCell className="text-right font-mono">
|
||
{new Intl.NumberFormat().format(payable.total_amount)}
|
||
</TableCell>
|
||
<TableCell className="text-gray-500 text-sm">
|
||
{formatDate(payable.due_date)}
|
||
</TableCell>
|
||
<TableCell className="text-center">
|
||
{/* @ts-ignore */}
|
||
<StatusBadge variant={getStatusBadgeVariant(payable.status)}>
|
||
{getStatusLabel(payable.status)}
|
||
</StatusBadge>
|
||
</TableCell>
|
||
<TableCell className="text-center">
|
||
<div
|
||
className="flex items-center justify-center gap-2"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<Can permission="account_payables.view">
|
||
<Link href={route('account-payables.show', [payable.id])}>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="button-outlined-primary"
|
||
title="查閱"
|
||
>
|
||
<Eye className="w-4 h-4 ml-0.5" />
|
||
</Button>
|
||
</Link>
|
||
</Can>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
|
||
<div className="mt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||
<span>每頁顯示</span>
|
||
<SearchableSelect
|
||
value={perPage}
|
||
onValueChange={handlePerPageChange}
|
||
options={[
|
||
{ label: "10", value: "10" },
|
||
{ label: "20", value: "20" },
|
||
{ label: "50", value: "50" },
|
||
{ label: "100", value: "100" }
|
||
]}
|
||
className="w-[90px] h-8"
|
||
showSearch={false}
|
||
/>
|
||
<span>筆</span>
|
||
</div>
|
||
<span className="text-sm text-gray-500">共 {payables.total} 筆資料</span>
|
||
</div>
|
||
<Pagination links={payables.links} />
|
||
</div>
|
||
</div>
|
||
</AuthenticatedLayout>
|
||
);
|
||
}
|