279 lines
14 KiB
TypeScript
279 lines
14 KiB
TypeScript
/**
|
||
* 查看採購單詳情頁面
|
||
*/
|
||
|
||
import { ArrowLeft, ShoppingCart, Send, CheckCircle, XCircle, RotateCcw } from "lucide-react";
|
||
import { Button } from "@/Components/ui/button";
|
||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||
import { Head, Link, useForm, usePage, router } from "@inertiajs/react";
|
||
import { StatusProgressBar } from "@/Components/PurchaseOrder/StatusProgressBar";
|
||
import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderStatusBadge";
|
||
import CopyButton from "@/Components/shared/CopyButton";
|
||
import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable";
|
||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||
import { formatCurrency, formatDateTime } from "@/utils/format";
|
||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||
import { toast } from "sonner";
|
||
import { PageProps } from "@/types/global";
|
||
|
||
interface Props {
|
||
order: PurchaseOrder;
|
||
}
|
||
|
||
export default function ViewPurchaseOrderPage({ order }: Props) {
|
||
return (
|
||
<AuthenticatedLayout breadcrumbs={getShowBreadcrumbs("purchaseOrders", `詳情 (#${order.poNumber})`)}>
|
||
<Head title={`採購單詳情 - ${order.poNumber}`} />
|
||
<div className="container mx-auto p-6 max-w-7xl">
|
||
{/* Header */}
|
||
<div className="mb-6">
|
||
<Link href="/purchase-orders">
|
||
<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 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">單號:{order.poNumber}</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<PurchaseOrderStatusBadge status={order.status} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* 狀態流程條 */}
|
||
<div className="mb-8">
|
||
<StatusProgressBar currentStatus={order.status} />
|
||
</div>
|
||
|
||
<div className="space-y-8">
|
||
{/* 基本資訊與品項 */}
|
||
<div className="space-y-8">
|
||
|
||
|
||
{/* 基本資訊卡片 */}
|
||
<div className="bg-white rounded-lg border shadow-sm p-6">
|
||
<h2 className="text-lg font-bold text-gray-900 mb-6">基本資訊</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">採購單編號</span>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="font-mono font-medium text-gray-900">{order.poNumber}</span>
|
||
<CopyButton text={order.poNumber} label="複製單號" />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">供應商</span>
|
||
<span className="font-medium text-gray-900">{order.supplierName}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">申請單位 (申請人)</span>
|
||
<span className="font-medium text-gray-900">
|
||
{order.warehouse_name} ({order.createdBy})
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">建立日期</span>
|
||
<span className="font-medium text-gray-900">{formatDateTime(order.createdAt)}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">下單日期</span>
|
||
<span className="font-medium text-gray-900">{order.orderDate || "-"}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">預計到貨日期</span>
|
||
<span className="font-medium text-gray-900">{order.expectedDate || "-"}</span>
|
||
</div>
|
||
</div>
|
||
{order.remark && (
|
||
<div className="mt-8 pt-6 border-t border-gray-100">
|
||
<span className="text-sm text-gray-500 block mb-2">備註</span>
|
||
<p className="text-sm text-gray-700 bg-gray-50 p-4 rounded-lg leading-relaxed">
|
||
{order.remark}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 發票資訊卡片 */}
|
||
{(order.invoiceNumber || order.invoiceDate || (order.invoiceAmount !== null && order.invoiceAmount !== undefined)) && (
|
||
<div className="bg-white rounded-lg border shadow-sm p-6">
|
||
<h2 className="text-lg font-bold text-gray-900 mb-6">發票資訊</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-6">
|
||
{order.invoiceNumber && (
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">發票號碼</span>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="font-mono font-medium text-gray-900">{order.invoiceNumber}</span>
|
||
<CopyButton text={order.invoiceNumber} label="複製發票號碼" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
{order.invoiceDate && (
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">發票日期</span>
|
||
<span className="font-medium text-gray-900">{order.invoiceDate}</span>
|
||
</div>
|
||
)}
|
||
{order.invoiceAmount !== null && order.invoiceAmount !== undefined && (
|
||
<div>
|
||
<span className="text-sm text-gray-500 block mb-1">發票金額</span>
|
||
<span className="font-medium text-gray-900">{formatCurrency(order.invoiceAmount)}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 採購項目卡片 */}
|
||
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
|
||
<div className="p-6 border-b border-gray-100">
|
||
<h2 className="text-lg font-bold text-gray-900">採購項目清單</h2>
|
||
</div>
|
||
<div className="p-6">
|
||
<PurchaseOrderItemsTable
|
||
items={order.items}
|
||
isReadOnly={true}
|
||
/>
|
||
<div className="mt-6 flex justify-end">
|
||
<div className="w-full max-w-sm bg-primary/5 px-6 py-4 rounded-xl border border-primary/10 flex flex-col gap-3">
|
||
<div className="flex justify-between items-center w-full">
|
||
<span className="text-sm text-gray-500 font-medium">小計</span>
|
||
<span className="text-lg font-bold text-gray-700">{formatCurrency(order.totalAmount)}</span>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-center w-full">
|
||
<span className="text-sm text-gray-500 font-medium">稅額</span>
|
||
<span className="text-lg font-bold text-gray-700">{formatCurrency(order.taxAmount || 0)}</span>
|
||
</div>
|
||
|
||
<div className="h-px bg-primary/10 w-full my-1"></div>
|
||
|
||
<div className="flex justify-between items-end w-full">
|
||
<span className="text-sm text-gray-500 font-medium mb-1">總計 (含稅)</span>
|
||
<span className="text-2xl font-black text-primary">
|
||
{formatCurrency(order.grandTotal || (order.totalAmount + (order.taxAmount || 0)))}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按鈕 (底部) */}
|
||
<div className="flex justify-end pt-4">
|
||
<PurchaseOrderActions order={order} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</AuthenticatedLayout>
|
||
);
|
||
}
|
||
|
||
function PurchaseOrderActions({ order }: { order: PurchaseOrder }) {
|
||
const { auth } = usePage<PageProps>().props;
|
||
const permissions = auth.user?.permissions || [];
|
||
|
||
const { processing } = useForm({
|
||
status: order.status,
|
||
});
|
||
|
||
const handleUpdateStatus = (newStatus: string, actionName: string) => {
|
||
const formData = {
|
||
vendor_id: order.supplierId,
|
||
warehouse_id: order.warehouse_id,
|
||
order_date: order.orderDate,
|
||
expected_delivery_date: order.expectedDate ? new Date(order.expectedDate).toISOString().split('T')[0] : null,
|
||
items: order.items.map((item: any) => ({
|
||
productId: item.productId,
|
||
quantity: item.quantity,
|
||
unitId: item.unitId,
|
||
subtotal: item.subtotal,
|
||
})),
|
||
tax_amount: order.taxAmount,
|
||
status: newStatus,
|
||
remark: order.remark || "",
|
||
};
|
||
|
||
router.patch(route('purchase-orders.update', order.id), formData, {
|
||
onSuccess: () => toast.success(`採購單已${actionName === '取消' ? '作廢' : actionName}`),
|
||
onError: (errors: any) => {
|
||
console.error("Status Update Error:", errors);
|
||
toast.error(errors.error || "操作失敗");
|
||
}
|
||
});
|
||
};
|
||
|
||
// 權限判斷 (包含超級管理員檢查)
|
||
const isSuperAdmin = auth.user?.roles?.some((r: any) => r.name === 'super-admin');
|
||
const canApprove = isSuperAdmin || permissions.includes('purchase_orders.approve');
|
||
const canCancel = isSuperAdmin || permissions.includes('purchase_orders.cancel');
|
||
const canEdit = isSuperAdmin || permissions.includes('purchase_orders.edit');
|
||
const canView = isSuperAdmin || permissions.includes('purchase_orders.view');
|
||
|
||
// 送審權限:擁有檢視或編輯權限的人都可以送審
|
||
const canSubmit = canEdit || canView;
|
||
|
||
return (
|
||
<div className="flex items-center gap-4">
|
||
{['draft', 'pending', 'approved'].includes(order.status) && canCancel && (
|
||
<Button
|
||
onClick={() => handleUpdateStatus('cancelled', '作廢')}
|
||
disabled={processing}
|
||
variant="outline"
|
||
size="xl"
|
||
className="button-outlined-error shadow-red-200/20 border-red-600 text-red-600 hover:bg-red-50"
|
||
>
|
||
<XCircle className="h-5 w-5" /> 作廢
|
||
</Button>
|
||
)}
|
||
|
||
{order.status === 'pending' && canApprove && (
|
||
<Button
|
||
onClick={() => handleUpdateStatus('draft', '退回')}
|
||
disabled={processing}
|
||
variant="outline"
|
||
size="xl"
|
||
className="button-outlined-warning shadow-amber-200/20"
|
||
>
|
||
<RotateCcw className="h-5 w-5" /> 退回
|
||
</Button>
|
||
)}
|
||
|
||
<div className="flex-1" />
|
||
|
||
{order.status === 'draft' && canSubmit && (
|
||
<Button
|
||
onClick={() => handleUpdateStatus('pending', '送出審核')}
|
||
disabled={processing}
|
||
size="xl"
|
||
className="button-filled-primary shadow-primary/20"
|
||
>
|
||
<Send className="h-5 w-5" /> 送審
|
||
</Button>
|
||
)}
|
||
|
||
{order.status === 'pending' && canApprove && (
|
||
<Button
|
||
onClick={() => handleUpdateStatus('approved', '核准')}
|
||
disabled={processing}
|
||
size="xl"
|
||
className="button-filled-primary shadow-primary/20"
|
||
>
|
||
<CheckCircle className="h-5 w-5" /> 核准
|
||
</Button>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|