Files
star-erp/resources/js/Pages/PurchaseOrder/Index.tsx
sky121113 7367577f6a
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 59s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 統一採購單與操作紀錄 UI、增強各模組操作紀錄功能
- 統一採購單篩選列與表單樣式 (移除舊元件、標準化 Input)
- 增強操作紀錄功能 (加入篩選、快照、詳細異動比對)
- 統一刪除確認視窗與按鈕樣式
- 修復庫存編輯頁面樣式
- 實作採購單品項異動紀錄
- 實作角色分配異動紀錄
- 擴充供應商與倉庫模組紀錄
2026-01-19 17:07:45 +08:00

267 lines
12 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, 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 { 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";
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");
// 同步 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("");
router.get(route('purchase-orders.index'));
};
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 p-5 rounded-lg shadow-sm border border-gray-200 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
{/* 關鍵字搜尋 */}
<div className="space-y-1">
<Label className="text-xs text-gray-500"></Label>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜尋採購單號、廠商..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
</div>
</div>
{/* 狀態篩選 */}
<div className="space-y-1">
<Label className="text-xs text-gray-500"></Label>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger>
<SelectValue placeholder="選擇狀態" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="draft">稿</SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="processing"></SelectItem>
<SelectItem value="shipping"></SelectItem>
<SelectItem value="confirming"></SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="cancelled"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 倉庫篩選 */}
<div className="space-y-1">
<Label className="text-xs text-gray-500"></Label>
<SearchableSelect
value={warehouseId}
onValueChange={setWarehouseId}
options={[
{ label: "全部倉庫", value: "all" },
...warehouses.map(w => ({ label: w.name, value: String(w.id) }))
]}
placeholder="選擇倉庫"
className="w-full"
/>
</div>
{/* 日期範圍 - 開始 */}
<div className="space-y-1">
<Label className="text-xs text-gray-500"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400" />
<Input
type="date"
value={dateStart}
onChange={(e) => setDateStart(e.target.value)}
className="pl-9 block w-full"
/>
</div>
</div>
{/* 日期範圍 - 結束 */}
<div className="space-y-1">
<Label className="text-xs text-gray-500"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400" />
<Input
type="date"
value={dateEnd}
onChange={(e) => setDateEnd(e.target.value)}
className="pl-9 block w-full text-left"
/>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-2 border-t border-gray-100">
<Button
variant="outline"
onClick={handleReset}
className="flex items-center gap-2 button-outlined-primary"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
onClick={handleFilter}
className="flex items-center gap-2 button-filled-primary"
>
<Search className="h-4 w-4" />
</Button>
</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>
);
}