diff --git a/app/Http/Controllers/InventoryController.php b/app/Http/Controllers/InventoryController.php index 43f1826..4925c34 100644 --- a/app/Http/Controllers/InventoryController.php +++ b/app/Http/Controllers/InventoryController.php @@ -10,6 +10,7 @@ class InventoryController extends Controller { $warehouse->load([ 'inventories.product.category', + 'inventories.product.baseUnit', 'inventories.lastIncomingTransaction', 'inventories.lastOutgoingTransaction' ]); @@ -20,7 +21,7 @@ class InventoryController extends Controller return [ 'id' => (string) $product->id, // Frontend expects string 'name' => $product->name, - 'type' => $product->category ? $product->category->name : '其他', // 暫時用 Category Name 當 Type + 'type' => $product->category?->name ?? '其他', // 暫時用 Category Name 當 Type ]; }); @@ -32,9 +33,9 @@ class InventoryController extends Controller 'id' => (string) $inv->id, 'warehouseId' => (string) $inv->warehouse_id, 'productId' => (string) $inv->product_id, - 'productName' => $inv->product->name, - 'productCode' => $inv->product->code ?? 'N/A', - 'unit' => $inv->product->base_unit ?? '個', + 'productName' => $inv->product?->name ?? '未知商品', + 'productCode' => $inv->product?->code ?? 'N/A', + 'unit' => $inv->product?->baseUnit?->name ?? '個', 'quantity' => (float) $inv->quantity, 'safetyStock' => $inv->safety_stock !== null ? (float) $inv->safety_stock : null, 'status' => '正常', // 前端會根據 quantity 與 safetyStock 重算,但後端亦可提供基礎狀態 @@ -53,8 +54,8 @@ class InventoryController extends Controller 'id' => 'ss-' . $inv->id, 'warehouseId' => (string) $inv->warehouse_id, 'productId' => (string) $inv->product_id, - 'productName' => $inv->product->name, - 'productType' => $inv->product->category ? $inv->product->category->name : '其他', + 'productName' => $inv->product?->name ?? '未知商品', + 'productType' => $inv->product?->category?->name ?? '其他', 'safetyStock' => (float) $inv->safety_stock, 'createdAt' => $inv->created_at->toIso8601String(), 'updatedAt' => $inv->updated_at->toIso8601String(), @@ -72,11 +73,13 @@ class InventoryController extends Controller public function create(\App\Models\Warehouse $warehouse) { // 取得所有商品供前端選單使用 - $products = \App\Models\Product::select('id', 'name', 'base_unit')->get()->map(function ($product) { + $products = \App\Models\Product::with(['baseUnit', 'largeUnit'])->select('id', 'name', 'base_unit_id', 'large_unit_id', 'conversion_rate')->get()->map(function ($product) { return [ 'id' => (string) $product->id, 'name' => $product->name, - 'unit' => $product->base_unit, + 'baseUnit' => $product->baseUnit?->name ?? '個', + 'largeUnit' => $product->largeUnit?->name, // 可能為 null + 'conversionRate' => (float) $product->conversion_rate, ]; }); @@ -145,7 +148,7 @@ class InventoryController extends Controller 'id' => (string) $inventory->id, 'warehouseId' => (string) $inventory->warehouse_id, 'productId' => (string) $inventory->product_id, - 'productName' => $inventory->product->name, + 'productName' => $inventory->product?->name ?? '未知商品', 'quantity' => (float) $inventory->quantity, 'batchNumber' => 'BATCH-' . $inventory->id, // Mock 'expiryDate' => '2099-12-31', // Mock @@ -315,8 +318,8 @@ class InventoryController extends Controller 'warehouse' => $warehouse, 'inventory' => [ 'id' => (string) $inventory->id, - 'productName' => $inventory->product->name, - 'productCode' => $inventory->product->code, + 'productName' => $inventory->product?->name ?? '未知商品', + 'productCode' => $inventory->product?->code ?? 'N/A', 'quantity' => (float) $inventory->quantity, ], 'transactions' => $transactions diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index b92fbdb..9f40598 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -54,7 +54,7 @@ class PurchaseOrderController extends Controller public function create() { - $vendors = Vendor::with('products')->get()->map(function ($vendor) { + $vendors = Vendor::with(['products.baseUnit', 'products.largeUnit', 'products.purchaseUnit'])->get()->map(function ($vendor) { return [ 'id' => (string) $vendor->id, 'name' => $vendor->name, @@ -62,9 +62,11 @@ class PurchaseOrderController extends Controller return [ 'productId' => (string) $product->id, 'productName' => $product->name, - 'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), // 優先使用採購單位 > 大單位 > 基本單位 - 'base_unit' => $product->base_unit, - 'purchase_unit' => $product->purchase_unit ?: $product->large_unit, // 若無採購單位,預設為大單位 + 'base_unit_id' => $product->base_unit_id, + 'base_unit_name' => $product->baseUnit?->name, + 'large_unit_id' => $product->large_unit_id, + 'large_unit_name' => $product->largeUnit?->name, + 'purchase_unit_id' => $product->purchase_unit_id, 'conversion_rate' => (float) $product->conversion_rate, 'lastPrice' => (float) ($product->pivot->last_price ?? 0), ]; @@ -96,6 +98,7 @@ class PurchaseOrderController extends Controller 'items.*.productId' => 'required|exists:products,id', 'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.unitPrice' => 'required|numeric|min:0', + 'items.*.unitId' => 'nullable|exists:units,id', // 驗證單位ID ]); try { @@ -157,6 +160,7 @@ class PurchaseOrderController extends Controller $order->items()->create([ 'product_id' => $item['productId'], 'quantity' => $item['quantity'], + 'unit_id' => $item['unitId'] ?? null, // 儲存單位ID 'unit_price' => $item['unitPrice'], 'subtotal' => $item['quantity'] * $item['unitPrice'], ]); @@ -174,20 +178,39 @@ class PurchaseOrderController extends Controller public function show($id) { - $order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product'])->findOrFail($id); + $order = PurchaseOrder::with(['vendor', 'warehouse', 'user', 'items.product.baseUnit', 'items.product.largeUnit'])->findOrFail($id); - // Transform items to include product details needed for frontend calculation - $order->items->transform(function ($item) { + $order->items->transform(function ($item) use ($order) { $product = $item->product; if ($product) { - // 手動附加 productName 和 unit (因為已從 $appends 移除) + // 手動附加所有必要的屬性 + $item->productId = (string) $product->id; $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->base_unit_id = $product->base_unit_id; + $item->base_unit_name = $product->baseUnit?->name; + $item->large_unit_id = $product->large_unit_id; + $item->large_unit_name = $product->largeUnit?->name; + $item->purchase_unit_id = $product->purchase_unit_id; + $item->conversion_rate = (float) $product->conversion_rate; - // 優先使用採購單位 > 大單位 > 基本單位 - $item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit); + + // Fetch last price + $lastPrice = DB::table('product_vendor') + ->where('vendor_id', $order->vendor_id) + ->where('product_id', $product->id) + ->value('last_price'); + $item->previousPrice = (float) ($lastPrice ?? 0); + + // 設定當前選中的單位 ID (from saved item) + $item->unitId = $item->unit_id; + + // 決定 selectedUnit (用於 UI 顯示) + if ($item->unitId && $item->large_unit_id && $item->unitId == $item->large_unit_id) { + $item->selectedUnit = 'large'; + } else { + $item->selectedUnit = 'base'; + } + $item->unitPrice = (float) $item->unit_price; } return $item; @@ -202,7 +225,7 @@ class PurchaseOrderController extends Controller { $order = PurchaseOrder::with(['items.product'])->findOrFail($id); - $vendors = Vendor::with('products')->get()->map(function ($vendor) { + $vendors = Vendor::with(['products.baseUnit', 'products.largeUnit', 'products.purchaseUnit'])->get()->map(function ($vendor) { return [ 'id' => (string) $vendor->id, 'name' => $vendor->name, @@ -210,9 +233,11 @@ class PurchaseOrderController extends Controller return [ 'productId' => (string) $product->id, 'productName' => $product->name, - 'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), - 'base_unit' => $product->base_unit, - 'purchase_unit' => $product->purchase_unit ?: $product->large_unit, + 'base_unit_id' => $product->base_unit_id, + 'base_unit_name' => $product->baseUnit?->name, + 'large_unit_id' => $product->large_unit_id, + 'large_unit_name' => $product->largeUnit?->name, + 'purchase_unit_id' => $product->purchase_unit_id, 'conversion_rate' => (float) $product->conversion_rate, 'lastPrice' => (float) ($product->pivot->last_price ?? 0), ]; @@ -228,17 +253,38 @@ class PurchaseOrderController extends Controller }); // Transform items for frontend form - $order->items->transform(function ($item) { + // Transform items for frontend form + $vendorId = $order->vendor_id; + $order->items->transform(function ($item) use ($vendorId) { $product = $item->product; if ($product) { - // 手動附加所有必要的屬性 (因為已從 $appends 移除) - $item->productId = (string) $product->id; // Ensure consistent ID type + // 手動附加所有必要的屬性 + $item->productId = (string) $product->id; $item->productName = $product->name; - $item->base_unit = $product->base_unit; - $item->purchase_unit = $product->purchase_unit ?: $product->large_unit; + $item->base_unit_id = $product->base_unit_id; + $item->base_unit_name = $product->baseUnit?->name; + $item->large_unit_id = $product->large_unit_id; + $item->large_unit_name = $product->largeUnit?->name; + $item->conversion_rate = (float) $product->conversion_rate; - // 優先使用採購單位 > 大單位 > 基本單位 - $item->unit = $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit); + + // Fetch last price + $lastPrice = DB::table('product_vendor') + ->where('vendor_id', $vendorId) + ->where('product_id', $product->id) + ->value('last_price'); + $item->previousPrice = (float) ($lastPrice ?? 0); + + // 設定當前選中的單位 ID + $item->unitId = $item->unit_id; // 資料庫中的 unit_id + + // 決定 selectedUnit (用於 UI 狀態) + if ($item->unitId && $item->large_unit_id && $item->unitId == $item->large_unit_id) { + $item->selectedUnit = 'large'; + } else { + $item->selectedUnit = 'base'; + } + $item->unitPrice = (float) $item->unit_price; } return $item; @@ -265,6 +311,7 @@ class PurchaseOrderController extends Controller 'items.*.productId' => 'required|exists:products,id', 'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.unitPrice' => 'required|numeric|min:0', + 'items.*.unitId' => 'nullable|exists:units,id', // 驗證單位ID ]); try { @@ -296,6 +343,7 @@ class PurchaseOrderController extends Controller $order->items()->create([ 'product_id' => $item['productId'], 'quantity' => $item['quantity'], + 'unit_id' => $item['unitId'] ?? null, // 儲存單位ID 'unit_price' => $item['unitPrice'], 'subtotal' => $item['quantity'] * $item['unitPrice'], ]); diff --git a/app/Http/Controllers/SafetyStockController.php b/app/Http/Controllers/SafetyStockController.php index 9a7cde8..d120a65 100644 --- a/app/Http/Controllers/SafetyStockController.php +++ b/app/Http/Controllers/SafetyStockController.php @@ -18,7 +18,7 @@ class SafetyStockController extends Controller { $warehouse->load(['inventories.product.category']); - $allProducts = Product::with('category')->get(); + $allProducts = Product::with(['category', 'baseUnit'])->get(); // 準備可選商品列表 $availableProducts = $allProducts->map(function ($product) { @@ -26,7 +26,7 @@ class SafetyStockController extends Controller 'id' => (string) $product->id, 'name' => $product->name, 'type' => $product->category ? $product->category->name : '其他', - 'unit' => $product->base_unit, + 'unit' => $product->baseUnit?->name ?? '個', ]; }); @@ -51,7 +51,7 @@ class SafetyStockController extends Controller 'productName' => $inv->product->name, 'productType' => $inv->product->category ? $inv->product->category->name : '其他', 'safetyStock' => (float) $inv->safety_stock, - 'unit' => $inv->product->base_unit, + 'unit' => $inv->product->baseUnit?->name ?? '個', 'updatedAt' => $inv->updated_at->toIso8601String(), ]; })->values(); diff --git a/app/Http/Controllers/TransferOrderController.php b/app/Http/Controllers/TransferOrderController.php index 05988be..5516f2d 100644 --- a/app/Http/Controllers/TransferOrderController.php +++ b/app/Http/Controllers/TransferOrderController.php @@ -97,7 +97,7 @@ class TransferOrderController extends Controller public function getWarehouseInventories(Warehouse $warehouse) { $inventories = $warehouse->inventories() - ->with(['product:id,name,base_unit,category_id', 'product.category']) + ->with(['product.baseUnit', 'product.category']) ->where('quantity', '>', 0) // 只回傳有庫存的 ->get() ->map(function ($inv) { @@ -106,7 +106,7 @@ class TransferOrderController extends Controller 'productName' => $inv->product->name, 'batchNumber' => 'BATCH-' . $inv->id, // 模擬批號 'availableQty' => (float) $inv->quantity, - 'unit' => $inv->product->base_unit, + 'unit' => $inv->product->baseUnit?->name ?? '個', ]; }); diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index 828c292..f7487aa 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -51,10 +51,10 @@ class VendorController extends Controller */ public function show(Vendor $vendor): \Inertia\Response { - $vendor->load('products'); + $vendor->load(['products.baseUnit', 'products.largeUnit']); return \Inertia\Inertia::render('Vendor/Show', [ 'vendor' => $vendor, - 'products' => \App\Models\Product::all(), + 'products' => \App\Models\Product::with('baseUnit')->get(), ]); } diff --git a/app/Http/Controllers/WarehouseController.php b/app/Http/Controllers/WarehouseController.php index c70fb7d..420f975 100644 --- a/app/Http/Controllers/WarehouseController.php +++ b/app/Http/Controllers/WarehouseController.php @@ -72,9 +72,25 @@ class WarehouseController extends Controller public function destroy(Warehouse $warehouse) { - // 真實刪除 - $warehouse->delete(); + // 檢查是否有相關聯的採購單 + if ($warehouse->purchaseOrders()->exists()) { + return redirect()->back()->with('error', '無法刪除:該倉庫有相關聯的採購單,請先處理採購單。'); + } - return redirect()->back()->with('success', '倉庫已刪除'); + \Illuminate\Support\Facades\DB::transaction(function () use ($warehouse) { + // 刪除庫存異動紀錄 (透過庫存關聯) + foreach ($warehouse->inventories as $inventory) { + // 刪除該庫存的所有異動紀錄 + $inventory->transactions()->delete(); + } + + // 刪除庫存項目 + $warehouse->inventories()->delete(); + + // 刪除倉庫 + $warehouse->delete(); + }); + + return redirect()->back()->with('success', '倉庫及其庫存與紀錄已刪除'); } } diff --git a/app/Models/PurchaseOrderItem.php b/app/Models/PurchaseOrderItem.php index bdc96dd..4e8e731 100644 --- a/app/Models/PurchaseOrderItem.php +++ b/app/Models/PurchaseOrderItem.php @@ -14,6 +14,7 @@ class PurchaseOrderItem extends Model 'purchase_order_id', 'product_id', 'quantity', + 'unit_id', // 新增單位ID欄位 'unit_price', 'subtotal', 'received_quantity', @@ -26,25 +27,33 @@ class PurchaseOrderItem extends Model 'received_quantity' => 'decimal:2', ]; - // 移除 $appends 以避免自動附加導致的錯誤 - // 這些屬性將在 Controller 中需要時手動附加 - // protected $appends = ['productName', 'unit']; - public function getProductNameAttribute(): string { return $this->product?->name ?? ''; } - - public function getUnitAttribute(): string + + // 關聯單位 + public function unit(): \Illuminate\Database\Eloquent\Relations\BelongsTo { - // 優先使用採購單位 > 大單位 > 基本單位 - // 與 PurchaseOrderController 的邏輯保持一致 + return $this->belongsTo(Unit::class); + } + + public function getUnitNameAttribute(): string + { + // 優先使用關聯的 unit + if ($this->unit) { + return $this->unit->name; + } + if (!$this->product) { return ''; } - return $this->product->purchase_unit - ?: ($this->product->large_unit ?: $this->product->base_unit); + // Fallback: 嘗試從 Product 的關聯單位獲取 + return $this->product->purchaseUnit?->name + ?? $this->product->largeUnit?->name + ?? $this->product->baseUnit?->name + ?? ''; } public function purchaseOrder(): BelongsTo diff --git a/database/migrations/2026_01_08_152856_add_unit_to_purchase_order_items_table.php b/database/migrations/2026_01_08_152856_add_unit_to_purchase_order_items_table.php new file mode 100644 index 0000000..25ab26c --- /dev/null +++ b/database/migrations/2026_01_08_152856_add_unit_to_purchase_order_items_table.php @@ -0,0 +1,28 @@ +string('unit')->nullable()->after('quantity')->comment('採購單位'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('purchase_order_items', function (Blueprint $table) { + $table->dropColumn('unit'); + }); + } +}; diff --git a/database/migrations/2026_01_08_154909_add_unit_id_to_purchase_order_items_table.php b/database/migrations/2026_01_08_154909_add_unit_id_to_purchase_order_items_table.php new file mode 100644 index 0000000..b1632dc --- /dev/null +++ b/database/migrations/2026_01_08_154909_add_unit_id_to_purchase_order_items_table.php @@ -0,0 +1,29 @@ +foreignId('unit_id')->nullable()->after('quantity')->comment('選擇的單位ID')->constrained('units')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('purchase_order_items', function (Blueprint $table) { + $table->dropForeign(['unit_id']); + $table->dropColumn('unit_id'); + }); + } +}; diff --git a/resources/js/Components/Product/ProductDialog.tsx b/resources/js/Components/Product/ProductDialog.tsx index a00cd9a..ec40a2b 100644 --- a/resources/js/Components/Product/ProductDialog.tsx +++ b/resources/js/Components/Product/ProductDialog.tsx @@ -244,26 +244,6 @@ export default function ProductDialog({ {errors.conversion_rate &&

{errors.conversion_rate}

} -
- - - {errors.purchase_unit_id &&

{errors.purchase_unit_id}

} -
{data.large_unit_id && data.base_unit_id && data.conversion_rate && ( diff --git a/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx b/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx index 3fc4b5a..e1d7912 100644 --- a/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx +++ b/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx @@ -21,7 +21,7 @@ import { TableRow, } from "@/Components/ui/table"; import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order"; -import { isPriceAlert, formatCurrency } from "@/utils/purchase-order"; +import { formatCurrency } from "@/utils/purchase-order"; interface PurchaseOrderItemsTableProps { items: PurchaseOrderItem[]; @@ -46,12 +46,13 @@ export function PurchaseOrderItemsTable({ - 商品名稱 + 商品名稱 數量 - 採購單位 - 換算基本單位 - 預估單價 - 小計 + 單位 + 換算基本單位 + 金額 + 小計 + 單價 / 基本單位 {!isReadOnly && } @@ -59,137 +60,184 @@ export function PurchaseOrderItemsTable({ {items.length === 0 ? ( {isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"} ) : ( - items.map((item, index) => ( - - {/* 商品選擇 */} - - {isReadOnly ? ( - {item.productName} - ) : ( - - )} - + items.map((item, index) => { + // 計算換算後的單價 (基本單位單價) + const convertedUnitPrice = item.selectedUnit === 'large' && item.conversion_rate + ? item.unitPrice / item.conversion_rate + : item.unitPrice; - {/* 數量 */} - - {isReadOnly ? ( - {Math.floor(item.quantity)} - ) : ( - - onItemChange?.(index, "quantity", Math.floor(Number(e.target.value))) - } - disabled={isDisabled} - className="h-10 text-left border-gray-200 w-24" - /> - )} - + return ( + + {/* 商品選擇 */} + + {isReadOnly ? ( + {item.productName} + ) : ( + + )} + - {/* 採購單位 */} - - {item.unit || "-"} - - - {/* 換算基本單位 */} - - - {item.conversion_rate && item.base_unit - ? `${parseFloat((item.quantity * item.conversion_rate).toFixed(2))} ${item.base_unit}` - : "-"} - - - - {/* 單價 */} - - {isReadOnly ? ( - {formatCurrency(item.unitPrice)} - ) : ( -
+ {/* 數量 */} + + {isReadOnly ? ( + {Math.floor(item.quantity)} + ) : ( - onItemChange?.(index, "unitPrice", Number(e.target.value)) + onItemChange?.(index, "quantity", Math.floor(Number(e.target.value))) } disabled={isDisabled} - 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" - }`} + className="h-10 text-left border-gray-200 w-24" /> - {/* 錯誤提示:有數量但沒有單價 */} - {item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && ( -

- ❌ 請填寫預估單價 -

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

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

+ )} +
+ + {/* 單位選擇 */} + + {!isReadOnly && item.large_unit_id ? ( + + ) : ( + + {item.selectedUnit === 'large' && item.large_unit_name + ? item.large_unit_name + : (item.base_unit_name || "個")} + + )} + + + {/* 換算基本單位 */} + +
+ + {item.selectedUnit === 'large' && item.conversion_rate + ? item.quantity * item.conversion_rate + : item.quantity} + + {item.base_unit_name || "個"} +
+
+ + {/* 單價 */} + + {isReadOnly ? ( + {formatCurrency(item.unitPrice)} + ) : ( +
+ + onItemChange?.(index, "unitPrice", Number(e.target.value)) + } + disabled={isDisabled} + 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" + : "border-gray-200" + }`} + /> + {/* 錯誤提示 (保留必填提示) */} + {item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && ( +

+ ❌ 請填寫金額 +

+ )} +
+ )} +
+ + {/* 小計 */} + + {formatCurrency(item.subtotal)} + + + {/* 換算採購單價 / 基本單位 */} + +
+
+ {formatCurrency(convertedUnitPrice)} / {item.base_unit_name || "個"} +
+ {convertedUnitPrice > 0 && item.previousPrice && item.previousPrice > 0 && ( + <> + {convertedUnitPrice > item.previousPrice && ( +

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

+ )} + {convertedUnitPrice < item.previousPrice && ( +

+ 📉 低於上次: {formatCurrency(item.previousPrice)} +

+ )} + )}
- )} -
- - {/* 小計 */} - - {formatCurrency(item.subtotal)} - - - {/* 刪除按鈕 */} - {!isReadOnly && onRemoveItem && ( - - - )} - - )) + + {/* 刪除按鈕 */} + {!isReadOnly && onRemoveItem && ( + + + + )} + + ); + }) )}
diff --git a/resources/js/Components/Vendor/AddSupplyProductDialog.tsx b/resources/js/Components/Vendor/AddSupplyProductDialog.tsx index 12e4828..79cb979 100644 --- a/resources/js/Components/Vendor/AddSupplyProductDialog.tsx +++ b/resources/js/Components/Vendor/AddSupplyProductDialog.tsx @@ -53,8 +53,8 @@ export default function AddSupplyProductDialog({ // 過濾掉已經在供貨列表中的商品 const availableProducts = useMemo(() => { - const existingIds = new Set(existingSupplyProducts.map(sp => sp.productId)); - return products.filter(p => !existingIds.has(p.id)); + const existingIds = new Set(existingSupplyProducts.map(sp => String(sp.productId))); + return products.filter(p => !existingIds.has(String(p.id))); }, [products, existingSupplyProducts]); const selectedProduct = availableProducts.find(p => p.id === selectedProductId); @@ -105,7 +105,7 @@ export default function AddSupplyProductDialog({ - + @@ -132,7 +132,7 @@ export default function AddSupplyProductDialog({
{product.name} - {product.purchase_unit || product.base_unit || "個"} + {product.baseUnit?.name || (product.base_unit as any)?.name || product.base_unit || "個"}
@@ -146,15 +146,17 @@ export default function AddSupplyProductDialog({ {/* 單位(自動帶入) */}
- +
- {selectedProduct ? (selectedProduct.purchase_unit || selectedProduct.base_unit || "個") : "-"} + {selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "-"}
{/* 上次採購價格 */}
- + - + # 商品名稱 - 採購單位 - 上次採購單價 + 基本單位 + 轉換率 + + 上次採購單價 +
(以基本單位計算)
+
操作 {products.length === 0 ? ( - + 尚無供貨商品,請點擊上方按鈕新增 @@ -47,9 +51,26 @@ export default function SupplyProductList({ {index + 1} {product.productName} - {product.unit} + + {product.baseUnit || product.unit || "-"} + + + {product.largeUnit && product.conversionRate ? ( + + 1 {product.largeUnit} = {Number(product.conversionRate)} {product.baseUnit || product.unit} + + ) : ( + "-" + )} + - {product.lastPrice ? `$${product.lastPrice.toLocaleString()}` : "-"} + {product.lastPrice ? ( + + ${product.lastPrice.toLocaleString()} / {product.baseUnit || product.unit || "單位"} + + ) : ( + "-" + )}
diff --git a/resources/js/Components/Warehouse/TransferOrderDialog.tsx b/resources/js/Components/Warehouse/TransferOrderDialog.tsx index 4e766c3..b38225a 100644 --- a/resources/js/Components/Warehouse/TransferOrderDialog.tsx +++ b/resources/js/Components/Warehouse/TransferOrderDialog.tsx @@ -5,7 +5,7 @@ */ import { useState, useEffect } from "react"; -import { getCurrentDateTime, generateOrderNumber } from "@/utils/format"; +import { getCurrentDateTime } from "@/utils/format"; import axios from "axios"; import { Dialog, @@ -46,6 +46,7 @@ interface AvailableProduct { productName: string; batchNumber: string; availableQty: number; + unit: string; } export default function TransferOrderDialog({ @@ -276,7 +277,7 @@ export default function TransferOrderDialog({ value={`${product.productId}|||${product.batchNumber}`} > {product.productName} (庫存:{" "} - {product.availableQty}) + {product.availableQty} {product.unit}) )) )} @@ -303,7 +304,7 @@ export default function TransferOrderDialog({
{selectedProduct && (

- 可用庫存: {selectedProduct.availableQty} + 可用庫存: {selectedProduct.availableQty} {selectedProduct.unit}

)}
diff --git a/resources/js/Pages/PurchaseOrder/Show.tsx b/resources/js/Pages/PurchaseOrder/Show.tsx index fa67e60..fb15a51 100644 --- a/resources/js/Pages/PurchaseOrder/Show.tsx +++ b/resources/js/Pages/PurchaseOrder/Show.tsx @@ -9,6 +9,7 @@ import { Head, Link } from "@inertiajs/react"; import { StatusProgressBar } from "@/Components/PurchaseOrder/StatusProgressBar"; import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderStatusBadge"; import CopyButton from "@/Components/shared/CopyButton"; +import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable"; import type { PurchaseOrder } from "@/types/purchase-order"; import { formatCurrency, formatDateTime } from "@/utils/format"; import { getShowBreadcrumbs } from "@/utils/breadcrumb"; @@ -104,66 +105,17 @@ export default function ViewPurchaseOrderPage({ order }: Props) {

採購項目清單

-
- - - - - - - - - - - - {order.items.map((item, index) => ( - - - - - - - - ))} - - - - - - - -
- # - - 商品名稱 - - 單價 - - 數量 - - 小計 -
- {index + 1} - -
- {item.productName} - ID: {item.productId} -
-
-
- {formatCurrency(item.unitPrice)} -
-
- - {item.quantity} {item.unit} - - - {formatCurrency(item.subtotal)} -
- 總金額 - - {formatCurrency(order.totalAmount)} -
+
+ +
+ 總金額 + + {formatCurrency(order.totalAmount)} + +
diff --git a/resources/js/Pages/Vendor/Show.tsx b/resources/js/Pages/Vendor/Show.tsx index 90839eb..8362499 100644 --- a/resources/js/Pages/Vendor/Show.tsx +++ b/resources/js/Pages/Vendor/Show.tsx @@ -33,8 +33,14 @@ interface VendorProduct { id: number; name: string; unit?: string; - base_unit?: string; + // Relations might be camelCase or snake_case depending on serialization settings + baseUnit?: { name: string }; + base_unit?: { name: string }; + largeUnit?: { name: string }; + large_unit?: { name: string }; + purchaseUnit?: string; // Note: if it's a relation it might be an object, but original code treated it as string purchase_unit?: string; + conversion_rate?: number; pivot: Pivot; } @@ -54,13 +60,29 @@ export default function VendorShow({ vendor, products }: ShowProps) { const [selectedProduct, setSelectedProduct] = useState(null); // 轉換後端資料格式為前端組件需要的格式 - const supplyProducts: SupplyProduct[] = vendor.products.map(p => ({ - id: String(p.id), - productId: String(p.id), - productName: p.name, - unit: p.purchase_unit || p.base_unit || "個", - lastPrice: p.pivot.last_price || undefined, - })); + const supplyProducts: SupplyProduct[] = vendor.products.map(p => { + // Laravel load('relationName') usually results in camelCase key in JSON if method is camelCase + const baseUnitName = p.baseUnit?.name || p.base_unit?.name; + const largeUnitName = p.largeUnit?.name || p.large_unit?.name; + + // Check purchase unit - seemingly originally a field string, but if relation, check if object + // Assuming purchase_unit is a string field on product table here based on original code usage? + // Wait, original code usage: p.purchase_unit || ... + // In Product model: purchase_unit_id exists, purchaseUnit is relation. + // If p.purchase_unit was working before, it might be an attribute (accessors). + // Let's stick to safe access. + + return { + id: String(p.id), + productId: String(p.id), + productName: p.name, + unit: p.purchase_unit || baseUnitName || "個", + baseUnit: baseUnitName, + largeUnit: largeUnitName, + conversionRate: p.conversion_rate, + lastPrice: p.pivot.last_price || undefined, + }; + }); const handleAddProduct = (productId: string, lastPrice?: number) => { router.post(route('vendors.products.store', vendor.id), { diff --git a/resources/js/Pages/Warehouse/AddInventory.tsx b/resources/js/Pages/Warehouse/AddInventory.tsx index 16318be..711532d 100644 --- a/resources/js/Pages/Warehouse/AddInventory.tsx +++ b/resources/js/Pages/Warehouse/AddInventory.tsx @@ -33,7 +33,9 @@ import { getInventoryBreadcrumbs } from "@/utils/breadcrumb"; interface Product { id: string; name: string; - unit: string; + baseUnit: string; + largeUnit?: string; + conversionRate?: number; } interface Props { @@ -58,13 +60,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) { // 新增明細行 const handleAddItem = () => { - const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", unit: "kg" }; + const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", baseUnit: "個" }; const newItem: InboundItem = { tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, productId: defaultProduct.id, productName: defaultProduct.name, quantity: 0, - unit: defaultProduct.unit, + unit: defaultProduct.baseUnit, // 僅用於顯示當前選擇單位的名稱 + baseUnit: defaultProduct.baseUnit, + largeUnit: defaultProduct.largeUnit, + conversionRate: defaultProduct.conversionRate, + selectedUnit: 'base', }; setItems([...items, newItem]); }; @@ -86,11 +92,16 @@ export default function AddInventoryPage({ warehouse, products }: Props) { // 處理商品變更 const handleProductChange = (tempId: string, productId: string) => { const product = products.find((p) => p.id === productId); + if (product) { handleUpdateItem(tempId, { productId, productName: product.name, - unit: product.unit, + unit: product.baseUnit, + baseUnit: product.baseUnit, + largeUnit: product.largeUnit, + conversionRate: product.conversionRate, + selectedUnit: 'base', }); } }; @@ -135,10 +146,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) { inboundDate, reason, notes, - items: items.map(item => ({ - productId: item.productId, - quantity: item.quantity - })) + items: items.map(item => { + // 如果選擇大單位,則換算為基本單位數量 + const finalQuantity = item.selectedUnit === 'large' && item.conversionRate + ? item.quantity * item.conversionRate + : item.quantity; + + return { + productId: item.productId, + quantity: finalQuantity + }; + }) }, { onSuccess: () => { toast.success("庫存記錄已儲存"); @@ -296,71 +314,106 @@ export default function AddInventoryPage({ warehouse, products }: Props) { 數量 * 單位 + 轉換數量 {/* 效期 進貨編號 */} - {items.map((item, index) => ( - - {/* 商品 */} - - - {errors[`item-${index}-product`] && ( -

- {errors[`item-${index}-product`]} -

- )} -
+ {items.map((item, index) => { + // 計算轉換數量 + const convertedQuantity = item.selectedUnit === 'large' && item.conversionRate + ? item.quantity * item.conversionRate + : item.quantity; - {/* 數量 */} - - - handleUpdateItem(item.tempId, { - quantity: parseInt(e.target.value) || 0, - }) - } - className="border-gray-300" - /> - {errors[`item-${index}-quantity`] && ( -

- {errors[`item-${index}-quantity`]} -

- )} -
+ return ( + + {/* 商品 */} + + + {errors[`item-${index}-product`] && ( +

+ {errors[`item-${index}-product`]} +

+ )} +
- {/* 單位 */} - - - + {/* 數量 */} + + + handleUpdateItem(item.tempId, { + quantity: parseFloat(e.target.value) || 0, + }) + } + className="border-gray-300" + /> + {errors[`item-${index}-quantity`] && ( +

+ {errors[`item-${index}-quantity`]} +

+ )} +
- {/* 效期 */} - {/* + {/* 單位 */} + + {item.largeUnit ? ( + + ) : ( + + )} + + + {/* 轉換數量 */} + +
+ {convertedQuantity} + {item.baseUnit || "個"} +
+
+ + {/* 效期 */} + {/*
*/} - {/* 批號 */} - {/* + {/* 批號 */} + {/* @@ -392,20 +445,21 @@ export default function AddInventoryPage({ warehouse, products }: Props) { )} */} - {/* 刪除按鈕 */} - - - - - ))} + {/* 刪除按鈕 */} + + + + + ); + })}
diff --git a/resources/js/Pages/Warehouse/Index.tsx b/resources/js/Pages/Warehouse/Index.tsx index 0883c0d..7aa3d92 100644 --- a/resources/js/Pages/Warehouse/Index.tsx +++ b/resources/js/Pages/Warehouse/Index.tsx @@ -78,9 +78,17 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) { }; const handleDeleteWarehouse = (id: string) => { - if (confirm("確定要停用此倉庫嗎?\n注意:刪除倉庫將連帶刪除所有庫存與紀錄!")) { - router.delete(route('warehouses.destroy', id)); - } + router.delete(route('warehouses.destroy', id), { + onSuccess: () => { + toast.success('倉庫已刪除'); + setEditingWarehouse(null); + }, + onError: (errors: any) => { + // If backend returns error bag or flash error + // Flash error is handled by AuthenticatedLayout usually via usePage props. + // But we can also check errors bag here if needed. + } + }); }; const handleAddTransferOrder = () => { diff --git a/resources/js/Pages/Warehouse/Inventory.tsx b/resources/js/Pages/Warehouse/Inventory.tsx index d436e8a..a5e9c42 100644 --- a/resources/js/Pages/Warehouse/Inventory.tsx +++ b/resources/js/Pages/Warehouse/Inventory.tsx @@ -63,7 +63,7 @@ export default function WarehouseInventoryPage({ // 導航至流動紀錄頁 const handleView = (inventoryId: string) => { - router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventory: inventoryId })); + router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventoryId: inventoryId })); }; @@ -74,13 +74,17 @@ export default function WarehouseInventoryPage({ const handleDelete = () => { if (!deleteId) return; - router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventory: deleteId }), { + // 暫存 ID 以免在對話框關閉的瞬間 state 被清空 + const idToDelete = deleteId; + + router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventoryId: idToDelete }), { onSuccess: () => { toast.success("庫存記錄已刪除"); setDeleteId(null); }, onError: () => { toast.error("刪除失敗"); + // 保持對話框開啟以便重試,或根據需要關閉 } }); }; @@ -112,7 +116,7 @@ export default function WarehouseInventoryPage({ {/* 操作按鈕 (位於標題下方) */}
{/* 安全庫存設定按鈕 */} - +
- - - {/* 刪除確認對話框 */} !open && setDeleteId(null)}> @@ -176,7 +177,12 @@ export default function WarehouseInventoryPage({ 取消 - + { + handleDelete(); + }} + className="bg-red-600 hover:bg-red-700 text-white" + > 確認刪除 diff --git a/resources/js/hooks/usePurchaseOrderForm.ts b/resources/js/hooks/usePurchaseOrderForm.ts index c8b3f13..516dcb9 100644 --- a/resources/js/hooks/usePurchaseOrderForm.ts +++ b/resources/js/hooks/usePurchaseOrderForm.ts @@ -52,10 +52,10 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP { productId: "", productName: "", - quantity: 0, - unit: "", + quantity: 1, unitPrice: 0, subtotal: 0, + selectedUnit: "base", }, ]); }; @@ -66,32 +66,60 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP }; // 更新商品項目 - const updateItem = (index: number, field: keyof PurchaseOrderItem, value: string | number) => { + const updateItem = (index: number, field: keyof PurchaseOrderItem, value: any) => { const newItems = [...items]; - newItems[index] = { ...newItems[index], [field]: value }; + const item = { ...newItems[index] }; - // 當選擇商品時,自動填入商品資訊 if (field === "productId" && selectedSupplier) { + // value is productId string const product = selectedSupplier.commonProducts.find((p) => p.productId === value); 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; + // @ts-ignore + item.productId = value; + item.productName = product.productName; + item.base_unit_id = product.base_unit_id; + item.base_unit_name = product.base_unit_name; + item.large_unit_id = product.large_unit_id; + item.large_unit_name = product.large_unit_name; + item.purchase_unit_id = product.purchase_unit_id; + item.conversion_rate = product.conversion_rate; + item.unitPrice = product.lastPrice; + item.previousPrice = product.lastPrice; + + // 決定預設單位 + // 若有採購單位且等於大單位,預設為大單位 + const isPurchaseUnitLarge = product.purchase_unit_id && product.large_unit_id && product.purchase_unit_id === product.large_unit_id; + + if (isPurchaseUnitLarge) { + item.selectedUnit = 'large'; + item.unitId = product.large_unit_id; + } else { + item.selectedUnit = 'base'; + item.unitId = product.base_unit_id; + } } + } else if (field === "selectedUnit") { + // @ts-ignore + item.selectedUnit = value; + if (value === 'large') { + item.unitId = item.large_unit_id; + } else { + item.unitId = item.base_unit_id; + } + } else { + // @ts-ignore + item[field] = value; } // 計算小計 - if (field === "quantity" || field === "unitPrice") { - newItems[index].subtotal = calculateSubtotal( - Number(newItems[index].quantity), - Number(newItems[index].unitPrice) + if (field === "quantity" || field === "unitPrice" || field === "productId") { + item.subtotal = calculateSubtotal( + Number(item.quantity), + Number(item.unitPrice) ); } + newItems[index] = item; setItems(newItems); }; diff --git a/resources/js/types/product.ts b/resources/js/types/product.ts index 56e959b..83d5454 100644 --- a/resources/js/types/product.ts +++ b/resources/js/types/product.ts @@ -14,4 +14,5 @@ export interface Product { conversion_rate?: number; purchase_unit?: string; unit?: string; // 相容舊有程式碼 + baseUnit?: { name: string }; } diff --git a/resources/js/types/purchase-order.ts b/resources/js/types/purchase-order.ts index 53357f2..0b7381a 100644 --- a/resources/js/types/purchase-order.ts +++ b/resources/js/types/purchase-order.ts @@ -21,10 +21,14 @@ export interface PurchaseOrderItem { productId: string; productName: string; quantity: number; - unit: string; - base_unit?: string; // 基本庫存單位 - purchase_unit?: string; // 採購單位 - conversion_rate?: number;// 換算率 + unitId?: number; // 選擇的單位ID + base_unit_id?: number; + base_unit_name?: string; + large_unit_id?: number; + large_unit_name?: string; + purchase_unit_id?: number; + conversion_rate?: number; + selectedUnit?: 'base' | 'large'; // 前端狀態輔助 unitPrice: number; previousPrice?: number; subtotal: number; @@ -79,9 +83,11 @@ export interface PurchaseOrder { export interface CommonProduct { productId: string; productName: string; - unit: string; - base_unit?: string; - purchase_unit?: string; + base_unit_id?: number; + base_unit_name?: string; + large_unit_id?: number; + large_unit_name?: string; + purchase_unit_id?: number; conversion_rate?: number; lastPrice: number; } diff --git a/resources/js/types/vendor.ts b/resources/js/types/vendor.ts index fd16d85..1a066e3 100644 --- a/resources/js/types/vendor.ts +++ b/resources/js/types/vendor.ts @@ -8,6 +8,9 @@ export interface SupplyProduct { productId: string; productName: string; unit: string; + baseUnit?: string; + largeUnit?: string; + conversionRate?: number; lastPrice?: number; } diff --git a/resources/js/types/warehouse.ts b/resources/js/types/warehouse.ts index 62597f3..56c257b 100644 --- a/resources/js/types/warehouse.ts +++ b/resources/js/types/warehouse.ts @@ -147,6 +147,10 @@ export interface InboundItem { productName: string; quantity: number; unit: string; + baseUnit?: string; + largeUnit?: string; + conversionRate?: number; + selectedUnit?: 'base' | 'large'; } /**