Files
star-erp/resources/js/Pages/PurchaseOrder/Show.tsx
sky121113 455f945296
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m8s
feat: 完成進貨單自動拋轉應付帳款流程與AP介面優化
1. 新增 AccountPayable (應付帳款) 模組,包含 Migration、Model、Service 與 Controller
2. 修改 GoodsReceipt (進貨單) 流程,在確認進貨時自動產生對應的應付帳款單 (AP-YYYYMMDD-XX)
3. 實作應付帳款詳細頁面 (Show.tsx),包含發票登記與標記付款功能
4. 修正應付帳款 Show 頁面的排版,將發票資訊套用標準的綠色背景區塊,並調整按鈕位置
5. 更新相關的 Service Provider 與 Routes
2026-02-24 16:46:55 +08:00

274 lines
14 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 { 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"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
</div>
{/* 頁面標題與操作 */}
<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>
<div className="flex items-center gap-2 mt-1">
<PurchaseOrderStatusBadge status={order.status} />
<span className="text-gray-500 text-sm">{order.poNumber}</span>
</div>
</div>
<div className="flex items-center gap-3">
<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 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>
</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>
);
}