Files
star-erp/resources/js/Pages/AccountPayable/Index.tsx
sky121113 e3df090afd
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m5s
feat: 統一各模組分頁組件佈局並新增系統設定功能相關檔案
2026-02-25 16:16:49 +08:00

289 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
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>
);
}