Files
star-erp/resources/js/Pages/PurchaseOrder/Create.tsx
sky121113 fad74df6ac
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 52s
更新採購單跟商品資料一些bug
2026-01-06 15:45:13 +08:00

343 lines
15 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, 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 handleSave = () => {
if (!warehouseId) {
toast.error("請選擇入庫倉庫");
return;
}
if (!supplierId) {
toast.error("請選擇供應商");
return;
}
if (!expectedDate) {
toast.error("請選擇預計到貨日期");
return;
}
if (items.length === 0) {
toast.error("請至少新增一項採購商品");
return;
}
// 檢查是否有數量大於 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;
}
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) {
router.put(`/purchase-orders/${order.id}`, data, {
onSuccess: () => toast.success("採購單已更新"),
onError: (errors) => {
// 顯示更詳細的錯誤訊息
if (errors.items) {
toast.error("商品資料有誤,請檢查數量和單價是否正確填寫");
} else if (errors.error) {
toast.error(errors.error);
} else {
toast.error("更新失敗,請檢查輸入內容");
}
console.error(errors);
}
});
} else {
router.post("/purchase-orders", data, {
onSuccess: () => toast.success("採購單已成功建立"),
onError: (errors) => {
if (errors.items) {
toast.error("商品資料有誤,請檢查數量和單價是否正確填寫");
} else if (errors.error) {
toast.error(errors.error);
} else {
toast.error("建立失敗,請檢查輸入內容");
}
console.error(errors);
}
});
}
};
const hasSupplier = !!supplierId;
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>
{/* 步驟二:品項明細 */}
<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>
</div>
<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>
)}
<PurchaseOrderItemsTable
items={items}
supplier={selectedSupplier}
isReadOnly={isOrderSent}
isDisabled={!hasSupplier}
onRemoveItem={removeItem}
onItemChange={updateItem}
/>
{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>
</div>
)}
</div>
</div>
</div>
{/* 底部按鈕 */}
<div className="flex items-center justify-end gap-4 py-8 border-t border-gray-100 mt-8">
<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}
>
{order ? "更新採購單" : "確認發布採購單"}
</Button>
</div>
</div>
</AuthenticatedLayout>
);
}