Files
star-erp/resources/js/Pages/Inventory/GoodsReceipt/Index.tsx
sky121113 220478641d
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m3s
feat: 更新庫存報表、銷售匯入及採購單相關功能
2026-02-10 17:18:59 +08:00

311 lines
14 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 AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Plus, Search, FileText, RotateCcw, Calendar } from 'lucide-react';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { SearchableSelect } from '@/Components/ui/searchable-select';
import Pagination from '@/Components/shared/Pagination';
import { useState, useEffect } from 'react';
import { Can } from '@/Components/Permission/Can';
import { getDateRange } from '@/utils/format';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import GoodsReceiptTable from '@/Components/Inventory/GoodsReceiptTable';
interface Warehouse {
id: number;
name: string;
type: string;
}
interface Filters {
search?: string;
status?: string;
warehouse_id?: string;
date_start?: string;
date_end?: string;
per_page?: string;
}
interface Props {
receipts: any;
filters: Filters;
warehouses: Warehouse[];
}
export default function GoodsReceiptIndex({ receipts, filters, warehouses }: Props) {
const [search, setSearch] = useState(filters.search || '');
const [status, setStatus] = useState(filters.status || 'all');
const [warehouseId, setWarehouseId] = useState(filters.warehouse_id || 'all');
const [dateStart, setDateStart] = useState(filters.date_start || '');
const [dateEnd, setDateEnd] = useState(filters.date_end || '');
const [perPage, setPerPage] = useState(filters.per_page || '10');
const [dateRangeType, setDateRangeType] = useState('custom');
// Advanced Filter Toggle
// Sync filters from props
useEffect(() => {
setSearch(filters.search || '');
setStatus(filters.status || 'all');
setWarehouseId(filters.warehouse_id || 'all');
setDateStart(filters.date_start || '');
setDateEnd(filters.date_end || '');
setPerPage(filters.per_page || '10');
}, [filters]);
const handleFilter = () => {
router.get(route('goods-receipts.index'), {
search,
status: status !== 'all' ? status : undefined,
warehouse_id: warehouseId !== 'all' ? warehouseId : undefined,
date_start: dateStart || undefined,
date_end: dateEnd || undefined,
per_page: perPage,
}, { preserveState: true, replace: true });
};
const handleReset = () => {
setSearch('');
setStatus('all');
setWarehouseId('all');
setDateStart('');
setDateEnd('');
setDateRangeType('custom');
setPerPage('10');
router.get(route('goods-receipts.index'), {}, { preserveState: false });
};
const handleDateRangeChange = (type: string) => {
setDateRangeType(type);
if (type === 'custom') return;
const { start, end } = getDateRange(type);
setDateStart(start);
setDateEnd(end);
};
const handlePerPageChange = (value: string) => {
setPerPage(value);
router.get(route('goods-receipts.index'), {
search,
status: status !== 'all' ? status : undefined,
warehouse_id: warehouseId !== 'all' ? warehouseId : undefined,
date_start: dateStart || undefined,
date_end: dateEnd || undefined,
per_page: value,
}, { preserveState: true, preserveScroll: true, replace: true });
};
const statusOptions = [
{ label: '全部狀態', value: 'all' },
{ label: '已完成', value: 'completed' },
{ label: '處理中', value: 'processing' },
];
const warehouseOptions = [
{ label: '全部倉庫', value: 'all' },
...warehouses.map(w => ({ label: w.name, value: w.id.toString() }))
];
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '供應鏈管理', href: '#' },
{ label: '進貨單管理', href: route('goods-receipts.index'), isPage: true },
]}
>
<Head title="進貨單管理" />
<div className="container mx-auto p-6 max-w-7xl">
{/* Header Section */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
<FileText className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
<Can permission="goods_receipts.create">
<Link href={route('goods-receipts.create')}>
<Button className="button-filled-primary">
<Plus className="mr-2 h-4 w-4" />
</Button>
</Link>
</Can>
</div>
{/* Filter Bar */}
<div className="bg-white rounded-xl shadow-sm border border-grey-4 p-5 mb-6">
<div className="space-y-4">
{/* Row 1: Date Range & Quick Buttons */}
<div className="flex flex-col lg:flex-row gap-4 lg:items-end">
<div className="flex-none space-y-2">
<Label className="text-xs font-medium text-grey-2"></Label>
<div className="flex flex-wrap gap-2">
{[
{ label: "今日", value: "today" },
{ label: "昨日", value: "yesterday" },
{ label: "本週", value: "this_week" },
{ label: "本月", value: "this_month" },
{ label: "上月", value: "last_month" },
].map((opt) => (
<Button
key={opt.value}
size="sm"
onClick={() => handleDateRangeChange(opt.value)}
className={
dateRangeType === opt.value
? 'button-filled-primary h-9 px-4 shadow-sm'
: 'button-outlined-primary h-9 px-4 bg-white'
}
>
{opt.label}
</Button>
))}
</div>
</div>
{/* Date Inputs */}
<div className="w-full lg:flex-1">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs text-grey-2 font-medium"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
type="date"
value={dateStart}
onChange={(e) => {
setDateStart(e.target.value);
setDateRangeType('custom');
}}
className="pl-9 block w-full h-9 bg-white"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs text-grey-2 font-medium"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
type="date"
value={dateEnd}
onChange={(e) => {
setDateEnd(e.target.value);
setDateRangeType('custom');
}}
className="pl-9 block w-full h-9 bg-white text-left"
/>
</div>
</div>
</div>
</div>
</div>
{/* Row 2: Filters & Actions */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
{/* Search */}
<div className="md:col-span-4 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜尋單號..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 h-9 block"
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
</div>
</div>
{/* Status */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger className="h-9">
<SelectValue placeholder="選擇狀態" />
</SelectTrigger>
<SelectContent>
{statusOptions.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Warehouse */}
<div className="md:col-span-3 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<SearchableSelect
value={warehouseId}
onValueChange={setWarehouseId}
options={warehouseOptions}
placeholder="選擇倉庫"
className="w-full h-9"
showSearch={warehouses.length > 10}
/>
</div>
{/* Actions */}
<div className="md:col-span-3 flex items-center justify-end gap-2">
<Button
variant="outline"
onClick={handleReset}
className="flex-1 flex items-center justify-center gap-2 button-outlined-primary h-9"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
onClick={handleFilter}
className="flex-1 flex items-center justify-center gap-2 button-filled-primary h-9"
>
<Search className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
{/* Table Section */}
<GoodsReceiptTable receipts={receipts.data} />
{/* Pagination */}
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between 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-[100px] h-8"
showSearch={false}
/>
<span></span>
</div>
<Pagination links={receipts.links} />
</div>
</div>
</AuthenticatedLayout>
);
}