2025-12-30 15:03:19 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 建立/編輯採購單頁面
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { ArrowLeft, Plus, Info } from "lucide-react";
|
|
|
|
|
|
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";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from "@/Components/ui/select";
|
|
|
|
|
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
|
|
|
|
|
import { Head, Link, router } from "@inertiajs/react";
|
|
|
|
|
|
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 {
|
|
|
|
|
|
validatePurchaseOrder,
|
|
|
|
|
|
filterValidItems,
|
|
|
|
|
|
calculateTotalAmount,
|
|
|
|
|
|
getTodayDate,
|
|
|
|
|
|
formatCurrency,
|
|
|
|
|
|
} from "@/utils/purchase-order";
|
|
|
|
|
|
import { STATUS_OPTIONS } from "@/constants/purchase-order";
|
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
order?: PurchaseOrder;
|
|
|
|
|
|
suppliers: Supplier[];
|
|
|
|
|
|
warehouses: Warehouse[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function CreatePurchaseOrder({
|
|
|
|
|
|
order,
|
|
|
|
|
|
suppliers,
|
|
|
|
|
|
warehouses,
|
|
|
|
|
|
}: Props) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
supplierId,
|
|
|
|
|
|
expectedDate,
|
|
|
|
|
|
items,
|
|
|
|
|
|
notes,
|
|
|
|
|
|
selectedSupplier,
|
|
|
|
|
|
isOrderSent,
|
|
|
|
|
|
warehouseId,
|
|
|
|
|
|
setSupplierId,
|
|
|
|
|
|
setExpectedDate,
|
|
|
|
|
|
setNotes,
|
|
|
|
|
|
setWarehouseId,
|
|
|
|
|
|
addItem,
|
|
|
|
|
|
removeItem,
|
|
|
|
|
|
updateItem,
|
|
|
|
|
|
status,
|
|
|
|
|
|
setStatus,
|
|
|
|
|
|
} = usePurchaseOrderForm({ order, suppliers });
|
|
|
|
|
|
|
|
|
|
|
|
const totalAmount = calculateTotalAmount(items);
|
|
|
|
|
|
const isValid = validatePurchaseOrder(String(supplierId), expectedDate, items);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
|
if (!isValid || !warehouseId) {
|
|
|
|
|
|
toast.error("請填寫完整的表單資訊");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const validItems = filterValidItems(items);
|
|
|
|
|
|
if (validItems.length === 0) {
|
|
|
|
|
|
toast.error("請至少新增一項採購商品");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
vendor_id: supplierId,
|
|
|
|
|
|
warehouse_id: warehouseId,
|
|
|
|
|
|
expected_delivery_date: expectedDate,
|
|
|
|
|
|
remark: notes,
|
|
|
|
|
|
status: status,
|
|
|
|
|
|
items: validItems.map(item => ({
|
|
|
|
|
|
productId: item.productId,
|
|
|
|
|
|
quantity: item.quantity,
|
|
|
|
|
|
unitPrice: item.unitPrice,
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (order) {
|
|
|
|
|
|
// Edit not implemented yet but structure is ready
|
|
|
|
|
|
router.put(`/purchase-orders/${order.id}`, data, {
|
|
|
|
|
|
onSuccess: () => toast.success("採購單已更新")
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.post("/purchase-orders", data, {
|
|
|
|
|
|
onSuccess: () => toast.success("採購單已成功建立")
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const hasSupplier = !!supplierId;
|
|
|
|
|
|
const canSave = isValid && !!warehouseId && items.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<AuthenticatedLayout>
|
|
|
|
|
|
<Head title={order ? "編輯採購單" : "建立採購單"} />
|
|
|
|
|
|
<div className="container mx-auto p-6 max-w-5xl">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
|
<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="mb-6">
|
|
|
|
|
|
<h1 className="mb-2">{order ? "編輯採購單" : "建立採購單"}</h1>
|
|
|
|
|
|
<p className="text-gray-600">
|
|
|
|
|
|
{order ? `修改採購單 ${order.poNumber} 的詳細資訊` : "填寫新採購單的資訊以開始流程"}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-8">
|
|
|
|
|
|
{/* 步驟一:基本資訊 */}
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="p-8 space-y-8">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={String(warehouseId)}
|
|
|
|
|
|
onValueChange={setWarehouseId}
|
|
|
|
|
|
disabled={isOrderSent}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-12 border-gray-200 focus:ring-primary/20">
|
|
|
|
|
|
<SelectValue placeholder="請選擇倉庫" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{warehouses.map((w) => (
|
|
|
|
|
|
<SelectItem key={w.id} value={String(w.id)}>
|
|
|
|
|
|
{w.name}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">供應商</label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={String(supplierId)}
|
|
|
|
|
|
onValueChange={setSupplierId}
|
|
|
|
|
|
disabled={isOrderSent}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-12 border-gray-200">
|
|
|
|
|
|
<SelectValue placeholder="選擇供應商" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{suppliers.map((s) => (
|
|
|
|
|
|
<SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</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">
|
|
|
|
|
|
預計到貨日期
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={expectedDate || ""}
|
|
|
|
|
|
onChange={(e) => setExpectedDate(e.target.value)}
|
|
|
|
|
|
min={getTodayDate()}
|
|
|
|
|
|
className="h-12 border-gray-200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{order && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<label className="text-sm font-bold text-gray-700">狀態</label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={status}
|
|
|
|
|
|
onValueChange={(v) => setStatus(v as any)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-12 border-gray-200">
|
|
|
|
|
|
<SelectValue placeholder="選擇狀態" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{STATUS_OPTIONS.map((opt) => (
|
|
|
|
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</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="備註這筆採購單的特殊需求..."
|
|
|
|
|
|
className="min-h-[100px] border-gray-200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</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">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
2025-12-30 17:05:19 +08:00
|
|
|
|
<div className="p-8">
|
|
|
|
|
|
{!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 && (
|
|
|
|
|
|
<div className="mt-8 flex justify-end">
|
|
|
|
|
|
<div className="bg-primary/5 px-8 py-5 rounded-xl border border-primary/10 inline-flex flex-col items-end min-w-[240px]">
|
|
|
|
|
|
<span className="text-sm text-gray-500 font-medium mb-1">採購預估總額</span>
|
|
|
|
|
|
<span className="text-3xl font-black text-primary">{formatCurrency(totalAmount)}</span>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 底部按鈕 */}
|
2025-12-30 17:05:19 +08:00
|
|
|
|
<div className="flex items-center justify-end gap-4 py-8 border-t border-gray-100 mt-8">
|
2025-12-30 15:03:19 +08:00
|
|
|
|
<Link href="/purchase-orders">
|
|
|
|
|
|
<Button variant="ghost" className="h-12 px-8 text-gray-500 hover:text-gray-700">
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
className="bg-primary hover:bg-primary/90 text-white px-12 h-14 rounded-xl shadow-lg shadow-primary/20 text-lg font-bold transition-all hover:scale-[1.02] active:scale-[0.98]"
|
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
|
disabled={!canSave}
|
|
|
|
|
|
>
|
|
|
|
|
|
{order ? "更新採購單" : "確認發布採購單"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</AuthenticatedLayout>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|