Files
star-erp/resources/js/Pages/ShippingOrder/Show.tsx

237 lines
14 KiB
TypeScript
Raw Normal View History

import { ArrowLeft, Package, Info, CheckCircle2, AlertCircle, Trash2, Edit } from "lucide-react";
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, router } from "@inertiajs/react";
import { StatusBadge } from "@/Components/shared/StatusBadge";
import { toast } from "sonner";
import ActivityLog from "@/Components/ActivityLog/ActivityLog";
interface Props {
order: any;
activities: any[];
}
export default function ShippingOrderShow({ order, activities = [] }: Props) {
const isDraft = order.status === 'draft';
const isCompleted = order.status === 'completed';
const handlePost = () => {
if (confirm('確定要執行過帳嗎?這將會從倉庫中扣除庫存數量。')) {
router.post(route('delivery-notes.post', order.id), {}, {
onSuccess: () => toast.success('過帳成功'),
onError: (errors: any) => toast.error(errors.error || '過帳失敗')
});
}
};
const handleDelete = () => {
if (confirm('確定要刪除這張出貨單嗎?')) {
router.delete(route('delivery-notes.destroy', order.id));
}
};
const getStatusBadge = (status: string) => {
const statusConfig: Record<string, { variant: "neutral" | "success" | "destructive", label: string }> = {
draft: { variant: 'neutral', label: '草稿' },
completed: { variant: 'success', label: '已完成' },
cancelled: { variant: 'destructive', label: '已取消' },
};
const config = statusConfig[status];
if (!config) return <StatusBadge variant="neutral">{status}</StatusBadge>;
return <StatusBadge variant={config.variant}>{config.label}</StatusBadge>;
};
return (
<AuthenticatedLayout breadcrumbs={[
{ label: '供應鏈管理', href: '#' },
{ label: '出貨單管理', href: route('delivery-notes.index') },
{ label: `出貨單詳情 (${order.doc_no})`, isPage: true }
] as any}>
<Head title={`出貨單詳情 - ${order.doc_no}`} />
<div className="container mx-auto p-6 max-w-7xl">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<div>
<Link href={route('delivery-notes.index')}>
<Button variant="outline" size="sm" className="gap-2 mb-4">
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{order.doc_no}</h1>
{getStatusBadge(order.status)}
</div>
<p className="text-gray-500 mt-1">
: {new Date(order.created_at).toLocaleString()} | : {order.creator_name}
</p>
</div>
<div className="flex items-center gap-2">
{isDraft && (
<>
<Button variant="outline" className="gap-2" asChild>
<Link href={route('delivery-notes.edit', order.id)}>
<Edit className="h-4 w-4" />
</Link>
</Button>
<Button variant="destructive" className="gap-2" onClick={handleDelete}>
<Trash2 className="h-4 w-4" />
</Button>
<Button className="button-filled-primary gap-2" onClick={handlePost}>
<CheckCircle2 className="h-4 w-4" />
</Button>
</>
)}
{isCompleted && (
<div className="flex items-center gap-2 text-green-600 font-medium bg-green-50 px-4 py-2 rounded-lg border border-green-200">
<CheckCircle2 className="h-5 w-5" />
{new Date(order.posted_at).toLocaleString()}
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* 基本資訊 */}
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-lg font-bold mb-6 flex items-center gap-2 border-b pb-4">
<Info className="h-5 w-5 text-primary-main" />
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-6">
<div>
<label className="text-sm text-gray-500 block"></label>
<div className="font-medium text-lg">{order.warehouse_name}</div>
</div>
<div>
<label className="text-sm text-gray-500 block"></label>
<div className="font-medium text-lg">{order.shipping_date}</div>
</div>
<div>
<label className="text-sm text-gray-500 block"></label>
<div className="font-medium text-lg">{order.customer_name || '-'}</div>
</div>
<div>
<label className="text-sm text-gray-500 block"></label>
<div className="font-medium text-lg">{isCompleted ? '已完成 (已扣庫存)' : '草稿 (暫存中)'}</div>
</div>
<div className="md:col-span-2">
<label className="text-sm text-gray-500 block"></label>
<div className="text-gray-700 mt-1 bg-gray-50 p-3 rounded">{order.remarks || '無備註'}</div>
</div>
</div>
</div>
{/* 品項明細 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="p-6 border-b border-gray-100 flex items-center justify-between">
<h2 className="text-lg font-bold flex items-center gap-2">
<Package className="h-5 w-5 text-primary-main" />
</h2>
<StatusBadge variant="neutral">{order.items.length} </StatusBadge>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 text-gray-600">
<th className="px-6 py-4 text-left font-semibold"> / </th>
<th className="px-6 py-4 text-left font-semibold"></th>
<th className="px-6 py-4 text-right font-semibold"></th>
<th className="px-6 py-4 text-right font-semibold"></th>
<th className="px-6 py-4 text-right font-semibold"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{order.items.map((item: any) => (
<tr key={item.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{item.product_name}</div>
<div className="text-xs text-gray-500 font-mono">{item.product_code}</div>
</td>
<td className="px-6 py-4">
<StatusBadge variant="neutral" className="font-mono">{item.batch_number || 'N/A'}</StatusBadge>
</td>
<td className="px-6 py-4 text-right">
<span className="font-medium text-gray-900">{parseFloat(item.quantity).toLocaleString()}</span>
<span className="text-xs text-gray-500 ml-1">{item.unit_name}</span>
</td>
<td className="px-6 py-4 text-right text-gray-600">
${parseFloat(item.unit_price).toLocaleString()}
</td>
<td className="px-6 py-4 text-right font-bold text-gray-900">
${parseFloat(item.subtotal).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 活動日誌區塊 */}
<div className="mt-8">
<ActivityLog
activities={activities}
/>
</div>
</div>
{/* 右側:金額摘要 */}
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 sticky top-6">
<h2 className="text-lg font-bold mb-6 flex items-center gap-2 border-b pb-4">
<CalculatorIcon className="h-5 w-5 text-primary-main" />
</h2>
<div className="space-y-4">
<div className="flex justify-between items-center py-2 border-b border-dashed">
<span className="text-gray-500"></span>
<span className="font-medium">${Number(order.total_amount).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-dashed">
<span className="text-gray-500"> (5%)</span>
<span className="font-medium">${Number(order.tax_amount).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center py-4">
<span className="font-bold text-lg text-gray-900"></span>
<span className="font-black text-2xl text-primary-main">
${Number(order.grand_total).toLocaleString()}
</span>
</div>
{isDraft && (
<div className="mt-6 p-4 bg-amber-50 rounded-lg border border-amber-200 text-sm text-amber-800 flex gap-3">
<AlertCircle className="h-5 w-5 shrink-0" />
<div>
<p className="font-bold mb-1"></p>
<p>稿</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
function CalculatorIcon({ className }: { className?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
<rect width="16" height="20" x="4" y="2" rx="2" />
<line x1="8" x2="16" y1="6" y2="6" />
<line x1="16" x2="16" y1="14" y2="18" />
<path d="M16 10h.01" />
<path d="M12 10h.01" />
<path d="M8 10h.01" />
<path d="M12 14h.01" />
<path d="M8 14h.01" />
<path d="M12 18h.01" />
<path d="M8 18h.01" />
</svg>
);
}