feat: 修正庫存與撥補單邏輯並整合文件
1. 修復倉庫統計數據加總與樣式。 2. 修正可用庫存計算邏輯(排除不可銷售倉庫)。 3. 撥補單商品列表加入批號與效期顯示。 4. 修正撥補單儲存邏輯以支援精確批號轉移。 5. 整合 FEATURES.md 至 README.md。
This commit is contained in:
@@ -41,7 +41,7 @@ interface Props {
|
||||
activity: Activity | null;
|
||||
}
|
||||
|
||||
// Field translation map
|
||||
// 欄位翻譯對照表
|
||||
const fieldLabels: Record<string, string> = {
|
||||
name: '名稱',
|
||||
code: '商品代號',
|
||||
@@ -66,19 +66,19 @@ const fieldLabels: Record<string, string> = {
|
||||
role_id: '角色',
|
||||
email_verified_at: '電子郵件驗證時間',
|
||||
remember_token: '登入權杖',
|
||||
// Snapshot fields
|
||||
// 快照欄位
|
||||
category_name: '分類名稱',
|
||||
base_unit_name: '基本單位名稱',
|
||||
large_unit_name: '大單位名稱',
|
||||
purchase_unit_name: '採購單位名稱',
|
||||
// Vendor fields
|
||||
// 廠商欄位
|
||||
short_name: '簡稱',
|
||||
tax_id: '統編',
|
||||
owner: '負責人',
|
||||
contact_name: '聯絡人',
|
||||
tel: '電話',
|
||||
remark: '備註',
|
||||
// Warehouse & Inventory fields
|
||||
// 倉庫與庫存欄位
|
||||
warehouse_name: '倉庫名稱',
|
||||
product_name: '商品名稱',
|
||||
warehouse_id: '倉庫',
|
||||
@@ -86,7 +86,7 @@ const fieldLabels: Record<string, string> = {
|
||||
quantity: '數量',
|
||||
safety_stock: '安全庫存',
|
||||
location: '儲位',
|
||||
// Inventory fields
|
||||
// 庫存欄位
|
||||
batch_number: '批號',
|
||||
box_number: '箱號',
|
||||
origin_country: '來源國家',
|
||||
@@ -95,7 +95,7 @@ const fieldLabels: Record<string, string> = {
|
||||
source_purchase_order_id: '來源採購單',
|
||||
quality_status: '品質狀態',
|
||||
quality_remark: '品質備註',
|
||||
// Purchase Order fields
|
||||
// 採購單欄位
|
||||
po_number: '採購單號',
|
||||
vendor_id: '廠商',
|
||||
vendor_name: '廠商名稱',
|
||||
@@ -110,13 +110,13 @@ const fieldLabels: Record<string, string> = {
|
||||
invoice_date: '發票日期',
|
||||
invoice_amount: '發票金額',
|
||||
last_price: '供貨價格',
|
||||
// Utility Fee fields
|
||||
// 公共事業費欄位
|
||||
transaction_date: '費用日期',
|
||||
category: '費用類別',
|
||||
amount: '金額',
|
||||
};
|
||||
|
||||
// Purchase Order Status Map
|
||||
// 採購單狀態對照表
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
pending: '待審核',
|
||||
@@ -127,7 +127,7 @@ const statusMap: Record<string, string> = {
|
||||
completed: '已完成',
|
||||
};
|
||||
|
||||
// Inventory Quality Status Map
|
||||
// 庫存品質狀態對照表
|
||||
const qualityStatusMap: Record<string, string> = {
|
||||
normal: '正常',
|
||||
frozen: '凍結',
|
||||
@@ -141,17 +141,17 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
const old = activity.properties?.old || {};
|
||||
const snapshot = activity.properties?.snapshot || {};
|
||||
|
||||
// Get all keys from both attributes and old to ensure we show all changes
|
||||
// 取得屬性和舊值的所有鍵,以確保顯示所有變更
|
||||
const allKeys = Array.from(new Set([...Object.keys(attributes), ...Object.keys(old)]));
|
||||
|
||||
// Custom sort order for fields
|
||||
// 自訂欄位排序順序
|
||||
const sortOrder = [
|
||||
'po_number', 'vendor_name', 'warehouse_name', 'expected_delivery_date', 'status', 'remark',
|
||||
'invoice_number', 'invoice_date', 'invoice_amount',
|
||||
'total_amount', 'tax_amount', 'grand_total' // Ensure specific order for amounts
|
||||
'total_amount', 'tax_amount', 'grand_total' // 確保金額的特定順序
|
||||
];
|
||||
|
||||
// Filter out internal keys often logged but not useful for users
|
||||
// 過濾掉通常會記錄但對使用者無用的內部鍵
|
||||
const filteredKeys = allKeys
|
||||
.filter(key =>
|
||||
!['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token'].includes(key)
|
||||
@@ -160,16 +160,16 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
const indexA = sortOrder.indexOf(a);
|
||||
const indexB = sortOrder.indexOf(b);
|
||||
|
||||
// If both are in sortOrder, compare indices
|
||||
// 如果兩者都在排序順序中,比較索引
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||||
// If only A is in sortOrder, it comes first (or wherever logic dictates, usually put known fields first)
|
||||
// 如果只有 A 在排序順序中,它排在前面(或根據邏輯,通常將已知欄位排在前面)
|
||||
if (indexA !== -1) return -1;
|
||||
if (indexB !== -1) return 1;
|
||||
// Otherwise alphabetical or default
|
||||
// 否則按字母順序或預設
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
// Helper to check if a key is a snapshot name field
|
||||
// 檢查鍵是否為快照名稱欄位的輔助函式
|
||||
const isSnapshotField = (key: string) => {
|
||||
return [
|
||||
'category_name', 'base_unit_name', 'large_unit_name', 'purchase_unit_name',
|
||||
@@ -197,26 +197,26 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
};
|
||||
|
||||
const formatValue = (key: string, value: any) => {
|
||||
// Mask password
|
||||
// 遮蔽密碼
|
||||
if (key === 'password') return '******';
|
||||
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||||
if (key === 'is_active') return value ? '啟用' : '停用';
|
||||
|
||||
// Handle Purchase Order Status
|
||||
// 處理採購單狀態
|
||||
if (key === 'status' && typeof value === 'string' && statusMap[value]) {
|
||||
return statusMap[value];
|
||||
}
|
||||
|
||||
// Handle Inventory Quality Status
|
||||
// 處理庫存品質狀態
|
||||
if (key === 'quality_status' && typeof value === 'string' && qualityStatusMap[value]) {
|
||||
return qualityStatusMap[value];
|
||||
}
|
||||
|
||||
// Handle Date Fields (YYYY-MM-DD)
|
||||
// 處理日期欄位 (YYYY-MM-DD)
|
||||
if ((key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date') && typeof value === 'string') {
|
||||
// Take only the date part (YYYY-MM-DD)
|
||||
// 僅取日期部分 (YYYY-MM-DD)
|
||||
return value.split('T')[0].split(' ')[0];
|
||||
}
|
||||
|
||||
@@ -224,10 +224,10 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
};
|
||||
|
||||
const getFormattedValue = (key: string, value: any) => {
|
||||
// If it's an ID field, try to find a corresponding name in snapshot or attributes
|
||||
// 如果是 ID 欄位,嘗試在快照或屬性中尋找對應名稱
|
||||
if (key.endsWith('_id')) {
|
||||
const nameKey = key.replace('_id', '_name');
|
||||
// Check snapshot first, then attributes
|
||||
// 先檢查快照,然後檢查屬性
|
||||
const nameValue = snapshot[nameKey] || attributes[nameKey];
|
||||
if (nameValue) {
|
||||
return `${nameValue}`;
|
||||
@@ -236,14 +236,14 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
return formatValue(key, value);
|
||||
};
|
||||
|
||||
// Helper to get translated field label
|
||||
// 取得翻譯欄位標籤的輔助函式
|
||||
const getFieldLabel = (key: string) => {
|
||||
return fieldLabels[key] || key;
|
||||
};
|
||||
|
||||
// Get subject name for header
|
||||
// 取得標題的主題名稱
|
||||
const getSubjectName = () => {
|
||||
// Special handling for Inventory: show "Warehouse - Product"
|
||||
// 庫存的特殊處理:顯示 "倉庫 - 商品"
|
||||
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;
|
||||
@@ -276,7 +276,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Modern Metadata Strip */}
|
||||
{/* 現代化元數據條 */}
|
||||
<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" />
|
||||
@@ -293,7 +293,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
{activity.properties?.sub_subject || activity.subject_type}
|
||||
</span>
|
||||
</div>
|
||||
{/* Only show 'description' if it differs from event name (unlikely but safe) */}
|
||||
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */}
|
||||
{activity.description !== getEventLabel(activity.event) &&
|
||||
activity.description !== 'created' && activity.description !== 'updated' && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -367,7 +367,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
const newValue = attributes[key];
|
||||
const isChanged = JSON.stringify(oldValue) !== JSON.stringify(newValue);
|
||||
|
||||
// For deleted events, we want to show the current attributes in the "Before" column
|
||||
// 對於刪除事件,我們希望在 "變更前" 欄位顯示當前屬性
|
||||
const displayBefore = activity.event === 'deleted'
|
||||
? getFormattedValue(key, newValue || oldValue)
|
||||
: getFormattedValue(key, oldValue);
|
||||
@@ -399,7 +399,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
{/* Items Diff Section (Special for Purchase Orders) */}
|
||||
{/* 項目差異區塊(採購單專用) */}
|
||||
{activity.properties?.items_diff && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<h3 className="text-sm font-bold text-gray-900 flex items-center gap-2 px-1">
|
||||
@@ -417,7 +417,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{/* Updated Items */}
|
||||
{/* 更新項目 */}
|
||||
{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>
|
||||
@@ -440,7 +440,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{/* Added Items */}
|
||||
{/* 新增項目 */}
|
||||
{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>
|
||||
@@ -453,7 +453,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{/* Removed Items */}
|
||||
{/* 移除項目 */}
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user