Files
star-erp/resources/js/Pages/Production/Index.tsx
sky121113 bb78a432f5
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m1s
fix(product): 修復條碼掃描自動送出問題並優化手動輸入體驗
2026-02-02 09:06:06 +08:00

324 lines
16 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, Factory, Search, Eye, Pencil, Trash2 } from 'lucide-react';
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, router, Link } from "@inertiajs/react";
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Badge } from "@/Components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
interface ProductionOrder {
id: number;
code: string;
product: { id: number; name: string; code: string } | null;
warehouse: { id: number; name: string } | null;
user: { id: number; name: string } | null;
output_batch_number: string;
output_quantity: number;
production_date: string;
status: 'draft' | 'completed' | 'cancelled';
created_at: string;
}
interface Props {
productionOrders: {
data: ProductionOrder[];
links: any[];
total: number;
from: number;
to: number;
};
filters: {
search?: string;
status?: string;
per_page?: string;
};
}
const statusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
draft: { label: "草稿", variant: "secondary" },
completed: { label: "已完成", variant: "default" },
cancelled: { label: "已取消", variant: "destructive" },
};
export default function ProductionIndex({ productionOrders, filters }: Props) {
const [search, setSearch] = useState(filters.search || "");
const [status, setStatus] = useState<string>(filters.status || "all");
const [perPage, setPerPage] = useState<string>(filters.per_page || "10");
useEffect(() => {
setSearch(filters.search || "");
setStatus(filters.status || "all");
setPerPage(filters.per_page || "10");
}, [filters]);
const handleFilter = () => {
router.get(
route('production-orders.index'),
{
search,
status: status === 'all' ? undefined : status,
per_page: perPage,
},
{ preserveState: true, replace: true }
);
};
const handlePerPageChange = (value: string) => {
setPerPage(value);
router.get(
route("production-orders.index"),
{ ...filters, per_page: value },
{ preserveState: false, replace: true, preserveScroll: true }
);
};
const handleNavigateToCreate = () => {
router.get(route('production-orders.create'));
};
return (
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrders")}>
<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">
<Factory className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
使
</p>
</div>
</div>
{/* Toolbar */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<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={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 pr-10 h-9"
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
{search && (
<button
onClick={() => {
setSearch("");
router.get(route('production-orders.index'), { ...filters, search: "" }, { preserveState: true, replace: true });
}}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<Trash2 className="h-4 w-4" />
</button>
)}
</div>
{/* Status Filter */}
<Select
value={status}
onValueChange={(val) => {
setStatus(val);
router.get(
route('production-orders.index'),
{ ...filters, status: val === 'all' ? undefined : val },
{ preserveState: true, replace: true }
);
}}
>
<SelectTrigger className="w-full md:w-[150px] h-9 text-sm">
<SelectValue placeholder="選擇狀態" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="draft">稿</SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="cancelled"></SelectItem>
</SelectContent>
</Select>
{/* Action Buttons */}
<div className="flex gap-2 w-full md:w-auto">
<Button
variant="outline"
className="button-outlined-primary"
onClick={handleFilter}
>
<Search className="w-4 h-4 mr-2" />
</Button>
<Can permission="production_orders.create">
<Button
onClick={handleNavigateToCreate}
className="gap-2 button-filled-primary"
>
<Plus className="h-4 w-4" />
</Button>
</Can>
</div>
</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-[150px]"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center w-[100px]"></TableHead>
<TableHead className="text-center w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{productionOrders.data.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center text-gray-500">
<div className="flex flex-col items-center justify-center gap-2">
<Factory className="h-10 w-10 text-gray-300" />
<p></p>
</div>
</TableCell>
</TableRow>
) : (
productionOrders.data.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium text-gray-900">
{order.code}
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="font-medium text-gray-900">{order.product?.name || '-'}</span>
<span className="text-gray-400 text-xs">
{order.product?.code || '-'}
</span>
</div>
</TableCell>
<TableCell>
<code className="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">
{order.output_batch_number}
</code>
</TableCell>
<TableCell className="text-right font-medium">
{order.output_quantity.toLocaleString()}
</TableCell>
<TableCell className="text-gray-600">
{order.warehouse?.name || '-'}
</TableCell>
<TableCell className="text-gray-600">
{order.production_date}
</TableCell>
<TableCell className="text-center">
<Badge variant={statusConfig[order.status]?.variant || "secondary"} className="font-normal capitalize">
{statusConfig[order.status]?.label || order.status}
</Badge>
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-2">
{order.status === 'draft' && (
<Can permission="production_orders.edit">
<Link href={route('production-orders.edit', order.id)}>
<Button
variant="outline"
size="sm"
className="button-outlined-primary"
title="編輯"
>
<Pencil className="h-4 w-4" />
</Button>
</Link>
</Can>
)}
<Can permission="production_orders.view">
<Link href={route('production-orders.show', order.id)}>
<Button
variant="outline"
size="sm"
className="button-outlined-primary"
title="檢視"
>
<Eye className="h-4 w-4" />
</Button>
</Link>
</Can>
<Can permission="production_orders.delete">
<Button
variant="outline"
size="sm"
className="button-outlined-error"
title="刪除"
onClick={() => {
if (confirm('確定要刪除此生產工單嗎?')) {
router.delete(route('production-orders.destroy', order.id));
}
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</Can>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 分頁 */}
<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={productionOrders.links} />
</div>
</div>
</div>
</AuthenticatedLayout >
);
}