大更新
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 58s

This commit is contained in:
2026-01-08 16:32:10 +08:00
parent 7848976a06
commit 0b60dab208
25 changed files with 661 additions and 392 deletions

View File

@@ -10,6 +10,7 @@ class InventoryController extends Controller
{ {
$warehouse->load([ $warehouse->load([
'inventories.product.category', 'inventories.product.category',
'inventories.product.baseUnit',
'inventories.lastIncomingTransaction', 'inventories.lastIncomingTransaction',
'inventories.lastOutgoingTransaction' 'inventories.lastOutgoingTransaction'
]); ]);
@@ -20,7 +21,7 @@ class InventoryController extends Controller
return [ return [
'id' => (string) $product->id, // Frontend expects string 'id' => (string) $product->id, // Frontend expects string
'name' => $product->name, '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, 'id' => (string) $inv->id,
'warehouseId' => (string) $inv->warehouse_id, 'warehouseId' => (string) $inv->warehouse_id,
'productId' => (string) $inv->product_id, 'productId' => (string) $inv->product_id,
'productName' => $inv->product->name, 'productName' => $inv->product?->name ?? '未知商品',
'productCode' => $inv->product->code ?? 'N/A', 'productCode' => $inv->product?->code ?? 'N/A',
'unit' => $inv->product->base_unit ?? '個', 'unit' => $inv->product?->baseUnit?->name ?? '個',
'quantity' => (float) $inv->quantity, 'quantity' => (float) $inv->quantity,
'safetyStock' => $inv->safety_stock !== null ? (float) $inv->safety_stock : null, 'safetyStock' => $inv->safety_stock !== null ? (float) $inv->safety_stock : null,
'status' => '正常', // 前端會根據 quantity 與 safetyStock 重算,但後端亦可提供基礎狀態 'status' => '正常', // 前端會根據 quantity 與 safetyStock 重算,但後端亦可提供基礎狀態
@@ -53,8 +54,8 @@ class InventoryController extends Controller
'id' => 'ss-' . $inv->id, 'id' => 'ss-' . $inv->id,
'warehouseId' => (string) $inv->warehouse_id, 'warehouseId' => (string) $inv->warehouse_id,
'productId' => (string) $inv->product_id, 'productId' => (string) $inv->product_id,
'productName' => $inv->product->name, 'productName' => $inv->product?->name ?? '未知商品',
'productType' => $inv->product->category ? $inv->product->category->name : '其他', 'productType' => $inv->product?->category?->name ?? '其他',
'safetyStock' => (float) $inv->safety_stock, 'safetyStock' => (float) $inv->safety_stock,
'createdAt' => $inv->created_at->toIso8601String(), 'createdAt' => $inv->created_at->toIso8601String(),
'updatedAt' => $inv->updated_at->toIso8601String(), 'updatedAt' => $inv->updated_at->toIso8601String(),
@@ -72,11 +73,13 @@ class InventoryController extends Controller
public function create(\App\Models\Warehouse $warehouse) 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 [ return [
'id' => (string) $product->id, 'id' => (string) $product->id,
'name' => $product->name, '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, 'id' => (string) $inventory->id,
'warehouseId' => (string) $inventory->warehouse_id, 'warehouseId' => (string) $inventory->warehouse_id,
'productId' => (string) $inventory->product_id, 'productId' => (string) $inventory->product_id,
'productName' => $inventory->product->name, 'productName' => $inventory->product?->name ?? '未知商品',
'quantity' => (float) $inventory->quantity, 'quantity' => (float) $inventory->quantity,
'batchNumber' => 'BATCH-' . $inventory->id, // Mock 'batchNumber' => 'BATCH-' . $inventory->id, // Mock
'expiryDate' => '2099-12-31', // Mock 'expiryDate' => '2099-12-31', // Mock
@@ -315,8 +318,8 @@ class InventoryController extends Controller
'warehouse' => $warehouse, 'warehouse' => $warehouse,
'inventory' => [ 'inventory' => [
'id' => (string) $inventory->id, 'id' => (string) $inventory->id,
'productName' => $inventory->product->name, 'productName' => $inventory->product?->name ?? '未知商品',
'productCode' => $inventory->product->code, 'productCode' => $inventory->product?->code ?? 'N/A',
'quantity' => (float) $inventory->quantity, 'quantity' => (float) $inventory->quantity,
], ],
'transactions' => $transactions 'transactions' => $transactions

View File

@@ -54,7 +54,7 @@ class PurchaseOrderController extends Controller
public function create() 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 [ return [
'id' => (string) $vendor->id, 'id' => (string) $vendor->id,
'name' => $vendor->name, 'name' => $vendor->name,
@@ -62,9 +62,11 @@ class PurchaseOrderController extends Controller
return [ return [
'productId' => (string) $product->id, 'productId' => (string) $product->id,
'productName' => $product->name, 'productName' => $product->name,
'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), // 優先使用採購單位 > 大單位 > 基本單位 'base_unit_id' => $product->base_unit_id,
'base_unit' => $product->base_unit, 'base_unit_name' => $product->baseUnit?->name,
'purchase_unit' => $product->purchase_unit ?: $product->large_unit, // 若無採購單位,預設為大單位 '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, 'conversion_rate' => (float) $product->conversion_rate,
'lastPrice' => (float) ($product->pivot->last_price ?? 0), 'lastPrice' => (float) ($product->pivot->last_price ?? 0),
]; ];
@@ -96,6 +98,7 @@ class PurchaseOrderController extends Controller
'items.*.productId' => 'required|exists:products,id', 'items.*.productId' => 'required|exists:products,id',
'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.quantity' => 'required|numeric|min:0.01',
'items.*.unitPrice' => 'required|numeric|min:0', 'items.*.unitPrice' => 'required|numeric|min:0',
'items.*.unitId' => 'nullable|exists:units,id', // 驗證單位ID
]); ]);
try { try {
@@ -157,6 +160,7 @@ class PurchaseOrderController extends Controller
$order->items()->create([ $order->items()->create([
'product_id' => $item['productId'], 'product_id' => $item['productId'],
'quantity' => $item['quantity'], 'quantity' => $item['quantity'],
'unit_id' => $item['unitId'] ?? null, // 儲存單位ID
'unit_price' => $item['unitPrice'], 'unit_price' => $item['unitPrice'],
'subtotal' => $item['quantity'] * $item['unitPrice'], 'subtotal' => $item['quantity'] * $item['unitPrice'],
]); ]);
@@ -174,20 +178,39 @@ class PurchaseOrderController extends Controller
public function show($id) 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) use ($order) {
$order->items->transform(function ($item) {
$product = $item->product; $product = $item->product;
if ($product) { if ($product) {
// 手動附加 productName 和 unit (因為已從 $appends 移除) // 手動附加所有必要的屬性
$item->productId = (string) $product->id;
$item->productName = $product->name; $item->productName = $product->name;
$item->productId = $product->id; $item->base_unit_id = $product->base_unit_id;
$item->base_unit = $product->base_unit; $item->base_unit_name = $product->baseUnit?->name;
$item->purchase_unit = $product->purchase_unit ?: $product->large_unit; // Fallback logic same as Create $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->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; $item->unitPrice = (float) $item->unit_price;
} }
return $item; return $item;
@@ -202,7 +225,7 @@ class PurchaseOrderController extends Controller
{ {
$order = PurchaseOrder::with(['items.product'])->findOrFail($id); $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 [ return [
'id' => (string) $vendor->id, 'id' => (string) $vendor->id,
'name' => $vendor->name, 'name' => $vendor->name,
@@ -210,9 +233,11 @@ class PurchaseOrderController extends Controller
return [ return [
'productId' => (string) $product->id, 'productId' => (string) $product->id,
'productName' => $product->name, 'productName' => $product->name,
'unit' => $product->purchase_unit ?: ($product->large_unit ?: $product->base_unit), 'base_unit_id' => $product->base_unit_id,
'base_unit' => $product->base_unit, 'base_unit_name' => $product->baseUnit?->name,
'purchase_unit' => $product->purchase_unit ?: $product->large_unit, '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, 'conversion_rate' => (float) $product->conversion_rate,
'lastPrice' => (float) ($product->pivot->last_price ?? 0), 'lastPrice' => (float) ($product->pivot->last_price ?? 0),
]; ];
@@ -228,17 +253,38 @@ class PurchaseOrderController extends Controller
}); });
// Transform items for frontend form // 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; $product = $item->product;
if ($product) { if ($product) {
// 手動附加所有必要的屬性 (因為已從 $appends 移除) // 手動附加所有必要的屬性
$item->productId = (string) $product->id; // Ensure consistent ID type $item->productId = (string) $product->id;
$item->productName = $product->name; $item->productName = $product->name;
$item->base_unit = $product->base_unit; $item->base_unit_id = $product->base_unit_id;
$item->purchase_unit = $product->purchase_unit ?: $product->large_unit; $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->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; $item->unitPrice = (float) $item->unit_price;
} }
return $item; return $item;
@@ -265,6 +311,7 @@ class PurchaseOrderController extends Controller
'items.*.productId' => 'required|exists:products,id', 'items.*.productId' => 'required|exists:products,id',
'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.quantity' => 'required|numeric|min:0.01',
'items.*.unitPrice' => 'required|numeric|min:0', 'items.*.unitPrice' => 'required|numeric|min:0',
'items.*.unitId' => 'nullable|exists:units,id', // 驗證單位ID
]); ]);
try { try {
@@ -296,6 +343,7 @@ class PurchaseOrderController extends Controller
$order->items()->create([ $order->items()->create([
'product_id' => $item['productId'], 'product_id' => $item['productId'],
'quantity' => $item['quantity'], 'quantity' => $item['quantity'],
'unit_id' => $item['unitId'] ?? null, // 儲存單位ID
'unit_price' => $item['unitPrice'], 'unit_price' => $item['unitPrice'],
'subtotal' => $item['quantity'] * $item['unitPrice'], 'subtotal' => $item['quantity'] * $item['unitPrice'],
]); ]);

View File

@@ -18,7 +18,7 @@ class SafetyStockController extends Controller
{ {
$warehouse->load(['inventories.product.category']); $warehouse->load(['inventories.product.category']);
$allProducts = Product::with('category')->get(); $allProducts = Product::with(['category', 'baseUnit'])->get();
// 準備可選商品列表 // 準備可選商品列表
$availableProducts = $allProducts->map(function ($product) { $availableProducts = $allProducts->map(function ($product) {
@@ -26,7 +26,7 @@ class SafetyStockController extends Controller
'id' => (string) $product->id, 'id' => (string) $product->id,
'name' => $product->name, 'name' => $product->name,
'type' => $product->category ? $product->category->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, 'productName' => $inv->product->name,
'productType' => $inv->product->category ? $inv->product->category->name : '其他', 'productType' => $inv->product->category ? $inv->product->category->name : '其他',
'safetyStock' => (float) $inv->safety_stock, 'safetyStock' => (float) $inv->safety_stock,
'unit' => $inv->product->base_unit, 'unit' => $inv->product->baseUnit?->name ?? '個',
'updatedAt' => $inv->updated_at->toIso8601String(), 'updatedAt' => $inv->updated_at->toIso8601String(),
]; ];
})->values(); })->values();

View File

@@ -97,7 +97,7 @@ class TransferOrderController extends Controller
public function getWarehouseInventories(Warehouse $warehouse) public function getWarehouseInventories(Warehouse $warehouse)
{ {
$inventories = $warehouse->inventories() $inventories = $warehouse->inventories()
->with(['product:id,name,base_unit,category_id', 'product.category']) ->with(['product.baseUnit', 'product.category'])
->where('quantity', '>', 0) // 只回傳有庫存的 ->where('quantity', '>', 0) // 只回傳有庫存的
->get() ->get()
->map(function ($inv) { ->map(function ($inv) {
@@ -106,7 +106,7 @@ class TransferOrderController extends Controller
'productName' => $inv->product->name, 'productName' => $inv->product->name,
'batchNumber' => 'BATCH-' . $inv->id, // 模擬批號 'batchNumber' => 'BATCH-' . $inv->id, // 模擬批號
'availableQty' => (float) $inv->quantity, 'availableQty' => (float) $inv->quantity,
'unit' => $inv->product->base_unit, 'unit' => $inv->product->baseUnit?->name ?? '個',
]; ];
}); });

View File

@@ -51,10 +51,10 @@ class VendorController extends Controller
*/ */
public function show(Vendor $vendor): \Inertia\Response public function show(Vendor $vendor): \Inertia\Response
{ {
$vendor->load('products'); $vendor->load(['products.baseUnit', 'products.largeUnit']);
return \Inertia\Inertia::render('Vendor/Show', [ return \Inertia\Inertia::render('Vendor/Show', [
'vendor' => $vendor, 'vendor' => $vendor,
'products' => \App\Models\Product::all(), 'products' => \App\Models\Product::with('baseUnit')->get(),
]); ]);
} }

View File

@@ -72,9 +72,25 @@ class WarehouseController extends Controller
public function destroy(Warehouse $warehouse) 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', '倉庫及其庫存與紀錄已刪除');
} }
} }

View File

@@ -14,6 +14,7 @@ class PurchaseOrderItem extends Model
'purchase_order_id', 'purchase_order_id',
'product_id', 'product_id',
'quantity', 'quantity',
'unit_id', // 新增單位ID欄位
'unit_price', 'unit_price',
'subtotal', 'subtotal',
'received_quantity', 'received_quantity',
@@ -26,25 +27,33 @@ class PurchaseOrderItem extends Model
'received_quantity' => 'decimal:2', 'received_quantity' => 'decimal:2',
]; ];
// 移除 $appends 以避免自動附加導致的錯誤
// 這些屬性將在 Controller 中需要時手動附加
// protected $appends = ['productName', 'unit'];
public function getProductNameAttribute(): string public function getProductNameAttribute(): string
{ {
return $this->product?->name ?? ''; return $this->product?->name ?? '';
} }
public function getUnitAttribute(): string // 關聯單位
public function unit(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{ {
// 優先使用採購單位 > 大單位 > 基本單位 return $this->belongsTo(Unit::class);
// 與 PurchaseOrderController 的邏輯保持一致 }
public function getUnitNameAttribute(): string
{
// 優先使用關聯的 unit
if ($this->unit) {
return $this->unit->name;
}
if (!$this->product) { if (!$this->product) {
return ''; return '';
} }
return $this->product->purchase_unit // Fallback: 嘗試從 Product 的關聯單位獲取
?: ($this->product->large_unit ?: $this->product->base_unit); return $this->product->purchaseUnit?->name
?? $this->product->largeUnit?->name
?? $this->product->baseUnit?->name
?? '';
} }
public function purchaseOrder(): BelongsTo public function purchaseOrder(): BelongsTo

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('purchase_order_items', function (Blueprint $table) {
$table->string('unit')->nullable()->after('quantity')->comment('採購單位');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('purchase_order_items', function (Blueprint $table) {
$table->dropColumn('unit');
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('purchase_order_items', function (Blueprint $table) {
$table->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');
});
}
};

View File

@@ -244,26 +244,6 @@ export default function ProductDialog({
{errors.conversion_rate && <p className="text-sm text-red-500">{errors.conversion_rate}</p>} {errors.conversion_rate && <p className="text-sm text-red-500">{errors.conversion_rate}</p>}
</div> </div>
<div className="space-y-2">
<Label htmlFor="purchase_unit_id"></Label>
<Select
value={data.purchase_unit_id}
onValueChange={(value) => setData("purchase_unit_id", value)}
>
<SelectTrigger id="purchase_unit_id" className={errors.purchase_unit_id ? "border-red-500" : ""}>
<SelectValue placeholder="通常同大單位" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
{units.map((unit) => (
<SelectItem key={unit.id} value={unit.id.toString()}>
{unit.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.purchase_unit_id && <p className="text-sm text-red-500">{errors.purchase_unit_id}</p>}
</div>
</div> </div>
{data.large_unit_id && data.base_unit_id && data.conversion_rate && ( {data.large_unit_id && data.base_unit_id && data.conversion_rate && (

View File

@@ -21,7 +21,7 @@ import {
TableRow, TableRow,
} from "@/Components/ui/table"; } from "@/Components/ui/table";
import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order"; import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order";
import { isPriceAlert, formatCurrency } from "@/utils/purchase-order"; import { formatCurrency } from "@/utils/purchase-order";
interface PurchaseOrderItemsTableProps { interface PurchaseOrderItemsTableProps {
items: PurchaseOrderItem[]; items: PurchaseOrderItem[];
@@ -46,12 +46,13 @@ export function PurchaseOrderItemsTable({
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-gray-50 hover:bg-gray-50"> <TableRow className="bg-gray-50 hover:bg-gray-50">
<TableHead className="w-[25%] text-left"></TableHead> <TableHead className="w-[20%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead> <TableHead className="w-[10%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead> <TableHead className="w-[12%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead> <TableHead className="w-[12%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead> <TableHead className="w-[12%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead> <TableHead className="w-[12%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"> / </TableHead>
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>} {!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -59,137 +60,184 @@ export function PurchaseOrderItemsTable({
{items.length === 0 ? ( {items.length === 0 ? (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={isReadOnly ? 6 : 7} colSpan={isReadOnly ? 7 : 8}
className="text-center text-gray-400 py-12 italic" className="text-center text-gray-400 py-12 italic"
> >
{isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"} {isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"}
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
items.map((item, index) => ( items.map((item, index) => {
<TableRow key={index}> // 計算換算後的單價 (基本單位單價)
{/* 商品選擇 */} const convertedUnitPrice = item.selectedUnit === 'large' && item.conversion_rate
<TableCell> ? item.unitPrice / item.conversion_rate
{isReadOnly ? ( : item.unitPrice;
<span className="font-medium">{item.productName}</span>
) : (
<Select
value={item.productId}
onValueChange={(value) =>
onItemChange?.(index, "productId", value)
}
disabled={isDisabled}
>
<SelectTrigger className="h-10 border-gray-200">
<SelectValue placeholder="選擇商品" />
</SelectTrigger>
<SelectContent>
{supplier?.commonProducts.map((product) => (
<SelectItem key={product.productId} value={product.productId}>
{product.productName}
</SelectItem>
))}
{(!supplier || supplier.commonProducts.length === 0) && (
<div className="p-2 text-sm text-gray-400 text-center"></div>
)}
</SelectContent>
</Select>
)}
</TableCell>
{/* 數量 */} return (
<TableCell className="text-left"> <TableRow key={index}>
{isReadOnly ? ( {/* 商品選擇 */}
<span>{Math.floor(item.quantity)}</span> <TableCell>
) : ( {isReadOnly ? (
<Input <span className="font-medium">{item.productName}</span>
type="number" ) : (
min="0" <Select
step="1" value={item.productId}
value={item.quantity === 0 ? "" : Math.floor(item.quantity)} onValueChange={(value) =>
onChange={(e) => onItemChange?.(index, "productId", value)
onItemChange?.(index, "quantity", Math.floor(Number(e.target.value))) }
} disabled={isDisabled}
disabled={isDisabled} >
className="h-10 text-left border-gray-200 w-24" <SelectTrigger className="h-10 border-gray-200">
/> <SelectValue placeholder="選擇商品" />
)} </SelectTrigger>
</TableCell> <SelectContent>
{supplier?.commonProducts.map((product) => (
<SelectItem key={product.productId} value={product.productId}>
{product.productName}
</SelectItem>
))}
{(!supplier || supplier.commonProducts.length === 0) && (
<div className="p-2 text-sm text-gray-400 text-center"></div>
)}
</SelectContent>
</Select>
)}
</TableCell>
{/* 採購單位 */} {/* 數量 */}
<TableCell> <TableCell className="text-left">
<span className="text-gray-500 font-medium">{item.unit || "-"}</span> {isReadOnly ? (
</TableCell> <span>{Math.floor(item.quantity)}</span>
) : (
{/* 換算基本單位 */}
<TableCell>
<span className="text-gray-500 font-medium">
{item.conversion_rate && item.base_unit
? `${parseFloat((item.quantity * item.conversion_rate).toFixed(2))} ${item.base_unit}`
: "-"}
</span>
</TableCell>
{/* 單價 */}
<TableCell className="text-left">
{isReadOnly ? (
<span className="font-medium text-gray-900">{formatCurrency(item.unitPrice)}</span>
) : (
<div className="space-y-1">
<Input <Input
type="number" type="number"
min="0" min="0"
step="0.1" step="1"
value={item.unitPrice || ""} value={item.quantity === 0 ? "" : Math.floor(item.quantity)}
onChange={(e) => onChange={(e) =>
onItemChange?.(index, "unitPrice", Number(e.target.value)) onItemChange?.(index, "quantity", Math.floor(Number(e.target.value)))
} }
disabled={isDisabled} disabled={isDisabled}
className={`h-10 text-left w-32 ${ className="h-10 text-left border-gray-200 w-24"
// 如果有數量但沒有單價,顯示錯誤樣式
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"
}`}
/> />
{/* 錯誤提示:有數量但沒有單價 */} )}
{item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && ( </TableCell>
<p className="text-[10px] text-red-600 font-medium">
{/* 單位選擇 */}
</p> <TableCell>
)} {!isReadOnly && item.large_unit_id ? (
{/* 價格警示:單價高於上次 */} <Select
{item.unitPrice > 0 && isPriceAlert(item.unitPrice, item.previousPrice) && ( value={item.selectedUnit || 'base'}
<p className="text-[10px] text-amber-600 font-medium animate-pulse"> onValueChange={(value) =>
: {formatCurrency(item.previousPrice || 0)} onItemChange?.(index, "selectedUnit", value)
</p> }
disabled={isDisabled}
>
<SelectTrigger className="h-10 border-gray-200 w-24">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[9999]">
<SelectItem value="base">{item.base_unit_name || "個"}</SelectItem>
<SelectItem value="large">{item.large_unit_name}</SelectItem>
</SelectContent>
</Select>
) : (
<span className="text-gray-500 font-medium">
{item.selectedUnit === 'large' && item.large_unit_name
? item.large_unit_name
: (item.base_unit_name || "個")}
</span>
)}
</TableCell>
{/* 換算基本單位 */}
<TableCell>
<div className="text-gray-700 font-medium">
<span>
{item.selectedUnit === 'large' && item.conversion_rate
? item.quantity * item.conversion_rate
: item.quantity}
</span>
<span className="ml-1 text-gray-500 text-sm">{item.base_unit_name || "個"}</span>
</div>
</TableCell>
{/* 單價 */}
<TableCell className="text-left">
{isReadOnly ? (
<span className="font-medium text-gray-900">{formatCurrency(item.unitPrice)}</span>
) : (
<div className="space-y-1">
<Input
type="number"
min="0"
step="0.1"
value={item.unitPrice || ""}
onChange={(e) =>
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) && (
<p className="text-[10px] text-red-600 font-medium">
</p>
)}
</div>
)}
</TableCell>
{/* 小計 */}
<TableCell className="text-left">
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
</TableCell>
{/* 換算採購單價 / 基本單位 */}
<TableCell className="text-left">
<div className="flex flex-col">
<div className="text-gray-500 font-medium text-sm">
{formatCurrency(convertedUnitPrice)} / {item.base_unit_name || "個"}
</div>
{convertedUnitPrice > 0 && item.previousPrice && item.previousPrice > 0 && (
<>
{convertedUnitPrice > item.previousPrice && (
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
: {formatCurrency(item.previousPrice)}
</p>
)}
{convertedUnitPrice < item.previousPrice && (
<p className="text-[10px] text-green-600 font-medium">
📉 : {formatCurrency(item.previousPrice)}
</p>
)}
</>
)} )}
</div> </div>
)}
</TableCell>
{/* 小計 */}
<TableCell className="text-left">
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
</TableCell>
{/* 刪除按鈕 */}
{!isReadOnly && onRemoveItem && (
<TableCell className="text-center">
<Button
variant="ghost"
size="icon"
onClick={() => onRemoveItem(index)}
className="h-8 w-8 text-gray-300 hover:text-red-500 hover:bg-red-50 transition-colors"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell> </TableCell>
)}
</TableRow> {/* 刪除按鈕 */}
)) {!isReadOnly && onRemoveItem && (
<TableCell className="text-center">
<Button
variant="ghost"
size="icon"
onClick={() => onRemoveItem(index)}
className="h-8 w-8 text-gray-300 hover:text-red-500 hover:bg-red-50 transition-colors"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
)}
</TableRow>
);
})
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -53,8 +53,8 @@ export default function AddSupplyProductDialog({
// 過濾掉已經在供貨列表中的商品 // 過濾掉已經在供貨列表中的商品
const availableProducts = useMemo(() => { const availableProducts = useMemo(() => {
const existingIds = new Set(existingSupplyProducts.map(sp => sp.productId)); const existingIds = new Set(existingSupplyProducts.map(sp => String(sp.productId)));
return products.filter(p => !existingIds.has(p.id)); return products.filter(p => !existingIds.has(String(p.id)));
}, [products, existingSupplyProducts]); }, [products, existingSupplyProducts]);
const selectedProduct = availableProducts.find(p => p.id === selectedProductId); const selectedProduct = availableProducts.find(p => p.id === selectedProductId);
@@ -105,7 +105,7 @@ export default function AddSupplyProductDialog({
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[450px] p-0 shadow-lg border-2" align="start"> <PopoverContent className="w-[450px] p-0 shadow-lg border-2 z-[9999]" align="start">
<Command> <Command>
<CommandInput placeholder="搜尋商品名稱..." /> <CommandInput placeholder="搜尋商品名稱..." />
<CommandList className="max-h-[300px]"> <CommandList className="max-h-[300px]">
@@ -132,7 +132,7 @@ export default function AddSupplyProductDialog({
<div className="flex items-center justify-between flex-1"> <div className="flex items-center justify-between flex-1">
<span className="font-medium">{product.name}</span> <span className="font-medium">{product.name}</span>
<span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded"> <span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded">
{product.purchase_unit || product.base_unit || "個"} {product.baseUnit?.name || (product.base_unit as any)?.name || product.base_unit || "個"}
</span> </span>
</div> </div>
</CommandItem> </CommandItem>
@@ -146,15 +146,17 @@ export default function AddSupplyProductDialog({
{/* 單位(自動帶入) */} {/* 單位(自動帶入) */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label className="text-sm font-medium text-gray-500"></Label> <Label className="text-sm font-medium text-gray-500"></Label>
<div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-gray-600 font-medium text-sm flex items-center"> <div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-gray-600 font-medium text-sm flex items-center">
{selectedProduct ? (selectedProduct.purchase_unit || selectedProduct.base_unit || "個") : "-"} {selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "-"}
</div> </div>
</div> </div>
{/* 上次採購價格 */} {/* 上次採購價格 */}
<div> <div>
<Label className="text-muted-foreground text-xs"></Label> <Label className="text-muted-foreground text-xs">
/ {selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "單位"}
</Label>
<Input <Input
type="number" type="number"
placeholder="輸入價格" placeholder="輸入價格"

View File

@@ -83,7 +83,7 @@ export default function EditSupplyProductDialog({
{/* 上次採購價格 */} {/* 上次採購價格 */}
<div> <div>
<Label className="text-muted-foreground text-xs"></Label> <Label className="text-muted-foreground text-xs"> / {product.baseUnit || "單位"}</Label>
<Input <Input
type="number" type="number"
placeholder="輸入價格" placeholder="輸入價格"

View File

@@ -28,15 +28,19 @@ export default function SupplyProductList({
<TableRow> <TableRow>
<TableHead className="w-[50px] text-center">#</TableHead> <TableHead className="w-[50px] text-center">#</TableHead>
<TableHead className="font-semibold"></TableHead> <TableHead className="font-semibold"></TableHead>
<TableHead className="font-semibold"></TableHead> <TableHead className="font-semibold"></TableHead>
<TableHead className="text-right font-semibold"></TableHead> <TableHead className="font-semibold"></TableHead>
<TableHead className="text-right font-semibold">
<div className="text-xs font-normal text-muted-foreground">()</div>
</TableHead>
<TableHead className="text-center font-semibold w-[150px]"></TableHead> <TableHead className="text-center font-semibold w-[150px]"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{products.length === 0 ? ( {products.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-8"> <TableCell colSpan={6} className="text-center text-muted-foreground py-8">
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -47,9 +51,26 @@ export default function SupplyProductList({
{index + 1} {index + 1}
</TableCell> </TableCell>
<TableCell>{product.productName}</TableCell> <TableCell>{product.productName}</TableCell>
<TableCell>{product.unit}</TableCell> <TableCell>
{product.baseUnit || product.unit || "-"}
</TableCell>
<TableCell>
{product.largeUnit && product.conversionRate ? (
<span className="text-sm text-gray-500">
1 {product.largeUnit} = {Number(product.conversionRate)} {product.baseUnit || product.unit}
</span>
) : (
"-"
)}
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{product.lastPrice ? `$${product.lastPrice.toLocaleString()}` : "-"} {product.lastPrice ? (
<span>
${product.lastPrice.toLocaleString()} / {product.baseUnit || product.unit || "單位"}
</span>
) : (
"-"
)}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">

View File

@@ -5,7 +5,7 @@
*/ */
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getCurrentDateTime, generateOrderNumber } from "@/utils/format"; import { getCurrentDateTime } from "@/utils/format";
import axios from "axios"; import axios from "axios";
import { import {
Dialog, Dialog,
@@ -46,6 +46,7 @@ interface AvailableProduct {
productName: string; productName: string;
batchNumber: string; batchNumber: string;
availableQty: number; availableQty: number;
unit: string;
} }
export default function TransferOrderDialog({ export default function TransferOrderDialog({
@@ -276,7 +277,7 @@ export default function TransferOrderDialog({
value={`${product.productId}|||${product.batchNumber}`} value={`${product.productId}|||${product.batchNumber}`}
> >
{product.productName} (:{" "} {product.productName} (:{" "}
{product.availableQty}) {product.availableQty} {product.unit})
</SelectItem> </SelectItem>
)) ))
)} )}
@@ -303,7 +304,7 @@ export default function TransferOrderDialog({
<div className="h-5"> <div className="h-5">
{selectedProduct && ( {selectedProduct && (
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
: {selectedProduct.availableQty} : {selectedProduct.availableQty} {selectedProduct.unit}
</p> </p>
)} )}
</div> </div>

View File

@@ -9,6 +9,7 @@ import { Head, Link } from "@inertiajs/react";
import { StatusProgressBar } from "@/Components/PurchaseOrder/StatusProgressBar"; import { StatusProgressBar } from "@/Components/PurchaseOrder/StatusProgressBar";
import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderStatusBadge"; import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderStatusBadge";
import CopyButton from "@/Components/shared/CopyButton"; import CopyButton from "@/Components/shared/CopyButton";
import { PurchaseOrderItemsTable } from "@/Components/PurchaseOrder/PurchaseOrderItemsTable";
import type { PurchaseOrder } from "@/types/purchase-order"; import type { PurchaseOrder } from "@/types/purchase-order";
import { formatCurrency, formatDateTime } from "@/utils/format"; import { formatCurrency, formatDateTime } from "@/utils/format";
import { getShowBreadcrumbs } from "@/utils/breadcrumb"; import { getShowBreadcrumbs } from "@/utils/breadcrumb";
@@ -104,66 +105,17 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
<div className="p-6 border-b border-gray-100"> <div className="p-6 border-b border-gray-100">
<h2 className="text-lg font-bold text-gray-900"></h2> <h2 className="text-lg font-bold text-gray-900"></h2>
</div> </div>
<div className="overflow-x-auto"> <div className="p-6">
<table className="w-full"> <PurchaseOrderItemsTable
<thead> items={order.items}
<tr className="bg-gray-50/50"> isReadOnly={true}
<th className="text-left py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider w-[50px]"> />
# <div className="mt-4 flex justify-end items-center gap-4 border-t pt-4">
</th> <span className="text-gray-600 font-medium"></span>
<th className="text-left py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider"> <span className="text-xl font-bold text-primary">
{formatCurrency(order.totalAmount)}
</th> </span>
<th className="text-right py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider"> </div>
</th>
<th className="text-right py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider w-32">
</th>
<th className="text-right py-4 px-6 text-xs font-semibold text-gray-500 uppercase tracking-wider">
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{order.items.map((item, index) => (
<tr key={index} className="hover:bg-gray-50/30 transition-colors">
<td className="py-4 px-6 text-gray-500 font-medium text-center">
{index + 1}
</td>
<td className="py-4 px-6">
<div className="flex flex-col">
<span className="font-medium text-gray-900">{item.productName}</span>
<span className="text-xs text-gray-400">ID: {item.productId}</span>
</div>
</td>
<td className="py-4 px-6 text-right">
<div className="flex flex-col items-end">
<span className="text-gray-900">{formatCurrency(item.unitPrice)}</span>
</div>
</td>
<td className="py-4 px-6 text-right">
<span className="text-gray-900 font-medium">
{item.quantity} {item.unit}
</span>
</td>
<td className="py-4 px-6 text-right font-bold text-gray-900">
{formatCurrency(item.subtotal)}
</td>
</tr>
))}
</tbody>
<tfoot className="bg-gray-50/50 border-t border-gray-100">
<tr>
<td colSpan={4} className="py-5 px-6 text-right font-medium text-gray-600">
</td>
<td className="py-5 px-6 text-right font-bold text-xl text-primary">
{formatCurrency(order.totalAmount)}
</td>
</tr>
</tfoot>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -33,8 +33,14 @@ interface VendorProduct {
id: number; id: number;
name: string; name: string;
unit?: 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; purchase_unit?: string;
conversion_rate?: number;
pivot: Pivot; pivot: Pivot;
} }
@@ -54,13 +60,29 @@ export default function VendorShow({ vendor, products }: ShowProps) {
const [selectedProduct, setSelectedProduct] = useState<SupplyProduct | null>(null); const [selectedProduct, setSelectedProduct] = useState<SupplyProduct | null>(null);
// 轉換後端資料格式為前端組件需要的格式 // 轉換後端資料格式為前端組件需要的格式
const supplyProducts: SupplyProduct[] = vendor.products.map(p => ({ const supplyProducts: SupplyProduct[] = vendor.products.map(p => {
id: String(p.id), // Laravel load('relationName') usually results in camelCase key in JSON if method is camelCase
productId: String(p.id), const baseUnitName = p.baseUnit?.name || p.base_unit?.name;
productName: p.name, const largeUnitName = p.largeUnit?.name || p.large_unit?.name;
unit: p.purchase_unit || p.base_unit || "個",
lastPrice: p.pivot.last_price || undefined, // 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) => { const handleAddProduct = (productId: string, lastPrice?: number) => {
router.post(route('vendors.products.store', vendor.id), { router.post(route('vendors.products.store', vendor.id), {

View File

@@ -33,7 +33,9 @@ import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
interface Product { interface Product {
id: string; id: string;
name: string; name: string;
unit: string; baseUnit: string;
largeUnit?: string;
conversionRate?: number;
} }
interface Props { interface Props {
@@ -58,13 +60,17 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
// 新增明細行 // 新增明細行
const handleAddItem = () => { 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 = { const newItem: InboundItem = {
tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
productId: defaultProduct.id, productId: defaultProduct.id,
productName: defaultProduct.name, productName: defaultProduct.name,
quantity: 0, quantity: 0,
unit: defaultProduct.unit, unit: defaultProduct.baseUnit, // 僅用於顯示當前選擇單位的名稱
baseUnit: defaultProduct.baseUnit,
largeUnit: defaultProduct.largeUnit,
conversionRate: defaultProduct.conversionRate,
selectedUnit: 'base',
}; };
setItems([...items, newItem]); setItems([...items, newItem]);
}; };
@@ -86,11 +92,16 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
// 處理商品變更 // 處理商品變更
const handleProductChange = (tempId: string, productId: string) => { const handleProductChange = (tempId: string, productId: string) => {
const product = products.find((p) => p.id === productId); const product = products.find((p) => p.id === productId);
if (product) { if (product) {
handleUpdateItem(tempId, { handleUpdateItem(tempId, {
productId, productId,
productName: product.name, 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, inboundDate,
reason, reason,
notes, notes,
items: items.map(item => ({ items: items.map(item => {
productId: item.productId, // 如果選擇大單位,則換算為基本單位數量
quantity: item.quantity const finalQuantity = item.selectedUnit === 'large' && item.conversionRate
})) ? item.quantity * item.conversionRate
: item.quantity;
return {
productId: item.productId,
quantity: finalQuantity
};
})
}, { }, {
onSuccess: () => { onSuccess: () => {
toast.success("庫存記錄已儲存"); toast.success("庫存記錄已儲存");
@@ -296,71 +314,106 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
<span className="text-red-500">*</span> <span className="text-red-500">*</span>
</TableHead> </TableHead>
<TableHead className="w-[100px]"></TableHead> <TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
{/* <TableHead className="w-[180px]">效期</TableHead> {/* <TableHead className="w-[180px]">效期</TableHead>
<TableHead className="w-[220px]">進貨編號</TableHead> */} <TableHead className="w-[220px]">進貨編號</TableHead> */}
<TableHead className="w-[60px]"></TableHead> <TableHead className="w-[60px]"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{items.map((item, index) => ( {items.map((item, index) => {
<TableRow key={item.tempId}> // 計算轉換數量
{/* 商品 */} const convertedQuantity = item.selectedUnit === 'large' && item.conversionRate
<TableCell> ? item.quantity * item.conversionRate
<Select : item.quantity;
value={item.productId}
onValueChange={(value) =>
handleProductChange(item.tempId, value)
}
>
<SelectTrigger className="border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent>
{products.map((product) => (
<SelectItem key={product.id} value={product.id}>
{product.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors[`item-${index}-product`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-product`]}
</p>
)}
</TableCell>
{/* 數量 */} return (
<TableCell> <TableRow key={item.tempId}>
<Input {/* 商品 */}
type="number" <TableCell>
min="1" <Select
value={item.quantity || ""} value={item.productId}
onChange={(e) => onValueChange={(value) =>
handleUpdateItem(item.tempId, { handleProductChange(item.tempId, value)
quantity: parseInt(e.target.value) || 0, }
}) >
} <SelectTrigger className="border-gray-300">
className="border-gray-300" <SelectValue />
/> </SelectTrigger>
{errors[`item-${index}-quantity`] && ( <SelectContent className="z-[9999]">
<p className="text-xs text-red-500 mt-1"> {products.map((product) => (
{errors[`item-${index}-quantity`]} <SelectItem key={product.id} value={product.id}>
</p> {product.name}
)} </SelectItem>
</TableCell> ))}
</SelectContent>
</Select>
{errors[`item-${index}-product`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-product`]}
</p>
)}
</TableCell>
{/* 單位 */} {/* 數量 */}
<TableCell> <TableCell>
<Input <Input
value={item.unit} type="number"
disabled min="1"
className="bg-gray-50 border-gray-200" value={item.quantity || ""}
/> onChange={(e) =>
</TableCell> handleUpdateItem(item.tempId, {
quantity: parseFloat(e.target.value) || 0,
})
}
className="border-gray-300"
/>
{errors[`item-${index}-quantity`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-quantity`]}
</p>
)}
</TableCell>
{/* 效期 */} {/* 單位 */}
{/* <TableCell> <TableCell>
{item.largeUnit ? (
<Select
value={item.selectedUnit}
onValueChange={(value) =>
handleUpdateItem(item.tempId, {
selectedUnit: value as 'base' | 'large',
unit: value === 'base' ? item.baseUnit : item.largeUnit
})
}
>
<SelectTrigger className="border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[9999]">
<SelectItem value="base">{item.baseUnit}</SelectItem>
<SelectItem value="large">{item.largeUnit}</SelectItem>
</SelectContent>
</Select>
) : (
<Input
value={item.baseUnit || "個"}
disabled
className="bg-gray-50 border-gray-200"
/>
)}
</TableCell>
{/* 轉換數量 */}
<TableCell>
<div className="flex items-center text-gray-700 font-medium bg-gray-50 px-3 py-2 rounded-md border border-gray-200">
<span>{convertedQuantity}</span>
<span className="ml-1 text-gray-500 text-sm">{item.baseUnit || "個"}</span>
</div>
</TableCell>
{/* 效期 */}
{/* <TableCell>
<div className="relative"> <div className="relative">
<Input <Input
type="date" type="date"
@@ -375,8 +428,8 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
</div> </div>
</TableCell> */} </TableCell> */}
{/* 批號 */} {/* 批號 */}
{/* <TableCell> {/* <TableCell>
<Input <Input
value={item.batchNumber} value={item.batchNumber}
onChange={(e) => onChange={(e) =>
@@ -392,20 +445,21 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
)} )}
</TableCell> */} </TableCell> */}
{/* 刪除按鈕 */} {/* 刪除按鈕 */}
<TableCell> <TableCell>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => handleRemoveItem(item.tempId)} onClick={() => handleRemoveItem(item.tempId)}
className="hover:bg-red-50 hover:text-red-600 h-8 w-8" className="hover:bg-red-50 hover:text-red-600 h-8 w-8"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} );
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@@ -78,9 +78,17 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) {
}; };
const handleDeleteWarehouse = (id: string) => { 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 = () => { const handleAddTransferOrder = () => {

View File

@@ -63,7 +63,7 @@ export default function WarehouseInventoryPage({
// 導航至流動紀錄頁 // 導航至流動紀錄頁
const handleView = (inventoryId: string) => { 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 = () => { const handleDelete = () => {
if (!deleteId) return; 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: () => { onSuccess: () => {
toast.success("庫存記錄已刪除"); toast.success("庫存記錄已刪除");
setDeleteId(null); setDeleteId(null);
}, },
onError: () => { onError: () => {
toast.error("刪除失敗"); toast.error("刪除失敗");
// 保持對話框開啟以便重試,或根據需要關閉
} }
}); });
}; };
@@ -112,7 +116,7 @@ export default function WarehouseInventoryPage({
{/* 操作按鈕 (位於標題下方) */} {/* 操作按鈕 (位於標題下方) */}
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
{/* 安全庫存設定按鈕 */} {/* 安全庫存設定按鈕 */}
<Link href={`/warehouses/${warehouse.id}/safety-stock-settings`}> <Link href={route('warehouses.safety-stock.index', warehouse.id)}>
<Button <Button
variant="outline" variant="outline"
className="button-outlined-primary" className="button-outlined-primary"
@@ -135,7 +139,7 @@ export default function WarehouseInventoryPage({
</Button> </Button>
{/* 新增庫存按鈕 */} {/* 新增庫存按鈕 */}
<Link href={`/warehouses/${warehouse.id}/add-inventory`}> <Link href={route('warehouses.inventory.create', warehouse.id)}>
<Button <Button
className="button-filled-primary" className="button-filled-primary"
> >
@@ -163,9 +167,6 @@ export default function WarehouseInventoryPage({
onDelete={confirmDelete} onDelete={confirmDelete}
/> />
</div> </div>
{/* 刪除確認對話框 */}
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}> <AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -176,7 +177,12 @@ export default function WarehouseInventoryPage({
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel> <AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700 text-white"> <AlertDialogAction
onClick={(e) => {
handleDelete();
}}
className="bg-red-600 hover:bg-red-700 text-white"
>
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>

View File

@@ -52,10 +52,10 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP
{ {
productId: "", productId: "",
productName: "", productName: "",
quantity: 0, quantity: 1,
unit: "",
unitPrice: 0, unitPrice: 0,
subtotal: 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]; const newItems = [...items];
newItems[index] = { ...newItems[index], [field]: value }; const item = { ...newItems[index] };
// 當選擇商品時,自動填入商品資訊
if (field === "productId" && selectedSupplier) { if (field === "productId" && selectedSupplier) {
// value is productId string
const product = selectedSupplier.commonProducts.find((p) => p.productId === value); const product = selectedSupplier.commonProducts.find((p) => p.productId === value);
if (product) { if (product) {
newItems[index].productName = product.productName; // @ts-ignore
newItems[index].unit = product.unit; item.productId = value;
newItems[index].base_unit = product.base_unit; item.productName = product.productName;
newItems[index].purchase_unit = product.purchase_unit; item.base_unit_id = product.base_unit_id;
newItems[index].conversion_rate = product.conversion_rate; item.base_unit_name = product.base_unit_name;
newItems[index].unitPrice = product.lastPrice; item.large_unit_id = product.large_unit_id;
newItems[index].previousPrice = product.lastPrice; 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") { if (field === "quantity" || field === "unitPrice" || field === "productId") {
newItems[index].subtotal = calculateSubtotal( item.subtotal = calculateSubtotal(
Number(newItems[index].quantity), Number(item.quantity),
Number(newItems[index].unitPrice) Number(item.unitPrice)
); );
} }
newItems[index] = item;
setItems(newItems); setItems(newItems);
}; };

View File

@@ -14,4 +14,5 @@ export interface Product {
conversion_rate?: number; conversion_rate?: number;
purchase_unit?: string; purchase_unit?: string;
unit?: string; // 相容舊有程式碼 unit?: string; // 相容舊有程式碼
baseUnit?: { name: string };
} }

View File

@@ -21,10 +21,14 @@ export interface PurchaseOrderItem {
productId: string; productId: string;
productName: string; productName: string;
quantity: number; quantity: number;
unit: string; unitId?: number; // 選擇的單位ID
base_unit?: string; // 基本庫存單位 base_unit_id?: number;
purchase_unit?: string; // 採購單位 base_unit_name?: string;
conversion_rate?: number;// 換算率 large_unit_id?: number;
large_unit_name?: string;
purchase_unit_id?: number;
conversion_rate?: number;
selectedUnit?: 'base' | 'large'; // 前端狀態輔助
unitPrice: number; unitPrice: number;
previousPrice?: number; previousPrice?: number;
subtotal: number; subtotal: number;
@@ -79,9 +83,11 @@ export interface PurchaseOrder {
export interface CommonProduct { export interface CommonProduct {
productId: string; productId: string;
productName: string; productName: string;
unit: string; base_unit_id?: number;
base_unit?: string; base_unit_name?: string;
purchase_unit?: string; large_unit_id?: number;
large_unit_name?: string;
purchase_unit_id?: number;
conversion_rate?: number; conversion_rate?: number;
lastPrice: number; lastPrice: number;
} }

View File

@@ -8,6 +8,9 @@ export interface SupplyProduct {
productId: string; productId: string;
productName: string; productName: string;
unit: string; unit: string;
baseUnit?: string;
largeUnit?: string;
conversionRate?: number;
lastPrice?: number; lastPrice?: number;
} }

View File

@@ -147,6 +147,10 @@ export interface InboundItem {
productName: string; productName: string;
quantity: number; quantity: number;
unit: string; unit: string;
baseUnit?: string;
largeUnit?: string;
conversionRate?: number;
selectedUnit?: 'base' | 'large';
} }
/** /**