From fad74df6ac94dc4b2c9d49926d89cecef11158ea Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 6 Jan 2026 15:45:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A1=E8=B3=BC=E5=96=AE?= =?UTF-8?q?=E8=B7=9F=E5=95=86=E5=93=81=E8=B3=87=E6=96=99=E4=B8=80=E4=BA=9B?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../Controllers/PurchaseOrderController.php | 44 +++++++++++++- app/Models/PurchaseOrderItem.php | 30 ++++------ .../js/Components/Product/ProductDialog.tsx | 57 +++++++++++-------- .../PurchaseOrder/PurchaseOrderItemsTable.tsx | 43 ++++++++++---- resources/js/Pages/PurchaseOrder/Create.tsx | 34 +++++++++-- resources/js/hooks/usePurchaseOrderForm.ts | 3 + resources/js/types/purchase-order.ts | 6 ++ resources/js/utils/purchase-order.ts | 4 +- 9 files changed, 159 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 7426401..227cd55 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ docker exec -it koori-erp-laravel.test-1 php artisan migrate --seed ```bash docker exec -it koori-erp-laravel.test-1 npm install -docker exec -it koori-erp-laravel.test-1 npm run build +docker exec -it koori-erp-laravel.test-1 npm run dev ``` 啟動後,您可以透過以下連結瀏覽專案: diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 8755c5a..b92fbdb 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -62,7 +62,10 @@ class PurchaseOrderController extends Controller return [ 'productId' => (string) $product->id, 'productName' => $product->name, - 'unit' => $product->base_unit, + 'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), // 優先使用採購單位 > 大單位 > 基本單位 + 'base_unit' => $product->base_unit, + 'purchase_unit' => $product->purchase_unit ?: $product->large_unit, // 若無採購單位,預設為大單位 + 'conversion_rate' => (float) $product->conversion_rate, 'lastPrice' => (float) ($product->pivot->last_price ?? 0), ]; }) @@ -173,6 +176,23 @@ class PurchaseOrderController extends Controller { $order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product'])->findOrFail($id); + // Transform items to include product details needed for frontend calculation + $order->items->transform(function ($item) { + $product = $item->product; + if ($product) { + // 手動附加 productName 和 unit (因為已從 $appends 移除) + $item->productName = $product->name; + $item->productId = $product->id; + $item->base_unit = $product->base_unit; + $item->purchase_unit = $product->purchase_unit ?: $product->large_unit; // Fallback logic same as Create + $item->conversion_rate = (float) $product->conversion_rate; + // 優先使用採購單位 > 大單位 > 基本單位 + $item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit); + $item->unitPrice = (float) $item->unit_price; + } + return $item; + }); + return Inertia::render('PurchaseOrder/Show', [ 'order' => $order ]); @@ -190,7 +210,10 @@ class PurchaseOrderController extends Controller return [ 'productId' => (string) $product->id, 'productName' => $product->name, - 'unit' => $product->base_unit, + 'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), + 'base_unit' => $product->base_unit, + 'purchase_unit' => $product->purchase_unit ?: $product->large_unit, + 'conversion_rate' => (float) $product->conversion_rate, 'lastPrice' => (float) ($product->pivot->last_price ?? 0), ]; }) @@ -204,6 +227,23 @@ class PurchaseOrderController extends Controller ]; }); + // Transform items for frontend form + $order->items->transform(function ($item) { + $product = $item->product; + if ($product) { + // 手動附加所有必要的屬性 (因為已從 $appends 移除) + $item->productId = (string) $product->id; // Ensure consistent ID type + $item->productName = $product->name; + $item->base_unit = $product->base_unit; + $item->purchase_unit = $product->purchase_unit ?: $product->large_unit; + $item->conversion_rate = (float) $product->conversion_rate; + // 優先使用採購單位 > 大單位 > 基本單位 + $item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit); + $item->unitPrice = (float) $item->unit_price; + } + return $item; + }); + return Inertia::render('PurchaseOrder/Create', [ 'order' => $order, 'suppliers' => $vendors, diff --git a/app/Models/PurchaseOrderItem.php b/app/Models/PurchaseOrderItem.php index 2b25208..bdc96dd 100644 --- a/app/Models/PurchaseOrderItem.php +++ b/app/Models/PurchaseOrderItem.php @@ -26,31 +26,25 @@ class PurchaseOrderItem extends Model 'received_quantity' => 'decimal:2', ]; - protected $appends = [ - 'productName', - 'unit', - 'productId', - 'unitPrice', - ]; - - public function getProductIdAttribute(): string - { - return (string) $this->attributes['product_id']; - } - - public function getUnitPriceAttribute(): float - { - return (float) $this->attributes['unit_price']; - } + // 移除 $appends 以避免自動附加導致的錯誤 + // 這些屬性將在 Controller 中需要時手動附加 + // protected $appends = ['productName', 'unit']; public function getProductNameAttribute(): string { - return $this->product ? $this->product->name : ''; + return $this->product?->name ?? ''; } public function getUnitAttribute(): string { - return $this->product ? $this->product->base_unit : ''; + // 優先使用採購單位 > 大單位 > 基本單位 + // 與 PurchaseOrderController 的邏輯保持一致 + if (!$this->product) { + return ''; + } + + return $this->product->purchase_unit + ?: ($this->product->large_unit ?: $this->product->base_unit); } public function purchaseOrder(): BelongsTo diff --git a/resources/js/Components/Product/ProductDialog.tsx b/resources/js/Components/Product/ProductDialog.tsx index 973b460..256493e 100644 --- a/resources/js/Components/Product/ProductDialog.tsx +++ b/resources/js/Components/Product/ProductDialog.tsx @@ -21,6 +21,13 @@ import { import { useForm } from "@inertiajs/react"; import { toast } from "sonner"; import type { Product, Category } from "@/Pages/Product/Index"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/Components/ui/dropdown-menu"; +import { ChevronDown } from "lucide-react"; interface ProductDialogProps { open: boolean; @@ -41,7 +48,7 @@ export default function ProductDialog({ category_id: "", brand: "", specification: "", - base_unit: "kg", + base_unit: "公斤", large_unit: "", conversion_rate: "", purchase_unit: "", @@ -184,32 +191,34 @@ export default function ProductDialog({ - +
+ setData("base_unit", e.target.value)} + placeholder="可輸入或選擇..." + className={errors.base_unit ? "border-red-500 flex-1" : "flex-1"} + /> + + + + + + {["公斤", "公克", "公升", "毫升", "個", "支", "包", "罐", "瓶", "箱", "袋"].map((u) => ( + setData("base_unit", u)}> + {u} + + ))} + + +
{errors.base_unit &&

{errors.base_unit}

}
- + - + ); } \ No newline at end of file diff --git a/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx b/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx index 18b6bc4..3fc4b5a 100644 --- a/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx +++ b/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx @@ -46,11 +46,12 @@ export function PurchaseOrderItemsTable({ - 商品名稱 - 數量 - 單位 - 預估單價 - 小計 + 商品名稱 + 數量 + 採購單位 + 換算基本單位 + 預估單價 + 小計 {!isReadOnly && } @@ -58,7 +59,7 @@ export function PurchaseOrderItemsTable({ {items.length === 0 ? ( {isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"} @@ -115,11 +116,20 @@ export function PurchaseOrderItemsTable({ )} - {/* 單位 */} + {/* 採購單位 */} {item.unit || "-"} + {/* 換算基本單位 */} + + + {item.conversion_rate && item.base_unit + ? `${parseFloat((item.quantity * item.conversion_rate).toFixed(2))} ${item.base_unit}` + : "-"} + + + {/* 單價 */} {isReadOnly ? ( @@ -135,12 +145,23 @@ export function PurchaseOrderItemsTable({ onItemChange?.(index, "unitPrice", Number(e.target.value)) } disabled={isDisabled} - className={`h-10 text-left w-32 ${isPriceAlert(item.unitPrice, item.previousPrice) - ? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500" - : "border-gray-200" + className={`h-10 text-left w-32 ${ + // 如果有數量但沒有單價,顯示錯誤樣式 + item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) + ? "border-red-400 bg-red-50 focus-visible:ring-red-500" + : isPriceAlert(item.unitPrice, item.previousPrice) + ? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500" + : "border-gray-200" }`} /> - {isPriceAlert(item.unitPrice, item.previousPrice) && ( + {/* 錯誤提示:有數量但沒有單價 */} + {item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && ( +

+ ❌ 請填寫預估單價 +

+ )} + {/* 價格警示:單價高於上次 */} + {item.unitPrice > 0 && isPriceAlert(item.unitPrice, item.previousPrice) && (

⚠️ 高於上次: {formatCurrency(item.previousPrice || 0)}

diff --git a/resources/js/Pages/PurchaseOrder/Create.tsx b/resources/js/Pages/PurchaseOrder/Create.tsx index 68d5ca6..770475f 100644 --- a/resources/js/Pages/PurchaseOrder/Create.tsx +++ b/resources/js/Pages/PurchaseOrder/Create.tsx @@ -60,8 +60,8 @@ export default function CreatePurchaseOrder({ setStatus, } = usePurchaseOrderForm({ order, suppliers }); + const totalAmount = calculateTotalAmount(items); - const isValid = validatePurchaseOrder(String(supplierId), expectedDate, items); const handleSave = () => { if (!warehouseId) { @@ -84,9 +84,23 @@ export default function CreatePurchaseOrder({ 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("請填寫有效的採購數量(必須大於 0)"); + toast.error("請確保所有商品都有填寫數量和單價"); return; } @@ -107,7 +121,14 @@ export default function CreatePurchaseOrder({ router.put(`/purchase-orders/${order.id}`, data, { onSuccess: () => toast.success("採購單已更新"), onError: (errors) => { - toast.error("更新失敗,請檢查輸入內容"); + // 顯示更詳細的錯誤訊息 + if (errors.items) { + toast.error("商品資料有誤,請檢查數量和單價是否正確填寫"); + } else if (errors.error) { + toast.error(errors.error); + } else { + toast.error("更新失敗,請檢查輸入內容"); + } console.error(errors); } }); @@ -115,10 +136,12 @@ export default function CreatePurchaseOrder({ router.post("/purchase-orders", data, { onSuccess: () => toast.success("採購單已成功建立"), onError: (errors) => { - if (errors.error) { + if (errors.items) { + toast.error("商品資料有誤,請檢查數量和單價是否正確填寫"); + } else if (errors.error) { toast.error(errors.error); } else { - toast.error("建立失敗,請檢查輸入內容"); + toast.error("建立失敗,請檢查輸入內容"); } console.error(errors); } @@ -127,7 +150,6 @@ export default function CreatePurchaseOrder({ }; const hasSupplier = !!supplierId; - const canSave = isValid && !!warehouseId && items.length > 0; return ( diff --git a/resources/js/hooks/usePurchaseOrderForm.ts b/resources/js/hooks/usePurchaseOrderForm.ts index c9c90af..c8b3f13 100644 --- a/resources/js/hooks/usePurchaseOrderForm.ts +++ b/resources/js/hooks/usePurchaseOrderForm.ts @@ -76,6 +76,9 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP if (product) { newItems[index].productName = product.productName; newItems[index].unit = product.unit; + newItems[index].base_unit = product.base_unit; + newItems[index].purchase_unit = product.purchase_unit; + newItems[index].conversion_rate = product.conversion_rate; newItems[index].unitPrice = product.lastPrice; newItems[index].previousPrice = product.lastPrice; } diff --git a/resources/js/types/purchase-order.ts b/resources/js/types/purchase-order.ts index d634273..53357f2 100644 --- a/resources/js/types/purchase-order.ts +++ b/resources/js/types/purchase-order.ts @@ -22,6 +22,9 @@ export interface PurchaseOrderItem { productName: string; quantity: number; unit: string; + base_unit?: string; // 基本庫存單位 + purchase_unit?: string; // 採購單位 + conversion_rate?: number;// 換算率 unitPrice: number; previousPrice?: number; subtotal: number; @@ -77,6 +80,9 @@ export interface CommonProduct { productId: string; productName: string; unit: string; + base_unit?: string; + purchase_unit?: string; + conversion_rate?: number; lastPrice: number; } diff --git a/resources/js/utils/purchase-order.ts b/resources/js/utils/purchase-order.ts index db9abfc..2b213dc 100644 --- a/resources/js/utils/purchase-order.ts +++ b/resources/js/utils/purchase-order.ts @@ -76,8 +76,8 @@ export function validatePurchaseOrder( } /** - * 過濾有效項目(數量大於 0) + * 過濾有效項目(數量和單價都必須大於 0) */ export function filterValidItems(items: PurchaseOrderItem[]): PurchaseOrderItem[] { - return items.filter((item) => item.quantity > 0); + return items.filter((item) => item.quantity > 0 && item.unitPrice > 0); }