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

258 lines
13 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 { formatDate } from "@/lib/date";
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"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
</div>
{/* 頁面標題與操作 */}
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4 mb-6">
<div className="space-y-2">
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<ShoppingCart className="h-6 w-6 text-primary-main" />
: {order.poNumber}
</h1>
<PurchaseOrderStatusBadge status={order.status} />
</div>
<p className="text-sm text-gray-500 font-medium flex items-center gap-2">
: {order.supplierName} <span className="mx-1">|</span>
{/* 此處將申請單位及申請人一起顯示 */}
: {order.warehouse_name} ({order.createdBy}) <span className="mx-1">|</span>
: {formatDateTime(order.createdAt)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<PurchaseOrderActions order={order} />
</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 lg:grid-cols-3 gap-x-8 gap-y-6">
<div>
<span className="text-sm text-gray-500 block mb-1"></span>
<span className="font-medium text-gray-900">{order.orderDate ? formatDate(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 ? formatDate(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>
</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-2">
{['draft', 'pending', 'approved'].includes(order.status) && canCancel && (
<Button
onClick={() => handleUpdateStatus('cancelled', '作廢')}
disabled={processing}
variant="outline"
className="button-outlined-error border-red-600 text-red-600 hover:bg-red-50"
>
<XCircle className="h-4 w-4 mr-1" />
</Button>
)}
{order.status === 'pending' && canApprove && (
<Button
onClick={() => handleUpdateStatus('draft', '退回')}
disabled={processing}
variant="outline"
className="button-outlined-warning"
>
<RotateCcw className="h-4 w-4 mr-1" /> 退
</Button>
)}
{order.status === 'draft' && canSubmit && (
<Button
onClick={() => handleUpdateStatus('pending', '送出審核')}
disabled={processing}
className="button-filled-primary shadow-primary/20"
>
<Send className="h-4 w-4 mr-1" />
</Button>
)}
{order.status === 'pending' && canApprove && (
<Button
onClick={() => handleUpdateStatus('approved', '核准')}
disabled={processing}
className="button-filled-primary shadow-primary/20"
>
<CheckCircle className="h-4 w-4 mr-1" />
</Button>
)}
</div>
);
}