Files
star-erp/resources/js/Pages/Production/Show.tsx
sky121113 1d134c9ad8
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 57s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
生產工單BOM以及批號完善
2026-01-22 15:39:35 +08:00

259 lines
14 KiB
TypeScript

/**
* 生產工單詳情頁面
* 含追溯資訊:成品批號 → 原物料批號 → 來源採購單
*/
import { Factory, ArrowLeft, Package, Calendar, User, Warehouse, FileText, Link2 } from 'lucide-react';
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link } from "@inertiajs/react";
import { getBreadcrumbs } from "@/utils/breadcrumb";
import { Badge } from "@/Components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
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;
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;
warehouse: { id: number; name: string } | 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: 'draft' | 'completed' | 'cancelled';
remark: string | null;
created_at: string;
items: ProductionOrderItem[];
}
interface Props {
productionOrder: ProductionOrder;
}
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 ProductionShow({ productionOrder }: Props) {
return (
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("productionOrdersShow")}>
<Head title={`生產單 ${productionOrder.code}`} />
<div className="container mx-auto p-6 max-w-7xl">
<div className="mb-6">
<Link href={route('production-orders.index')}>
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center justify-between">
<div>
<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>
<p className="text-gray-500 mt-1">
</p>
</div>
<Badge variant={statusConfig[productionOrder.status]?.variant || "secondary"} className="text-sm">
{statusConfig[productionOrder.status]?.label || productionOrder.status}
</Badge>
</div>
</div>
{/* 成品資訊 */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 mb-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Package className="h-5 w-5 text-gray-500" />
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-6">
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<p className="font-medium text-grey-0">
{productionOrder.product?.name || '-'}
<span className="text-gray-400 ml-2 text-sm font-normal">
({productionOrder.product?.code || '-'})
</span>
</p>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<p className="font-mono font-medium text-primary-main">
{productionOrder.output_batch_number}
</p>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<p className="font-medium text-grey-0">
{productionOrder.output_quantity.toLocaleString()}
{productionOrder.product?.base_unit?.name && (
<span className="text-gray-400 ml-1 font-normal">{productionOrder.product.base_unit.name}</span>
)}
{productionOrder.output_box_count && (
<span className="text-gray-400 ml-2 font-normal">({productionOrder.output_box_count} )</span>
)}
</p>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<div className="flex items-center gap-2">
<Warehouse className="h-4 w-4 text-gray-400" />
<p className="font-medium text-grey-0">{productionOrder.warehouse?.name || '-'}</p>
</div>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-400" />
<p className="font-medium text-grey-0">{productionOrder.production_date}</p>
</div>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-400" />
<p className="font-medium text-grey-0">{productionOrder.expiry_date || '-'}</p>
</div>
</div>
<div className="space-y-1">
<p className="text-xs font-medium text-grey-2"></p>
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-gray-400" />
<p className="font-medium text-grey-0">{productionOrder.user?.name || '-'}</p>
</div>
</div>
</div>
{productionOrder.remark && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex items-start gap-2">
<FileText className="h-4 w-4 text-gray-400 mt-1" />
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-gray-700">{productionOrder.remark}</p>
</div>
</div>
</div>
)}
</div>
{/* 原物料使用明細 (BOM) */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Link2 className="h-5 w-5 text-gray-500" />
使 (BOM) -
</h2>
{productionOrder.items.length === 0 ? (
<p className="text-center text-gray-500 py-8"></p>
) : (
<div className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="px-4 py-3 text-left text-xs font-medium text-grey-2">
</TableHead>
<TableHead className="px-4 py-3 text-left text-xs font-medium text-grey-2">
</TableHead>
<TableHead className="px-4 py-3 text-left text-xs font-medium text-grey-2">
</TableHead>
<TableHead className="px-4 py-3 text-left text-xs font-medium text-grey-2">
</TableHead>
<TableHead className="px-4 py-3 text-left text-xs font-medium text-grey-2">
使
</TableHead>
<TableHead className="px-4 py-3 text-left text-xs font-medium text-grey-2">
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{productionOrder.items.map((item) => (
<TableRow key={item.id} className="hover:bg-gray-50/50">
<TableCell className="px-4 py-4 text-sm">
<div className="font-medium text-grey-0">{item.inventory?.product?.name || '-'}</div>
<div className="text-gray-400 text-xs">
{item.inventory?.product?.code || '-'}
</div>
</TableCell>
<TableCell className="px-4 py-4 text-sm font-mono text-primary-main">
{item.inventory?.batch_number || '-'}
{item.inventory?.box_number && (
<span className="text-gray-300 ml-1">#{item.inventory.box_number}</span>
)}
</TableCell>
<TableCell className="px-4 py-4 text-sm text-grey-1">
{item.inventory?.origin_country || '-'}
</TableCell>
<TableCell className="px-4 py-4 text-sm text-grey-1">
{item.inventory?.arrival_date || '-'}
</TableCell>
<TableCell className="px-4 py-4 text-sm font-medium text-grey-0">
{item.quantity_used.toLocaleString()}
{item.unit?.name && (
<span className="text-gray-400 ml-1 font-normal text-xs">{item.unit.name}</span>
)}
</TableCell>
<TableCell className="px-4 py-4 text-sm">
{item.inventory?.source_purchase_order ? (
<div className="flex flex-col">
<Link
href={route('purchase-orders.show', item.inventory.source_purchase_order.id)}
className="text-primary-main hover:underline font-medium"
>
{item.inventory.source_purchase_order.code}
</Link>
{item.inventory.source_purchase_order.vendor && (
<span className="text-[11px] text-gray-400 mt-0.5">
{item.inventory.source_purchase_order.vendor.name}
</span>
)}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
</div>
</AuthenticatedLayout>
);
}