Files
star-erp/resources/js/Pages/PurchaseOrder/Create.tsx
sky121113 e15f85642c
Some checks failed
Koori-ERP-Deploy / auto-deploy (push) Failing after 12s
chore: bypass dns using host ip
2025-12-30 17:05:19 +08:00

296 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, 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>
{/* 步驟二:品項明細 */}
<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}
disabled={!canSave}
>
{order ? "更新採購單" : "確認發布採購單"}
</Button>
</div>
</div>
</AuthenticatedLayout>
);
}