實作 InventoryService 的批量入庫 (processIncomingInventory) 與庫存調整 (adjustInventory) 邏輯
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
This commit is contained in:
@@ -165,4 +165,23 @@ interface InventoryServiceInterface
|
||||
* @return \Illuminate\Support\Collection|null
|
||||
*/
|
||||
public function getPosInventoryByWarehouseCode(string $code);
|
||||
|
||||
/**
|
||||
* 處理批量入庫邏輯 (含批號產生與現有批號累加)。
|
||||
*
|
||||
* @param \App\Modules\Inventory\Models\Warehouse $warehouse
|
||||
* @param array $items 入庫品項清單
|
||||
* @param array $meta 資料包含 inboundDate, reason, notes
|
||||
* @return void
|
||||
*/
|
||||
public function processIncomingInventory(\App\Modules\Inventory\Models\Warehouse $warehouse, array $items, array $meta): void;
|
||||
|
||||
/**
|
||||
* 處理單一庫存項目的調整。
|
||||
*
|
||||
* @param \App\Modules\Inventory\Models\Inventory $inventory
|
||||
* @param array $data 包含 quantity, operation, type, reason, unit_cost 等
|
||||
* @return void
|
||||
*/
|
||||
public function adjustInventory(\App\Modules\Inventory\Models\Inventory $inventory, array $data): void;
|
||||
}
|
||||
@@ -22,10 +22,14 @@ use App\Modules\Core\Contracts\CoreServiceInterface;
|
||||
class InventoryController extends Controller
|
||||
{
|
||||
protected $coreService;
|
||||
protected $inventoryService;
|
||||
|
||||
public function __construct(CoreServiceInterface $coreService)
|
||||
{
|
||||
public function __construct(
|
||||
CoreServiceInterface $coreService,
|
||||
\App\Modules\Inventory\Contracts\InventoryServiceInterface $inventoryService
|
||||
) {
|
||||
$this->coreService = $coreService;
|
||||
$this->inventoryService = $inventoryService;
|
||||
}
|
||||
|
||||
public function index(Request $request, Warehouse $warehouse)
|
||||
@@ -182,97 +186,11 @@ class InventoryController extends Controller
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated, $warehouse) {
|
||||
foreach ($validated['items'] as $item) {
|
||||
// ... (略,傳遞 unit_cost 交給 Service 處理) ...
|
||||
// 這裡需要修改呼叫 Service 的地方或直接更新邏輯
|
||||
// 為求快速,我將在此更新邏輯
|
||||
|
||||
$inventory = null;
|
||||
|
||||
if ($item['batchMode'] === 'existing') {
|
||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||
$inventory = Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
|
||||
// 更新成本 (若有傳入)
|
||||
if (isset($item['unit_cost'])) {
|
||||
$inventory->unit_cost = $item['unit_cost'];
|
||||
}
|
||||
} elseif ($item['batchMode'] === 'none') {
|
||||
// 模式 C:不使用批號 (自動累加至 NO-BATCH)
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => 'NO-BATCH'
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'arrival_date' => $validated['inboundDate'],
|
||||
'expiry_date' => null,
|
||||
'origin_country' => 'TW',
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
$product = Product::find($item['productId']);
|
||||
|
||||
$batchNumber = Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$validated['inboundDate']
|
||||
);
|
||||
|
||||
// 檢查是否存在
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => $batchNumber
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0, // 新增
|
||||
'total_value' => 0, // 稍後計算
|
||||
'location' => $item['location'] ?? null,
|
||||
'arrival_date' => $validated['inboundDate'],
|
||||
'expiry_date' => $item['expiryDate'] ?? null,
|
||||
'origin_country' => $originCountry,
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
}
|
||||
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $currentQty + $item['quantity'];
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
// 更新總價值
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->saveQuietly(); // 使用 saveQuietly() 避免產生冗餘的「單據已更新」日誌
|
||||
|
||||
// 寫入異動紀錄
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動入庫',
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_cost' => $inventory->unit_cost, // 記錄成本
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $validated['reason'] . ($validated['notes'] ? ' - ' . $validated['notes'] : ''),
|
||||
'actual_time' => $validated['inboundDate'],
|
||||
'user_id' => auth()->id(),
|
||||
$this->inventoryService->processIncomingInventory($warehouse, $validated['items'], [
|
||||
'inboundDate' => $validated['inboundDate'],
|
||||
'reason' => $validated['reason'],
|
||||
'notes' => $validated['notes'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
||||
->with('success', '庫存記錄已儲存成功');
|
||||
@@ -401,81 +319,7 @@ class InventoryController extends Controller
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($validated, $inventory) {
|
||||
$currentQty = (float) $inventory->quantity;
|
||||
$newQty = (float) $validated['quantity'];
|
||||
|
||||
// 判斷是否來自調整彈窗 (包含 operation 參數)
|
||||
$isAdjustment = isset($validated['operation']);
|
||||
$changeQty = 0;
|
||||
|
||||
if ($isAdjustment) {
|
||||
switch ($validated['operation']) {
|
||||
case 'add':
|
||||
$changeQty = (float) $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'subtract':
|
||||
$changeQty = -(float) $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'set':
|
||||
$changeQty = $newQty - $currentQty;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 來自編輯頁面,直接 Set
|
||||
$changeQty = $newQty - $currentQty;
|
||||
}
|
||||
|
||||
// 更新成本 (若有傳)
|
||||
if (isset($validated['unit_cost'])) {
|
||||
$inventory->unit_cost = $validated['unit_cost'];
|
||||
}
|
||||
|
||||
// 更新庫存
|
||||
$inventory->quantity = $newQty;
|
||||
// 更新總值
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->saveQuietly(); // 使用 saveQuietly() 避免與下方的 transaction 紀錄重複
|
||||
|
||||
// 異動類型映射
|
||||
$type = $validated['type'] ?? ($isAdjustment ? 'manual_adjustment' : 'adjustment');
|
||||
$typeMapping = [
|
||||
'manual_adjustment' => '手動調整庫存',
|
||||
'adjustment' => '盤點調整',
|
||||
'purchase_in' => '採購進貨',
|
||||
'sales_out' => '銷售出庫',
|
||||
'return_in' => '退貨入庫',
|
||||
'return_out' => '退貨出庫',
|
||||
'transfer_in' => '撥補入庫',
|
||||
'transfer_out' => '撥補出庫',
|
||||
];
|
||||
$chineseType = $typeMapping[$type] ?? $type;
|
||||
|
||||
// 如果是編輯頁面來的,且沒傳 type,設為手動編輯
|
||||
if (!$isAdjustment && !isset($validated['type'])) {
|
||||
$chineseType = '手動編輯';
|
||||
}
|
||||
|
||||
// 整理原因
|
||||
$reason = $validated['reason'] ?? ($isAdjustment ? '手動庫存調整' : '編輯頁面更新');
|
||||
if (isset($validated['notes'])) {
|
||||
$reason .= ' - ' . $validated['notes'];
|
||||
}
|
||||
|
||||
// 寫入異動紀錄
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'unit_cost' => $inventory->unit_cost, // 記錄
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $reason,
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
$this->inventoryService->adjustInventory($inventory, $validated);
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $inventory->warehouse_id)
|
||||
->with('success', '庫存資料已更新');
|
||||
|
||||
@@ -674,4 +674,168 @@ class InventoryService implements InventoryServiceInterface
|
||||
->groupBy('inventories.product_id', 'products.external_pos_id', 'products.code', 'products.name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function processIncomingInventory(Warehouse $warehouse, array $items, array $meta): void
|
||||
{
|
||||
DB::transaction(function () use ($warehouse, $items, $meta) {
|
||||
foreach ($items as $item) {
|
||||
$inventory = null;
|
||||
|
||||
if ($item['batchMode'] === 'existing') {
|
||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||
$inventory = Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
|
||||
// 更新成本 (若有傳入)
|
||||
if (isset($item['unit_cost'])) {
|
||||
$inventory->unit_cost = $item['unit_cost'];
|
||||
}
|
||||
} elseif ($item['batchMode'] === 'none') {
|
||||
// 模式 C:不使用批號 (自動累加至 NO-BATCH)
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => 'NO-BATCH'
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'arrival_date' => $meta['inboundDate'],
|
||||
'origin_country' => 'TW',
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
$product = Product::find($item['productId']);
|
||||
|
||||
$batchNumber = Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$meta['inboundDate']
|
||||
);
|
||||
|
||||
// 檢查是否存在
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => $batchNumber
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'location' => $item['location'] ?? null,
|
||||
'arrival_date' => $meta['inboundDate'],
|
||||
'expiry_date' => $item['expiryDate'] ?? null,
|
||||
'origin_country' => $originCountry,
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
}
|
||||
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $currentQty + $item['quantity'];
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
// 更新總價值
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->saveQuietly();
|
||||
|
||||
// 寫入異動紀錄
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動入庫',
|
||||
'quantity' => $item['quantity'],
|
||||
'unit_cost' => $inventory->unit_cost,
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $meta['reason'] . (!empty($meta['notes']) ? ' - ' . $meta['notes'] : ''),
|
||||
'actual_time' => $meta['inboundDate'],
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function adjustInventory(Inventory $inventory, array $data): void
|
||||
{
|
||||
DB::transaction(function () use ($inventory, $data) {
|
||||
$currentQty = (float) $inventory->quantity;
|
||||
$newQty = (float) $data['quantity'];
|
||||
|
||||
$isAdjustment = isset($data['operation']);
|
||||
$changeQty = 0;
|
||||
|
||||
if ($isAdjustment) {
|
||||
switch ($data['operation']) {
|
||||
case 'add':
|
||||
$changeQty = (float) $data['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'subtract':
|
||||
$changeQty = -(float) $data['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'set':
|
||||
$changeQty = $newQty - $currentQty;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$changeQty = $newQty - $currentQty;
|
||||
}
|
||||
|
||||
if (isset($data['unit_cost'])) {
|
||||
$inventory->unit_cost = $data['unit_cost'];
|
||||
}
|
||||
|
||||
$inventory->quantity = $newQty;
|
||||
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
|
||||
$inventory->saveQuietly();
|
||||
|
||||
$type = $data['type'] ?? ($isAdjustment ? 'manual_adjustment' : 'adjustment');
|
||||
$typeMapping = [
|
||||
'manual_adjustment' => '手動調整庫存',
|
||||
'adjustment' => '盤點調整',
|
||||
'purchase_in' => '採購進貨',
|
||||
'sales_out' => '銷售出庫',
|
||||
'return_in' => '退貨入庫',
|
||||
'return_out' => '退貨出庫',
|
||||
'transfer_in' => '撥補入庫',
|
||||
'transfer_out' => '撥補出庫',
|
||||
];
|
||||
$chineseType = $typeMapping[$type] ?? $type;
|
||||
|
||||
if (!$isAdjustment && !isset($data['type'])) {
|
||||
$chineseType = '手動編輯';
|
||||
}
|
||||
|
||||
$reason = $data['reason'] ?? ($isAdjustment ? '手動庫存調整' : '編輯頁面更新');
|
||||
if (!empty($data['notes'])) {
|
||||
$reason .= ' - ' . $data['notes'];
|
||||
}
|
||||
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'unit_cost' => $inventory->unit_cost,
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $reason,
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,15 @@ interface Props {
|
||||
|
||||
// 欄位翻譯對照表
|
||||
const fieldLabels: Record<string, string> = {
|
||||
id: 'ID',
|
||||
created_at: '建立時間',
|
||||
updated_at: '更新時間',
|
||||
deleted_at: '刪除時間',
|
||||
name: '名稱',
|
||||
code: '商品代號',
|
||||
username: '登入帳號',
|
||||
description: '描述',
|
||||
// ... (保持原有翻譯)
|
||||
price: '價格',
|
||||
cost: '成本',
|
||||
stock: '庫存',
|
||||
@@ -66,6 +71,11 @@ const fieldLabels: Record<string, string> = {
|
||||
role_id: '角色',
|
||||
email_verified_at: '電子郵件驗證時間',
|
||||
remember_token: '登入權杖',
|
||||
barcode: '條碼',
|
||||
external_pos_id: '外部 POS ID',
|
||||
cost_price: '成本價',
|
||||
member_price: '會員價',
|
||||
wholesale_price: '批發價',
|
||||
// 快照欄位
|
||||
category_name: '分類名稱',
|
||||
base_unit_name: '基本單位名稱',
|
||||
@@ -78,6 +88,9 @@ const fieldLabels: Record<string, string> = {
|
||||
contact_name: '聯絡人',
|
||||
tel: '電話',
|
||||
remark: '備註',
|
||||
license_plate: '車牌號碼',
|
||||
driver_name: '司機姓名',
|
||||
default_transit_warehouse_id: '預設在途倉庫',
|
||||
// 倉庫與庫存欄位
|
||||
warehouse_name: '倉庫名稱',
|
||||
product_name: '商品名稱',
|
||||
@@ -98,6 +111,12 @@ const fieldLabels: Record<string, string> = {
|
||||
quality_status: '品質狀態',
|
||||
quality_remark: '品質備註',
|
||||
purchase_order_id: '來源採購單',
|
||||
inventory_id: '庫存 ID',
|
||||
balance_before: '異動前餘額',
|
||||
balance_after: '異動後餘額',
|
||||
reference_type: '參考單據類型',
|
||||
reference_id: '參考單據 ID',
|
||||
actual_time: '實際時點',
|
||||
// 採購單欄位
|
||||
po_number: '採購單號',
|
||||
vendor_id: '廠商',
|
||||
@@ -173,7 +192,50 @@ const statusMap: Record<string, string> = {
|
||||
in_progress: '生產中',
|
||||
// 調撥單狀態
|
||||
voided: '已作廢',
|
||||
// completed 已定義
|
||||
};
|
||||
|
||||
// 主體類型解析 (Model 類名轉中文)
|
||||
const subjectTypeMap: Record<string, string> = {
|
||||
// 完整路徑映射
|
||||
'App\\Modules\\Inventory\\Models\\Product': '商品資料',
|
||||
'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料',
|
||||
'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動',
|
||||
'App\\Modules\\Inventory\\Models\\Category': '商品分類',
|
||||
'App\\Modules\\Inventory\\Models\\Unit': '單位',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄',
|
||||
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
|
||||
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
|
||||
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
|
||||
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
|
||||
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
|
||||
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
|
||||
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
|
||||
'App\\Modules\\Core\\Models\\User': '使用者帳號',
|
||||
'App\\Modules\\Core\\Models\\Role': '角色權限',
|
||||
// 簡寫映射 (應對後端回傳 class_basename 的情況)
|
||||
'Product': '商品資料',
|
||||
'Warehouse': '倉庫資料',
|
||||
'Inventory': '庫存異動',
|
||||
'InventoryTransaction': '庫存異動紀錄',
|
||||
'Category': '商品分類',
|
||||
'Unit': '單位',
|
||||
'Vendor': '廠商資料',
|
||||
'PurchaseOrder': '採購單',
|
||||
'GoodsReceipt': '進貨單',
|
||||
'ProductionOrder': '生產工單',
|
||||
'Recipe': '生產配方',
|
||||
'InventoryCountDoc': '庫存盤點單',
|
||||
'InventoryAdjustDoc': '庫存盤調單',
|
||||
'InventoryTransferOrder': '庫存調撥單',
|
||||
'StockMovementDoc': '庫存單據',
|
||||
'User': '使用者帳號',
|
||||
'Role': '角色權限',
|
||||
'UtilityFee': '公共事業費',
|
||||
};
|
||||
|
||||
// 庫存品質狀態對照表
|
||||
@@ -202,9 +264,10 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
|
||||
// 自訂欄位排序順序
|
||||
const sortOrder = [
|
||||
'po_number', 'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
|
||||
'doc_no', 'po_number', 'gr_number', 'production_number',
|
||||
'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
|
||||
'invoice_number', 'invoice_date', 'invoice_amount',
|
||||
'total_amount', 'tax_amount', 'grand_total' // 確保金額的特定順序
|
||||
'total_amount', 'tax_amount', 'grand_total'
|
||||
];
|
||||
|
||||
// 過濾掉通常會記錄但對使用者無用的內部鍵
|
||||
@@ -215,30 +278,22 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
.sort((a, b) => {
|
||||
const indexA = sortOrder.indexOf(a);
|
||||
const indexB = sortOrder.indexOf(b);
|
||||
|
||||
// 如果兩者都在排序順序中,比較索引
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||||
// 如果只有 A 在排序順序中,它排在前面(或根據邏輯,通常將已知欄位排在前面)
|
||||
if (indexA !== -1) return -1;
|
||||
if (indexB !== -1) return 1;
|
||||
// 否則按字母順序或預設
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
// 檢查鍵是否為快照名稱欄位或輔助名稱欄位的輔助函式
|
||||
const isSnapshotField = (key: string) => {
|
||||
// 隱藏快照欄位
|
||||
const snapshotFields = [
|
||||
'category_name', 'base_unit_name', 'large_unit_name', 'purchase_unit_name',
|
||||
'warehouse_name', 'user_name', 'from_warehouse_name', 'to_warehouse_name',
|
||||
'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name'
|
||||
'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name',
|
||||
'vendor_name', 'product_name', 'recipe_name'
|
||||
];
|
||||
|
||||
if (snapshotFields.includes(key)) return true;
|
||||
|
||||
// 隱藏所有以 _name 結尾的欄位(因為它們通常是 ID 欄位的文字補充)
|
||||
if (key.endsWith('_name')) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -262,36 +317,26 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
};
|
||||
|
||||
const formatValue = (key: string, value: any) => {
|
||||
// 遮蔽密碼
|
||||
if (key === 'password') return '******';
|
||||
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||
if (key === 'is_active') return value ? '啟用' : '停用';
|
||||
|
||||
// 處理採購單狀態
|
||||
if (key === 'status' && typeof value === 'string' && statusMap[value]) {
|
||||
return statusMap[value];
|
||||
}
|
||||
|
||||
// 處理庫存品質狀態
|
||||
if (key === 'quality_status' && typeof value === 'string' && qualityStatusMap[value]) {
|
||||
return qualityStatusMap[value];
|
||||
}
|
||||
|
||||
// 處理入庫類型
|
||||
if (key === 'type' && typeof value === 'string' && typeMap[value]) {
|
||||
return typeMap[value];
|
||||
}
|
||||
|
||||
// 處理日期欄位 (YYYY-MM-DD)
|
||||
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date') && typeof value === 'string') {
|
||||
// 僅取日期部分 (YYYY-MM-DD)
|
||||
// 處理日期與時間
|
||||
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') {
|
||||
return value.split('T')[0].split(' ')[0];
|
||||
}
|
||||
|
||||
// 處理日期時間欄位 (YYYY-MM-DD HH:mm:ss)
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at') && typeof value === 'string') {
|
||||
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at') && typeof value === 'string') {
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleString('zh-TW', {
|
||||
@@ -308,44 +353,31 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const getFormattedValue = (key: string, value: any) => {
|
||||
// 如果是 ID 欄位,嘗試在快照或屬性中尋找對應名稱
|
||||
if (key.endsWith('_id')) {
|
||||
const nameKey = key.replace('_id', '_name');
|
||||
// 先檢查快照,然後檢查屬性
|
||||
const nameValue = snapshot[nameKey] || attributes[nameKey];
|
||||
if (nameValue) {
|
||||
return `${nameValue}`;
|
||||
}
|
||||
if (nameValue) return `${nameValue}`;
|
||||
}
|
||||
return formatValue(key, value);
|
||||
};
|
||||
|
||||
// 取得翻譯欄位標籤的輔助函式
|
||||
const getFieldLabel = (key: string) => {
|
||||
return fieldLabels[key] || key;
|
||||
};
|
||||
const getFieldLabel = (key: string) => fieldLabels[key] || key;
|
||||
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
|
||||
|
||||
// 取得標題的主題名稱
|
||||
const getSubjectName = () => {
|
||||
// 庫存的特殊處理:顯示 "倉庫 - 商品"
|
||||
if ((snapshot.warehouse_name || attributes.warehouse_name) && (snapshot.product_name || attributes.product_name)) {
|
||||
const wName = snapshot.warehouse_name || attributes.warehouse_name;
|
||||
const pName = snapshot.product_name || attributes.product_name;
|
||||
return `${wName} - ${pName}`;
|
||||
return `${snapshot.warehouse_name || attributes.warehouse_name} - ${snapshot.product_name || attributes.product_name}`;
|
||||
}
|
||||
|
||||
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
|
||||
const nameParams = ['doc_no', 'po_number', 'gr_number', 'production_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'title'];
|
||||
for (const param of nameParams) {
|
||||
if (snapshot[param]) return snapshot[param];
|
||||
if (attributes[param]) return attributes[param];
|
||||
if (old[param]) return old[param];
|
||||
}
|
||||
|
||||
if (attributes.id || old.id) return `#${attributes.id || old.id}`;
|
||||
return '';
|
||||
};
|
||||
@@ -365,7 +397,6 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 現代化元數據條 */}
|
||||
<div className="flex flex-wrap items-center gap-6 pt-2 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
@@ -379,19 +410,19 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
<Package className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-medium text-gray-700">
|
||||
{subjectName ? `${subjectName} ` : ''}
|
||||
{activity.properties?.sub_subject || activity.subject_type}
|
||||
({getSubjectTypeLabel(activity.properties?.sub_subject || activity.subject_type)})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */}
|
||||
{activity.description !== getEventLabel(activity.event) &&
|
||||
activity.description !== 'created' && activity.description !== 'updated' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-6 pb-2 -mt-2">
|
||||
<ActivityIcon className="w-4 h-4 text-gray-400" />
|
||||
<span>{activity.description}</span>
|
||||
<span className="text-sm text-gray-500">{activity.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="bg-gray-50/50 p-6 min-h-[300px]">
|
||||
{activity.event === 'created' ? (
|
||||
|
||||
@@ -29,6 +29,50 @@ interface LogTableProps {
|
||||
from?: number; // 起始索引編號 (paginator.from)
|
||||
}
|
||||
|
||||
// 主體類型解析 (Model 類名轉中文)
|
||||
const subjectTypeMap: Record<string, string> = {
|
||||
// 完整路徑映射
|
||||
'App\\Modules\\Inventory\\Models\\Product': '商品資料',
|
||||
'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料',
|
||||
'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動',
|
||||
'App\\Modules\\Inventory\\Models\\Category': '商品分類',
|
||||
'App\\Modules\\Inventory\\Models\\Unit': '單位',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄',
|
||||
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
|
||||
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
|
||||
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
|
||||
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
|
||||
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
|
||||
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
|
||||
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
|
||||
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
|
||||
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
|
||||
'App\\Modules\\Core\\Models\\User': '使用者帳號',
|
||||
'App\\Modules\\Core\\Models\\Role': '角色權限',
|
||||
// 簡寫映射
|
||||
'Product': '商品資料',
|
||||
'Warehouse': '倉庫資料',
|
||||
'Inventory': '庫存異動',
|
||||
'InventoryTransaction': '庫存異動紀錄',
|
||||
'Category': '商品分類',
|
||||
'Unit': '單位',
|
||||
'Vendor': '廠商資料',
|
||||
'PurchaseOrder': '採購單',
|
||||
'GoodsReceipt': '進貨單',
|
||||
'ProductionOrder': '生產工單',
|
||||
'Recipe': '生產配方',
|
||||
'InventoryCountDoc': '庫存盤點單',
|
||||
'InventoryAdjustDoc': '庫存盤調單',
|
||||
'InventoryTransferOrder': '庫存調撥單',
|
||||
'StockMovementDoc': '庫存單據',
|
||||
'User': '使用者帳號',
|
||||
'Role': '角色權限',
|
||||
'UtilityFee': '公共事業費',
|
||||
};
|
||||
|
||||
export default function LogTable({
|
||||
activities,
|
||||
sortField,
|
||||
@@ -37,6 +81,8 @@ export default function LogTable({
|
||||
onViewDetail,
|
||||
from = 1
|
||||
}: LogTableProps) {
|
||||
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
|
||||
|
||||
const getEventBadgeClass = (event: string) => {
|
||||
switch (event) {
|
||||
case 'created': return 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100';
|
||||
@@ -111,7 +157,7 @@ export default function LogTable({
|
||||
{props.sub_subject ? (
|
||||
<span className="text-gray-700">{props.sub_subject}</span>
|
||||
) : (
|
||||
<span className="text-gray-700">{activity.subject_type}</span>
|
||||
<span className="text-gray-700">{getSubjectTypeLabel(activity.subject_type)}</span>
|
||||
)}
|
||||
|
||||
{/* 如果有原因/來源則顯示(例如:來自補貨) */}
|
||||
@@ -185,7 +231,7 @@ export default function LogTable({
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px]">
|
||||
<Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 break-all whitespace-normal text-left h-auto py-1">
|
||||
{activity.subject_type}
|
||||
{getSubjectTypeLabel(activity.subject_type)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
|
||||
Reference in New Issue
Block a user