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);
}