實作 InventoryService 的批量入庫 (processIncomingInventory) 與庫存調整 (adjustInventory) 邏輯
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s

This commit is contained in:
2026-03-02 10:47:43 +08:00
parent 5f8b2a1c2d
commit 649af40919
5 changed files with 330 additions and 226 deletions

View File

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

View File

@@ -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', '庫存資料已更新');

View File

@@ -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(),
]);
}
});
}
}

View File

@@ -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' ? (
@@ -571,7 +602,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
</div>
)}
</div>
</DialogContent>
</Dialog>
</DialogContent >
</Dialog >
);
}

View File

@@ -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">