Files
star-erp/resources/js/Pages/Production/Show.tsx
sky121113 4fa87925a2
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m8s
UI優化: 全系統狀態標籤 (StatusBadge) 統一化重構完成 (Phase 3 & 4)
2026-02-13 13:16:05 +08:00

437 lines
26 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 { Factory, ArrowLeft, Package, Calendar, User, Warehouse, FileText, Link2, Send, CheckCircle2, PlayCircle, Ban, ArrowRightCircle } from 'lucide-react';
import { formatQuantity } from "@/lib/utils";
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, useForm, router } from "@inertiajs/react";
import { getBreadcrumbs } from "@/utils/breadcrumb";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
import { StatusBadge } from "@/Components/shared/StatusBadge";
import ProductionOrderStatusBadge from '@/Components/ProductionOrder/ProductionOrderStatusBadge';
import { ProductionStatusProgressBar } from '@/Components/ProductionOrder/ProductionStatusProgressBar';
import { PRODUCTION_ORDER_STATUS, ProductionOrderStatus } from '@/constants/production-order';
import WarehouseSelectionModal from '@/Components/ProductionOrder/WarehouseSelectionModal';
import { useState } from 'react';
import { formatDate } from '@/lib/date';
interface Warehouse {
id: number;
name: string;
}
interface ProductionOrderItem {
// ... (後面保持不變)
id: number;
quantity_used: number;
unit?: { id: number; name: string } | null;
inventory: {
id: number;
batch_number: string;
box_number: string | null;
arrival_date: string | null;
origin_country: string | null;
product: { id: number; name: string; code: string } | null;
warehouse?: { id: number; name: string } | null;
source_purchase_order?: {
id: number;
code: string;
vendor?: { id: number; name: string } | null;
} | null;
} | null;
}
interface ProductionOrder {
id: number;
code: string;
product: { id: number; name: string; code: string; base_unit?: { name: string } | null } | null;
product_id: number;
warehouse: { id: number; name: string } | null;
warehouse_id: number | null;
user: { id: number; name: string } | null;
output_batch_number: string;
output_box_count: string | null;
output_quantity: number;
production_date: string;
expiry_date: string | null;
status: ProductionOrderStatus;
remark: string | null;
created_at: string;
items: ProductionOrderItem[];
}
interface Props {
productionOrder: ProductionOrder;
warehouses: Warehouse[];
auth: {
user: {
id: number;
name: string;
roles: string[];
permissions: string[];
} | null;
};
}
export default function ProductionShow({ productionOrder, warehouses, auth }: Props) {
const [isWarehouseModalOpen, setIsWarehouseModalOpen] = useState(false);
const { processing } = useForm({
status: '' as ProductionOrderStatus,
warehouse_id: null as number | null,
});
const handleStatusUpdate = (newStatus: string, extraData?: {
warehouseId?: number;
batchNumber?: string;
expiryDate?: string;
}) => {
router.patch(route('production-orders.update-status', productionOrder.id), {
status: newStatus,
warehouse_id: extraData?.warehouseId,
output_batch_number: extraData?.batchNumber,
expiry_date: extraData?.expiryDate,
}, {
onSuccess: () => {
setIsWarehouseModalOpen(false);
},
preserveScroll: true,
});
};
const userPermissions = auth.user?.permissions || [];
const hasPermission = (permission: string) => auth.user?.roles?.includes('super-admin') || userPermissions.includes(permission);
// 權限判斷
const canApprove = hasPermission('production_orders.approve');
const canCancel = hasPermission('production_orders.cancel');
const canEdit = hasPermission('production_orders.edit');
return (
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersShow")}>
<Head title={`生產單 ${productionOrder.code}`} />
<WarehouseSelectionModal
isOpen={isWarehouseModalOpen}
onClose={() => setIsWarehouseModalOpen(false)}
onConfirm={(data) => handleStatusUpdate(PRODUCTION_ORDER_STATUS.COMPLETED, data)}
warehouses={warehouses}
processing={processing}
productCode={productionOrder.product?.code}
productId={productionOrder.product?.id}
/>
<div className="container mx-auto p-6 max-w-7xl animate-in fade-in duration-500">
{/* Header 區塊 */}
<div className="mb-6">
{/* 返回按鈕 (統一規範標題上方mb-4) */}
<Link href={route('production-orders.index')}>
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-4"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Factory className="h-6 w-6 text-primary-main" />
{productionOrder.code}
</h1>
<ProductionOrderStatusBadge status={productionOrder.status} />
</div>
<p className="text-gray-500 text-sm mt-1">
{productionOrder.user?.name || '-'} | {formatDate(productionOrder.created_at)}
</p>
</div>
{/* 操作按鈕區 (統一規範樣式類別) */}
<div className="flex items-center gap-2">
{/* 草稿 -> 提交審核 */}
{productionOrder.status === PRODUCTION_ORDER_STATUS.DRAFT && (
<>
{canEdit && (
<Link href={route('production-orders.edit', productionOrder.id)}>
<Button variant="outline" className="gap-2 button-outlined-primary">
<FileText className="h-4 w-4" />
</Button>
</Link>
)}
<Button
onClick={() => handleStatusUpdate(PRODUCTION_ORDER_STATUS.PENDING)}
className="gap-2 button-filled-primary"
>
<Send className="h-4 w-4" />
</Button>
</>
)}
{/* 待審核 -> 核准 / 駁回 */}
{productionOrder.status === PRODUCTION_ORDER_STATUS.PENDING && canApprove && (
<>
<Button
variant="outline"
onClick={() => handleStatusUpdate(PRODUCTION_ORDER_STATUS.DRAFT)}
className="gap-2 button-outlined-error"
>
<ArrowLeft className="h-4 w-4" />
退稿
</Button>
<Button
onClick={() => handleStatusUpdate(PRODUCTION_ORDER_STATUS.APPROVED)}
className="gap-2 button-filled-success"
>
<CheckCircle2 className="h-4 w-4" />
</Button>
</>
)}
{/* 已核准 -> 開始製作 */}
{productionOrder.status === PRODUCTION_ORDER_STATUS.APPROVED && (
<Button
onClick={() => handleStatusUpdate(PRODUCTION_ORDER_STATUS.IN_PROGRESS)}
className="gap-2 button-filled-primary"
>
<PlayCircle className="h-4 w-4" />
()
</Button>
)}
{/* 製作中 -> 完成製作 */}
{productionOrder.status === PRODUCTION_ORDER_STATUS.IN_PROGRESS && (
<Button
onClick={() => setIsWarehouseModalOpen(true)}
className="gap-2 button-filled-primary"
>
<ArrowRightCircle className="h-4 w-4" />
()
</Button>
)}
{/* 可作廢狀態 (非已完成/已作廢/草稿之外) */}
{!([PRODUCTION_ORDER_STATUS.COMPLETED, PRODUCTION_ORDER_STATUS.CANCELLED, PRODUCTION_ORDER_STATUS.DRAFT] as ProductionOrderStatus[]).includes(productionOrder.status) && canCancel && (
<Button
variant="outline"
onClick={() => {
if (confirm('確定要作廢此生產工單嗎?此動作無法復原。')) {
handleStatusUpdate(PRODUCTION_ORDER_STATUS.CANCELLED);
}
}}
className="gap-2 button-outlined-error"
>
<Ban className="h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
{/* 狀態進度條 */}
<div className="lg:col-span-3">
<ProductionStatusProgressBar currentStatus={productionOrder.status} />
</div>
{/* 成品資訊 (統一規範bg-white rounded-xl border border-gray-200 shadow-sm p-6) */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 h-full">
<h2 className="text-lg font-semibold mb-6 flex items-center gap-2 text-grey-0">
<Package className="h-5 w-5 text-primary-main" />
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-8">
<div className="space-y-1.5">
<p className="text-xs font-semibold text-grey-2 uppercase tracking-wider"></p>
<div>
<p className="font-bold text-grey-0 text-lg">
{productionOrder.product?.name || '-'}
</p>
<p className="text-gray-400 text-sm font-mono mt-0.5">
{productionOrder.product?.code || '-'}
</p>
</div>
</div>
<div className="space-y-1.5">
<p className="text-xs font-semibold text-grey-2 uppercase tracking-wider"></p>
<p className="font-mono font-bold text-primary-main text-lg py-1 px-2 bg-primary-lightest rounded-md inline-block">
{productionOrder.output_batch_number}
</p>
</div>
<div className="space-y-1.5">
<p className="text-xs font-semibold text-grey-2 uppercase tracking-wider">/</p>
<div className="flex items-baseline gap-1.5">
<p className="font-bold text-grey-0 text-xl">
{formatQuantity(productionOrder.output_quantity)}
</p>
{productionOrder.product?.base_unit?.name && (
<span className="text-grey-2 font-medium">{productionOrder.product.base_unit.name}</span>
)}
{productionOrder.output_box_count && (
<span className="text-grey-3 ml-2 text-sm">({productionOrder.output_box_count} )</span>
)}
</div>
</div>
<div className="space-y-1.5">
<p className="text-xs font-semibold text-grey-2 uppercase tracking-wider"></p>
<div className="flex items-center gap-2 bg-grey-5 p-2 rounded-lg border border-grey-4">
<Warehouse className="h-4 w-4 text-grey-3" />
<p className="font-semibold text-grey-0">{productionOrder.warehouse?.name || (productionOrder.status === PRODUCTION_ORDER_STATUS.COMPLETED ? '系統錯誤' : '待選取')}</p>
</div>
</div>
</div>
{productionOrder.remark && (
<div className="mt-8 pt-6 border-t border-grey-4 transition-all hover:bg-grey-5 p-2 rounded-lg">
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 text-grey-3 mt-0.5" />
<div>
<p className="text-xs font-semibold text-grey-2 uppercase tracking-wider mb-1"></p>
<p className="text-grey-1 leading-relaxed">{productionOrder.remark}</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* 次要資訊 */}
<div className="lg:col-span-1">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 h-full space-y-8">
<h2 className="text-lg font-semibold flex items-center gap-2 text-grey-0">
<Calendar className="h-5 w-5 text-primary-main" />
</h2>
<div className="space-y-6">
<div className="flex items-start gap-4 p-3 rounded-lg bg-primary-lightest border border-primary-light/20">
<Calendar className="h-5 w-5 text-primary-main mt-1" />
<div>
<p className="text-xs font-bold text-primary-main/60 uppercase"></p>
<p className="font-bold text-grey-0 text-lg">{formatDate(productionOrder.production_date)}</p>
</div>
</div>
<div className="flex items-start gap-4 p-3 rounded-lg bg-orange-50 border border-orange-100">
<Calendar className="h-5 w-5 text-orange-600 mt-1" />
<div>
<p className="text-xs font-bold text-orange-900/50 uppercase"></p>
<p className="font-bold text-orange-900 text-lg">{formatDate(productionOrder.expiry_date)}</p>
</div>
</div>
<div className="flex items-start gap-4 p-3 rounded-lg bg-grey-5 border border-grey-4">
<User className="h-5 w-5 text-grey-2 mt-1" />
<div>
<p className="text-xs font-bold text-grey-3 uppercase"></p>
<p className="font-bold text-grey-1 text-lg">{productionOrder.user?.name || '-'}</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 原物料使用明細 (BOM) (統一規範bg-white rounded-xl border border-gray-200 shadow-sm p-6) */}
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold flex items-center gap-2 text-grey-0">
<Link2 className="h-5 w-5 text-primary-main" />
</h2>
<StatusBadge variant="neutral" className="text-grey-3 font-medium">
{productionOrder.items.length}
</StatusBadge>
</div>
{productionOrder.items.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-grey-3 bg-grey-5 rounded-xl border-2 border-dashed border-grey-4">
<Package className="h-10 w-10 mb-4 opacity-20 text-grey-2" />
<p></p>
</div>
) : (
<div className="rounded-xl border border-grey-4 overflow-hidden shadow-sm">
<Table>
<TableHeader className="bg-grey-5/80 backdrop-blur-sm transition-colors">
<TableRow className="hover:bg-transparent border-b-grey-4">
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none"></TableHead>
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none"></TableHead>
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">使</TableHead>
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none text-center"></TableHead>
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none">使</TableHead>
<TableHead className="px-6 py-4 text-xs font-bold text-grey-2 uppercase tracking-widest leading-none"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{productionOrder.items.map((item) => (
<TableRow key={item.id} className="hover:bg-grey-5/80 transition-colors border-b-grey-4 last:border-0">
<TableCell className="px-6 py-5">
<div className="font-bold text-grey-0">{item.inventory?.product?.name || '-'}</div>
<div className="text-grey-3 text-xs font-mono mt-1 px-1.5 py-0.5 bg-grey-5 border border-grey-4 rounded inline-block">
{item.inventory?.product?.code || '-'}
</div>
</TableCell>
<TableCell className="px-6 py-5">
<div className="text-grey-0 font-medium">{item.inventory?.warehouse?.name || '-'}</div>
</TableCell>
<TableCell className="px-6 py-5">
<div className="font-mono font-bold text-primary-main bg-primary-lightest border border-primary-light/10 px-2 py-1 rounded inline-flex items-center gap-2">
{item.inventory?.batch_number || '-'}
{item.inventory?.box_number && (
<span className="text-primary-main/60 text-[10px] bg-white px-1 rounded shadow-sm">#{item.inventory.box_number}</span>
)}
</div>
</TableCell>
<TableCell className="px-6 py-5 text-center">
<span className="px-3 py-1 bg-grey-5 border border-grey-4 rounded-full text-xs font-bold text-grey-2">
{item.inventory?.origin_country || '-'}
</span>
</TableCell>
<TableCell className="px-6 py-5">
<div className="flex items-baseline gap-1">
<span className="font-bold text-grey-0 text-base">{formatQuantity(item.quantity_used)}</span>
{item.unit?.name && (
<span className="text-grey-3 text-xs font-medium uppercase">{item.unit.name}</span>
)}
</div>
</TableCell>
<TableCell className="px-6 py-5">
{item.inventory?.source_purchase_order ? (
<div className="group flex flex-col">
<Link
href={route('purchase-orders.show', item.inventory.source_purchase_order.id)}
className="text-primary-main hover:text-primary-dark font-bold inline-flex items-center gap-1 group-hover:underline transition-all"
>
{item.inventory.source_purchase_order.code}
<ArrowLeft className="h-3 w-3 rotate-180 opacity-0 group-hover:opacity-100 transition-opacity" />
</Link>
{item.inventory.source_purchase_order.vendor && (
<span className="text-[10px] text-grey-3 font-bold uppercase tracking-tight mt-0.5 whitespace-nowrap overflow-hidden text-ellipsis max-w-[150px]">
{item.inventory.source_purchase_order.vendor.name}
</span>
)}
</div>
) : (
<span className="text-grey-4"></span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
</div>
</AuthenticatedLayout>
);
}