2025-12-30 15:03:19 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 建立/編輯採購單頁面
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-01-13 17:00:58 +08:00
|
|
|
|
import { ArrowLeft, Plus, Info, ShoppingCart } from "lucide-react";
|
2026-01-19 15:32:41 +08:00
|
|
|
|
import { useEffect } from "react";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
import { Button } from "@/Components/ui/button";
|
|
|
|
|
|
import { Input } from "@/Components/ui/input";
|
|
|
|
|
|
import { Textarea } from "@/Components/ui/textarea";
|
|
|
|
|
|
import { Alert, AlertDescription } from "@/Components/ui/alert";
|
2026-01-09 10:18:52 +08:00
|
|
|
|
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
2026-02-06 15:32:12 +08:00
|
|
|
|
import { Head, Link, router, usePage } from "@inertiajs/react";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable";
|
|
|
|
|
|
import type { PurchaseOrder, Supplier } from "@/types/purchase-order";
|
|
|
|
|
|
import type { Warehouse } from "@/types/requester";
|
|
|
|
|
|
import { usePurchaseOrderForm } from "@/hooks/usePurchaseOrderForm";
|
|
|
|
|
|
import {
|
|
|
|
|
|
filterValidItems,
|
|
|
|
|
|
calculateTotalAmount,
|
|
|
|
|
|
getTodayDate,
|
|
|
|
|
|
formatCurrency,
|
|
|
|
|
|
} from "@/utils/purchase-order";
|
2026-02-06 15:32:12 +08:00
|
|
|
|
import { STATUS_CONFIG, MANUAL_STATUS_OPTIONS } from "@/constants/purchase-order";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
import { toast } from "sonner";
|
2026-02-06 15:32:12 +08:00
|
|
|
|
import { Can } from "@/Components/Permission/Can";
|
2026-01-07 13:06:49 +08:00
|
|
|
|
import { getCreateBreadcrumbs, getEditBreadcrumbs } from "@/utils/breadcrumb";
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
order?: PurchaseOrder;
|
|
|
|
|
|
suppliers: Supplier[];
|
|
|
|
|
|
warehouses: Warehouse[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function CreatePurchaseOrder({
|
|
|
|
|
|
order,
|
|
|
|
|
|
suppliers,
|
|
|
|
|
|
warehouses,
|
|
|
|
|
|
}: Props) {
|
2026-02-06 15:32:12 +08:00
|
|
|
|
const { auth } = usePage<any>().props;
|
|
|
|
|
|
const permissions = auth.user?.permissions || [];
|
|
|
|
|
|
const isSuperAdmin = auth.user?.roles?.some((r: any) => r.name === 'super-admin');
|
|
|
|
|
|
|
|
|
|
|
|
const canApprove = isSuperAdmin || permissions.includes('purchase_orders.approve');
|
|
|
|
|
|
const canCreate = isSuperAdmin || permissions.includes('purchase_orders.create');
|
|
|
|
|
|
const canEdit = isSuperAdmin || permissions.includes('purchase_orders.edit');
|
|
|
|
|
|
|
|
|
|
|
|
// 儲存權限判斷
|
|
|
|
|
|
const canSave = order ? canEdit : canCreate;
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
const {
|
|
|
|
|
|
supplierId,
|
|
|
|
|
|
expectedDate,
|
2026-01-26 17:27:34 +08:00
|
|
|
|
orderDate,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
items,
|
|
|
|
|
|
notes,
|
|
|
|
|
|
selectedSupplier,
|
|
|
|
|
|
isOrderSent,
|
|
|
|
|
|
warehouseId,
|
|
|
|
|
|
setSupplierId,
|
|
|
|
|
|
setExpectedDate,
|
2026-01-26 17:27:34 +08:00
|
|
|
|
setOrderDate,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
setNotes,
|
|
|
|
|
|
setWarehouseId,
|
|
|
|
|
|
addItem,
|
|
|
|
|
|
removeItem,
|
|
|
|
|
|
updateItem,
|
|
|
|
|
|
status,
|
|
|
|
|
|
setStatus,
|
2026-01-09 10:18:52 +08:00
|
|
|
|
invoiceNumber,
|
|
|
|
|
|
invoiceDate,
|
|
|
|
|
|
invoiceAmount,
|
|
|
|
|
|
setInvoiceNumber,
|
|
|
|
|
|
setInvoiceDate,
|
|
|
|
|
|
setInvoiceAmount,
|
2026-01-19 15:32:41 +08:00
|
|
|
|
taxAmount,
|
|
|
|
|
|
setTaxAmount,
|
|
|
|
|
|
isTaxManual,
|
|
|
|
|
|
setIsTaxManual,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
} = usePurchaseOrderForm({ order, suppliers });
|
|
|
|
|
|
|
2026-01-06 15:45:13 +08:00
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
const totalAmount = calculateTotalAmount(items);
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
// Auto-calculate tax if not manual
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!isTaxManual) {
|
|
|
|
|
|
const calculatedTax = Math.round(totalAmount * 0.05);
|
|
|
|
|
|
setTaxAmount(calculatedTax);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [totalAmount, isTaxManual]);
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
const handleSave = () => {
|
2025-12-31 17:48:36 +08:00
|
|
|
|
if (!warehouseId) {
|
|
|
|
|
|
toast.error("請選擇入庫倉庫");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!supplierId) {
|
|
|
|
|
|
toast.error("請選擇供應商");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 17:27:34 +08:00
|
|
|
|
if (!orderDate) {
|
2026-02-04 13:20:18 +08:00
|
|
|
|
toast.error("請選擇下單日期");
|
2026-01-26 17:27:34 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 17:48:36 +08:00
|
|
|
|
if (!expectedDate) {
|
|
|
|
|
|
toast.error("請選擇預計到貨日期");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
|
toast.error("請至少新增一項採購商品");
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-06 15:45:13 +08:00
|
|
|
|
// 檢查是否有數量大於 0 的項目
|
|
|
|
|
|
const itemsWithQuantity = items.filter(item => item.quantity > 0);
|
|
|
|
|
|
if (itemsWithQuantity.length === 0) {
|
|
|
|
|
|
toast.error("請填寫有效的採購數量(必須大於 0)");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查有數量的項目是否都有填寫單價
|
|
|
|
|
|
const itemsWithoutPrice = itemsWithQuantity.filter(item => !item.unitPrice || item.unitPrice <= 0);
|
|
|
|
|
|
if (itemsWithoutPrice.length > 0) {
|
|
|
|
|
|
toast.error("請填寫所有商品的預估單價(必須大於 0)");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
const validItems = filterValidItems(items);
|
|
|
|
|
|
if (validItems.length === 0) {
|
2026-01-06 15:45:13 +08:00
|
|
|
|
toast.error("請確保所有商品都有填寫數量和單價");
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
vendor_id: supplierId,
|
|
|
|
|
|
warehouse_id: warehouseId,
|
2026-01-26 17:27:34 +08:00
|
|
|
|
order_date: orderDate,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
expected_delivery_date: expectedDate,
|
|
|
|
|
|
remark: notes,
|
|
|
|
|
|
status: status,
|
2026-01-09 10:18:52 +08:00
|
|
|
|
invoice_number: invoiceNumber || null,
|
|
|
|
|
|
invoice_date: invoiceDate || null,
|
|
|
|
|
|
invoice_amount: invoiceAmount ? parseFloat(invoiceAmount) : null,
|
2026-01-19 15:32:41 +08:00
|
|
|
|
tax_amount: Number(taxAmount) || 0,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
items: validItems.map(item => ({
|
|
|
|
|
|
productId: item.productId,
|
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
|
unitPrice: item.unitPrice,
|
2026-01-08 16:52:03 +08:00
|
|
|
|
unitId: item.unitId,
|
2026-01-08 17:51:06 +08:00
|
|
|
|
subtotal: item.subtotal,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (order) {
|
|
|
|
|
|
router.put(`/purchase-orders/${order.id}`, data, {
|
2026-01-27 13:27:28 +08:00
|
|
|
|
|
|
|
|
|
|
onSuccess: () => { },//toast.success("採購單已更新"),
|
2025-12-31 17:48:36 +08:00
|
|
|
|
onError: (errors) => {
|
2026-01-06 15:45:13 +08:00
|
|
|
|
// 顯示更詳細的錯誤訊息
|
|
|
|
|
|
if (errors.items) {
|
|
|
|
|
|
toast.error("商品資料有誤,請檢查數量和單價是否正確填寫");
|
|
|
|
|
|
} else if (errors.error) {
|
|
|
|
|
|
toast.error(errors.error);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.error("更新失敗,請檢查輸入內容");
|
|
|
|
|
|
}
|
2025-12-31 17:48:36 +08:00
|
|
|
|
console.error(errors);
|
|
|
|
|
|
}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.post("/purchase-orders", data, {
|
2026-01-27 13:27:28 +08:00
|
|
|
|
|
|
|
|
|
|
onSuccess: () => { },//toast.success("採購單已成功建立"),
|
2025-12-31 17:48:36 +08:00
|
|
|
|
onError: (errors) => {
|
2026-01-06 15:45:13 +08:00
|
|
|
|
if (errors.items) {
|
|
|
|
|
|
toast.error("商品資料有誤,請檢查數量和單價是否正確填寫");
|
|
|
|
|
|
} else if (errors.error) {
|
2025-12-31 17:48:36 +08:00
|
|
|
|
toast.error(errors.error);
|
|
|
|
|
|
} else {
|
2026-01-06 15:45:13 +08:00
|
|
|
|
toast.error("建立失敗,請檢查輸入內容");
|
2025-12-31 17:48:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
console.error(errors);
|
|
|
|
|
|
}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const hasSupplier = !!supplierId;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-07 13:06:49 +08:00
|
|
|
|
<AuthenticatedLayout breadcrumbs={order ? getEditBreadcrumbs("purchaseOrders") : getCreateBreadcrumbs("purchaseOrders")}>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<Head title={order ? "編輯採購單" : "建立採購單"} />
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="container mx-auto p-6 max-w-7xl">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{/* Header */}
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="mb-6">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<Link href="/purchase-orders">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
2026-01-19 15:32:41 +08:00
|
|
|
|
className="gap-2 button-outlined-primary mb-4"
|
2025-12-30 15:03:19 +08:00
|
|
|
|
>
|
|
|
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
|
返回列表
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="mb-4">
|
2026-01-13 17:00:58 +08:00
|
|
|
|
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
2026-01-16 14:36:24 +08:00
|
|
|
|
<ShoppingCart className="h-6 w-6 text-primary-main" />
|
2026-01-13 17:00:58 +08:00
|
|
|
|
{order ? "編輯採購單" : "建立採購單"}
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="text-gray-500 mt-1">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{order ? `修改採購單 ${order.poNumber} 的詳細資訊` : "填寫新採購單的資訊以開始流程"}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="space-y-6">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{/* 步驟一:基本資訊 */}
|
|
|
|
|
|
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
|
|
|
|
|
|
<div className="p-6 bg-gray-50/50 border-b flex items-center gap-3">
|
|
|
|
|
|
<div className="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold">1</div>
|
|
|
|
|
|
<h2 className="text-lg font-bold">基本資訊</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="p-6 space-y-6">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">
|
|
|
|
|
|
預計入庫倉庫
|
|
|
|
|
|
</label>
|
2026-01-09 10:18:52 +08:00
|
|
|
|
<SearchableSelect
|
2025-12-30 15:03:19 +08:00
|
|
|
|
value={String(warehouseId)}
|
|
|
|
|
|
onValueChange={setWarehouseId}
|
|
|
|
|
|
disabled={isOrderSent}
|
2026-01-09 10:18:52 +08:00
|
|
|
|
options={warehouses.map((w) => ({ label: w.name, value: String(w.id) }))}
|
|
|
|
|
|
placeholder="請選擇倉庫"
|
|
|
|
|
|
searchPlaceholder="搜尋倉庫..."
|
|
|
|
|
|
/>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">供應商</label>
|
2026-01-09 10:18:52 +08:00
|
|
|
|
<SearchableSelect
|
2025-12-30 15:03:19 +08:00
|
|
|
|
value={String(supplierId)}
|
|
|
|
|
|
onValueChange={setSupplierId}
|
|
|
|
|
|
disabled={isOrderSent}
|
2026-01-09 10:18:52 +08:00
|
|
|
|
options={suppliers.map((s) => ({ label: s.name, value: String(s.id) }))}
|
|
|
|
|
|
placeholder="選擇供應商"
|
|
|
|
|
|
searchPlaceholder="搜尋供應商..."
|
|
|
|
|
|
/>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">
|
2026-02-04 13:20:18 +08:00
|
|
|
|
下單日期 <span className="text-red-500">*</span>
|
2026-01-26 17:27:34 +08:00
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={orderDate || ""}
|
|
|
|
|
|
onChange={(e) => setOrderDate(e.target.value)}
|
|
|
|
|
|
className="block w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
預計到貨日期
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={expectedDate || ""}
|
|
|
|
|
|
onChange={(e) => setExpectedDate(e.target.value)}
|
|
|
|
|
|
min={getTodayDate()}
|
2026-01-19 17:07:45 +08:00
|
|
|
|
className="block w-full"
|
2025-12-30 15:03:19 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{order && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">狀態</label>
|
2026-02-06 15:32:12 +08:00
|
|
|
|
<Can permission="purchase_orders.approve">
|
|
|
|
|
|
<SearchableSelect
|
|
|
|
|
|
value={status}
|
|
|
|
|
|
onValueChange={(v) => setStatus(v as any)}
|
|
|
|
|
|
options={MANUAL_STATUS_OPTIONS}
|
|
|
|
|
|
placeholder="選擇狀態"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Can>
|
|
|
|
|
|
<div className="!mt-1">
|
|
|
|
|
|
{!canApprove && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="px-3 py-2 bg-gray-50 border rounded-md text-sm text-gray-600">
|
|
|
|
|
|
{STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-[10px] text-gray-400 mt-1 italic">
|
|
|
|
|
|
* 您沒有權限在此修改狀態,請使用詳情頁面的動作按鈕進行操作。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">備註事項</label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
value={notes || ""}
|
|
|
|
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
|
|
|
|
placeholder="備註這筆採購單的特殊需求..."
|
2026-01-19 17:07:45 +08:00
|
|
|
|
className="min-h-[100px]"
|
2025-12-30 15:03:19 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-09 10:18:52 +08:00
|
|
|
|
{/* 發票資訊 */}
|
|
|
|
|
|
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
|
|
|
|
|
|
<div className="p-6 bg-gray-50/50 border-b flex items-center gap-3">
|
|
|
|
|
|
<div className="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold">2</div>
|
|
|
|
|
|
<h2 className="text-lg font-bold">發票資訊</h2>
|
|
|
|
|
|
<span className="text-sm text-gray-500">(選填)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="p-6 space-y-6">
|
2026-01-09 10:18:52 +08:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">
|
|
|
|
|
|
發票號碼
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={invoiceNumber}
|
|
|
|
|
|
onChange={(e) => setInvoiceNumber(e.target.value)}
|
|
|
|
|
|
placeholder="AB-12345678"
|
|
|
|
|
|
maxLength={11}
|
2026-01-19 17:07:45 +08:00
|
|
|
|
className="block w-full"
|
2026-01-09 10:18:52 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-500">格式:2 碼英文 + 分隔線 + 8 碼數字</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">
|
|
|
|
|
|
發票日期
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={invoiceDate}
|
|
|
|
|
|
onChange={(e) => setInvoiceDate(e.target.value)}
|
2026-01-19 17:07:45 +08:00
|
|
|
|
className="block w-full"
|
2026-01-09 10:18:52 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">
|
|
|
|
|
|
發票金額
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={invoiceAmount}
|
|
|
|
|
|
onChange={(e) => setInvoiceAmount(e.target.value)}
|
|
|
|
|
|
placeholder="0"
|
|
|
|
|
|
min="0"
|
2026-02-05 11:45:08 +08:00
|
|
|
|
step="any"
|
2026-01-19 17:07:45 +08:00
|
|
|
|
className="block w-full"
|
2026-01-09 10:18:52 +08:00
|
|
|
|
/>
|
|
|
|
|
|
{invoiceAmount && totalAmount > 0 && parseFloat(invoiceAmount) !== totalAmount && (
|
|
|
|
|
|
<p className="text-xs text-amber-600">
|
|
|
|
|
|
⚠️ 發票金額與採購總額不一致(總額:{formatCurrency(totalAmount)})
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 步驟三:品項明細 */}
|
2025-12-30 17:05:19 +08:00
|
|
|
|
<div className={`bg-white rounded-lg border shadow-sm overflow-hidden transition-all duration-300 ${!hasSupplier ? 'opacity-60 saturate-50' : ''}`}>
|
|
|
|
|
|
<div className="p-6 bg-gray-50/50 border-b flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
2026-01-09 10:18:52 +08:00
|
|
|
|
<div className="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-bold">3</div>
|
2025-12-30 17:05:19 +08:00
|
|
|
|
<h2 className="text-lg font-bold">採購商品明細</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={addItem}
|
|
|
|
|
|
disabled={!hasSupplier || isOrderSent}
|
|
|
|
|
|
className="button-filled-primary h-10 gap-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="h-4 w-4" /> 新增一個品項
|
|
|
|
|
|
</Button>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="p-6">
|
2025-12-30 17:05:19 +08:00
|
|
|
|
{!hasSupplier && (
|
|
|
|
|
|
<Alert className="mb-6 bg-amber-50 border-amber-200 text-amber-800">
|
|
|
|
|
|
<Info className="h-4 w-4 text-amber-600" />
|
|
|
|
|
|
<AlertDescription>
|
|
|
|
|
|
請先在步驟一選擇「供應商」,才能從該供應商的常用項目中選取商品。
|
|
|
|
|
|
</AlertDescription>
|
|
|
|
|
|
</Alert>
|
|
|
|
|
|
)}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2025-12-30 17:05:19 +08:00
|
|
|
|
<PurchaseOrderItemsTable
|
|
|
|
|
|
items={items}
|
|
|
|
|
|
supplier={selectedSupplier}
|
|
|
|
|
|
isReadOnly={isOrderSent}
|
|
|
|
|
|
isDisabled={!hasSupplier}
|
|
|
|
|
|
onRemoveItem={removeItem}
|
|
|
|
|
|
onItemChange={updateItem}
|
|
|
|
|
|
/>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2025-12-30 17:05:19 +08:00
|
|
|
|
{hasSupplier && items.length > 0 && (
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<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(totalAmount)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-between items-center w-full gap-4">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-sm text-gray-500 font-medium">稅額 (5%)</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="h-6 w-6 p-0 text-gray-400 hover:text-primary"
|
|
|
|
|
|
title="重設為自動計算 (5%)"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const autoTax = Math.round(totalAmount * 0.05);
|
|
|
|
|
|
setTaxAmount(autoTax);
|
|
|
|
|
|
setIsTaxManual(false);
|
|
|
|
|
|
toast.success("已重設為自動計算 (5%)");
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
↺
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="relative w-32">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
2026-02-05 11:45:08 +08:00
|
|
|
|
step="any"
|
2026-01-19 15:32:41 +08:00
|
|
|
|
value={taxAmount}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
setTaxAmount(e.target.value);
|
|
|
|
|
|
setIsTaxManual(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="text-right h-9 bg-white"
|
|
|
|
|
|
placeholder="0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</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(totalAmount + (Number(taxAmount) || 0))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2025-12-30 17:05:19 +08:00
|
|
|
|
</div>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
2025-12-30 17:05:19 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-12-30 15:03:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 底部按鈕 */}
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<div className="flex items-center justify-end gap-4 py-6 border-t border-gray-100 mt-6">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<Link href="/purchase-orders">
|
2026-01-19 15:32:41 +08:00
|
|
|
|
<Button variant="ghost" className="h-11 px-6 text-gray-500 hover:text-gray-700">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<Button
|
2026-02-06 15:32:12 +08:00
|
|
|
|
size="xl"
|
|
|
|
|
|
className="bg-primary hover:bg-primary/90 text-white shadow-primary/20"
|
2025-12-30 15:03:19 +08:00
|
|
|
|
onClick={handleSave}
|
2026-02-06 15:32:12 +08:00
|
|
|
|
disabled={!canSave}
|
|
|
|
|
|
title={!canSave ? "您沒有執行此動作的權限" : ""}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
>
|
|
|
|
|
|
{order ? "更新採購單" : "確認發布採購單"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</AuthenticatedLayout>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|