Files
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

323 lines
15 KiB
TypeScript
Raw Permalink 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, useEffect } from "react";
import { Plus, ShoppingCart, Search, RotateCcw, Calendar } from 'lucide-react';
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, router } from "@inertiajs/react";
import PurchaseOrderTable from "@/Components/PurchaseOrder/PurchaseOrderTable";
import type { PurchaseOrder } from "@/types/purchase-order";
import Pagination from "@/Components/shared/Pagination";
import { getBreadcrumbs } from "@/utils/breadcrumb";
import { getDateRange } from "@/utils/format";
import { Can } from "@/Components/Permission/Can";
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { MANUAL_STATUS_OPTIONS } from "@/constants/purchase-order";
interface Props {
orders: {
data: PurchaseOrder[];
links: any[];
total: number;
from: number;
to: number;
};
filters: {
search?: string;
status?: string;
warehouse_id?: string;
date_start?: string;
date_end?: string;
sort_field?: string;
sort_direction?: string;
per_page?: string;
};
warehouses: { id: number; name: string }[];
}
export default function PurchaseOrderIndex({ orders, filters, warehouses }: Props) {
// 篩選狀態
const [search, setSearch] = useState(filters.search || "");
const [status, setStatus] = useState<string>(filters.status || "all");
const [warehouseId, setWarehouseId] = useState<string>(filters.warehouse_id || "all");
const [dateStart, setDateStart] = useState(filters.date_start || "");
const [dateEnd, setDateEnd] = useState(filters.date_end || "");
const [perPage, setPerPage] = useState<string>(filters.per_page || "10");
const [dateRangeType, setDateRangeType] = useState('custom');
// Advanced Filter Toggle
// 同步 URL 參數到 State (雖有初始值,但若由外部連結進入可確保同步)
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('purchase-orders.index'),
{
search,
status: status === 'all' ? undefined : status,
warehouse_id: warehouseId === 'all' ? undefined : warehouseId,
date_start: dateStart,
date_end: dateEnd,
per_page: perPage,
sort_field: filters.sort_field,
sort_direction: filters.sort_direction,
},
{ preserveState: true, replace: true }
);
};
const handleReset = () => {
setSearch("");
setStatus("all");
setWarehouseId("all");
setDateStart("");
setDateEnd("");
setDateRangeType("custom");
router.get(route('purchase-orders.index'));
};
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("purchase-orders.index"),
{
...filters,
per_page: value,
},
{ preserveState: false, replace: true, preserveScroll: true }
);
};
const handleNavigateToCreateOrder = () => {
router.get(route('purchase-orders.create'));
};
return (
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("purchaseOrders")}>
<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">
<ShoppingCart className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
<div className="flex gap-2">
<Can permission="purchase_orders.create">
<Button
onClick={handleNavigateToCreateOrder}
className="gap-2 button-filled-primary"
>
<Plus className="h-4 w-4" />
</Button>
</Can>
</div>
</div>
{/* 篩選區塊 */}
{/* 篩選區塊 */}
<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>
<SelectItem value="all"></SelectItem>
{MANUAL_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.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={[
{ label: "全部倉庫", value: "all" },
...warehouses.map(w => ({ label: w.name, value: String(w.id) }))
]}
placeholder="選擇倉庫"
className="w-full h-9"
/>
</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>
<PurchaseOrderTable
orders={orders.data}
/>
{/* 分頁元件 - 統一樣式 */}
<div className="mt-4 flex flex-col sm:flex-row items-start sm: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>
<div className="w-full sm:w-auto flex justify-center sm:justify-end">
<Pagination links={orders.links} />
</div>
</div>
</div>
</AuthenticatedLayout>
);
}