feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s

This commit is contained in:
2026-03-02 16:42:12 +08:00
parent 7dac2d1f77
commit 0a955fb993
33 changed files with 1424 additions and 853 deletions

View File

@@ -168,6 +168,28 @@ const fieldLabels: Record<string, string> = {
posted_by: '過帳者',
counted_qty: '盤點數量',
adjust_qty: '調整數量',
// 調撥單專有欄位
transit_warehouse_id: '在途倉庫',
transit_warehouse_name: '在途倉庫名稱',
dispatched_at: '出貨日期',
dispatched_by: '出貨人',
received_at: '收貨日期',
received_by: '收貨人',
reserved_quantity: '預扣數量',
snapshot_quantity: '異動前庫存 (快照)',
// 門市叫貨欄位
store_warehouse_id: '申請倉庫',
store_warehouse_name: '申請倉庫',
supply_warehouse_id: '供貨倉庫',
supply_warehouse_name: '供貨倉庫',
approved_by: '審核人',
approved_user_name: '審核人',
approved_at: '審核時間',
submitted_at: '提交時間',
reject_reason: '駁回原因',
status_label: '處理狀態',
transfer_order_id: '調撥單 ID',
transfer_order_name: '調撥單號',
};
// 狀態翻譯對照表
@@ -192,6 +214,7 @@ const statusMap: Record<string, string> = {
in_progress: '生產中',
// 調撥單狀態
voided: '已作廢',
dispatched: '已出貨',
};
// 主體類型解析 (Model 類名轉中文)
@@ -206,17 +229,7 @@ const subjectTypeMap: Record<string, string> = {
'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': '角色權限',
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
// 簡寫映射 (應對後端回傳 class_basename 的情況)
'Product': '商品資料',
'Warehouse': '倉庫資料',
@@ -231,7 +244,8 @@ const subjectTypeMap: Record<string, string> = {
'Recipe': '生產配方',
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫調撥單',
'InventoryTransferOrder': '庫調撥單',
'StoreRequisition': '門市叫貨單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',
@@ -264,17 +278,31 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
// 自訂欄位排序順序
const sortOrder = [
'doc_no', 'po_number', 'gr_number', 'production_number',
'doc_no', 'po_number', 'gr_number', 'production_number', 'transfer_order_name',
'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
'invoice_number', 'invoice_date', 'invoice_amount',
'total_amount', 'tax_amount', 'grand_total'
];
// 過濾掉通常會記錄但對使用者無用的內部鍵
// 過濾掉通常會記錄但對使用者無用的內部鍵,以及已被解析為名稱的原始 ID 欄位
const filteredKeys = allKeys
.filter(key =>
!['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token'].includes(key)
)
.filter(key => {
if (['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token', 'activityProperties'].includes(key)) return false;
// 隱藏冗餘的狀態標籤 (因為後端已統一替換 status 內容)
if (key === 'status_label') return false;
// 隱藏技術用的 ID 欄位 (如果已有對應的名稱欄位)
if (key.endsWith('_id')) {
const nameKey = key.replace('_id', '_name');
const userNameKey = key.replace('_id', '_user_name');
if (allKeys.includes(nameKey) || allKeys.includes(userNameKey)) return false;
}
// 特別隱藏調撥單 ID
if (key === 'transfer_order_id' && (allKeys.includes('transfer_order_name') || allKeys.includes('transfer_order_doc_no'))) return false;
return true;
})
.sort((a, b) => {
const indexA = sortOrder.indexOf(a);
const indexB = sortOrder.indexOf(b);
@@ -336,9 +364,13 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
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];
}
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time') && typeof value === 'string') {
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time' || key === 'submitted_at' || key === 'approved_at') && typeof value === 'string') {
try {
const date = new Date(value);
// 處理部分 ISO 字串包含 T 的情況
const normalizedValue = value.replace('T', ' ');
const date = new Date(normalizedValue);
if (isNaN(date.getTime())) return value;
return date.toLocaleString('zh-TW', {
timeZone: 'Asia/Taipei',
year: 'numeric',
@@ -537,65 +569,89 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
</TableRow>
</TableHeader>
<TableBody>
{/* 更新項目 */}
{activity.properties.items_diff.updated?.map((item: any, idx: number) => (
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200"></Badge>
{/* 1. 處理物件格式的 items_diff (現有的採購、盤點、調撥初始紀錄格式) */}
{!Array.isArray(activity.properties?.items_diff) && (
<>
{/* 更新項目 */}
{activity.properties?.items_diff?.updated?.map((item: any, idx: number) => (
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200"></Badge>
</TableCell>
<TableCell className="text-sm">
<div className="space-y-1 text-xs">
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.quantity}</span> <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
)}
{item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
)}
</div>
</TableCell>
</TableRow>
)) || null}
{/* 新增項目 */}
{activity.properties?.items_diff?.added?.map((item: any, idx: number) => (
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200"></Badge>
</TableCell>
<TableCell className="text-sm">
: {item.new?.quantity ?? item.quantity} {item.unit_name || item.new?.unit_name || ''}
</TableCell>
</TableRow>
)) || null}
{/* 移除項目 */}
{activity.properties?.items_diff?.removed?.map((item: any, idx: number) => (
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200"></Badge>
</TableCell>
<TableCell className="text-sm text-gray-400">
: {item.old?.quantity ?? item.quantity} {item.unit_name || item.old?.unit_name || ''}
</TableCell>
</TableRow>
)) || null}
</>
)}
{/* 2. 處理陣列格式的 items_diff (調撥單過帳/收貨的複合紀錄格式) */}
{Array.isArray(activity.properties?.items_diff) && activity.properties.items_diff.map((item: any, idx: number) => (
<TableRow key={`trf-${idx}`} className="hover:bg-gray-50/50">
<TableCell className="font-medium">
{item.product_name}
<div className="text-[10px] text-gray-400 font-normal">: {item.batch_number}</div>
</TableCell>
<TableCell className="text-sm">
<div className="space-y-1">
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.quantity}</span> <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
<TableCell className="text-center">
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200"></Badge>
</TableCell>
<TableCell className="text-xs p-2">
<div className="flex flex-col gap-2">
{item.source_warehouse && (
<div className="flex items-center gap-2">
<span className="w-16 text-gray-400 truncate"> ({item.source_warehouse}):</span>
<span className="text-gray-400 line-through">{Number(item.source_before || 0).toFixed(0)}</span>
<span className="text-rose-600 font-bold"> {Number(item.source_after || 0).toFixed(0)}</span>
<span className="text-[10px] bg-rose-50 text-rose-600 px-1 rounded">-{item.quantity}</span>
</div>
)}
{item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
)}
{item.old?.adjust_qty !== item.new?.adjust_qty && (
<div>調: <span className="text-gray-500 line-through">{item.old?.adjust_qty ?? '0'}</span> <span className="text-blue-700 font-bold">{item.new?.adjust_qty ?? '0'}</span></div>
)}
{item.old?.unit_name !== item.new?.unit_name && item.old?.unit_name !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
)}
{item.old?.subtotal !== item.new?.subtotal && item.old?.subtotal !== undefined && (
<div>: <span className="text-gray-500 line-through">${item.old.subtotal}</span> <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
)}
{item.old?.notes !== item.new?.notes && (
<div>: <span className="text-gray-500 line-through">{item.old?.notes || '-'}</span> <span className="text-blue-700 font-bold">{item.new?.notes || '-'}</span></div>
{item.target_warehouse && (
<div className="flex items-center gap-2">
<span className="w-16 text-gray-400 truncate"> ({item.target_warehouse}):</span>
<span className="text-gray-400 line-through">{Number(item.target_before || 0).toFixed(0)}</span>
<span className="text-emerald-600 font-bold"> {Number(item.target_after || 0).toFixed(0)}</span>
<span className="text-[10px] bg-emerald-50 text-emerald-600 px-1 rounded">+{item.quantity}</span>
</div>
)}
</div>
</TableCell>
</TableRow>
)) || null}
{/* 新增項目 */}
{activity.properties.items_diff.added?.map((item: any, idx: number) => (
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200"></Badge>
</TableCell>
<TableCell className="text-sm">
{item.quantity !== undefined ? `數量: ${item.quantity} ${item.unit_name || ''} / ` : ''}
{item.adjust_qty !== undefined ? `調整量: ${item.adjust_qty} / ` : ''}
{item.subtotal !== undefined ? `小計: $${item.subtotal}` : ''}
</TableCell>
</TableRow>
)) || null}
{/* 移除項目 */}
{activity.properties.items_diff.removed?.map((item: any, idx: number) => (
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200"></Badge>
</TableCell>
<TableCell className="text-sm text-gray-400">
: {item.quantity} {item.unit_name}
</TableCell>
</TableRow>
)) || null}
))}
</TableBody>
</Table>
</div>

View File

@@ -42,6 +42,7 @@ const subjectTypeMap: Record<string, string> = {
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
@@ -67,6 +68,7 @@ const subjectTypeMap: Record<string, string> = {
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫存調撥單',
'StoreRequisition': '門市叫貨單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',