diff --git a/.agent/skills/activity-logging/SKILL.md b/.agent/skills/activity-logging/SKILL.md index 854df5a..3ea89fe 100644 --- a/.agent/skills/activity-logging/SKILL.md +++ b/.agent/skills/activity-logging/SKILL.md @@ -1,158 +1,111 @@ --- name: 操作紀錄實作規範 -description: 規範系統內 Activity Log 的實作標準,包含後端資料過濾、快照策略、與前端顯示邏輯。 +description: 規範系統內 Activity Log 的實作標準,包含自動名稱解析、複雜單據合併記錄、與前端顯示優化。 --- -# 操作紀錄實作規範 +# 操作紀錄實作規範 (Activity Logging Skill) -本文件說明如何在開發新功能時,依據系統規範實作 `spatie/laravel-activitylog` 操作紀錄,確保資料儲存效率與前端顯示一致性。 +本文件定義了 Star ERP 系統中操作紀錄的最高實作標準,旨在確保每筆日誌都具有「高度可讀性」與「單一性」。 -## 1. 後端實作標準 (Backend) +--- -所有 Model 之操作紀錄應遵循「僅儲存變動資料」與「保留關鍵快照」兩大原則。 +## 1. 後端實作核心 (Backend) -### 1.1 啟用 Activity Log - -在 Model 中引用 `LogsActivity` trait 並實作 `getActivitylogOptions` 方法。 - -```php -use Spatie\Activitylog\Traits\LogsActivity; -use Spatie\Activitylog\LogOptions; - -class Product extends Model -{ - use LogsActivity; - - public function getActivitylogOptions(): LogOptions - { - return LogOptions::defaults() - ->logAll() - ->logOnlyDirty() // ✅ 關鍵:只記錄有變動的欄位 - ->dontSubmitEmptyLogs(); // 若無變動則不記錄 - } -} -``` - -### 1.2 手動記錄 (Manual Logging) - -若需在 Controller 手動記錄(例如需客製化邏輯),**必須**自行實作變動過濾,不可直接儲存所有屬性。 - -**錯誤範例 (Do NOT do this):** -```php -// ❌ 錯誤:這會導致每次更新都記錄所有欄位,即使它們沒變 -activity() - ->withProperties(['attributes' => $newAttributes, 'old' => $oldAttributes]) - ->log('updated'); -``` - -**正確範例 (Do this):** -```php -// ✅ 正確:自行比對差異,只存變動值 -$changedAttributes = []; -$changedOldAttributes = []; - -foreach ($newAttributes as $key => $value) { - if ($value != ($oldAttributes[$key] ?? null)) { - $changedAttributes[$key] = $value; - $changedOldAttributes[$key] = $oldAttributes[$key] ?? null; - } -} - -if (!empty($changedAttributes)) { - activity() - ->withProperties(['attributes' => $changedAttributes, 'old' => $changedOldAttributes]) - ->log('updated'); -} -``` - -### 1.3 快照策略 (Snapshot Strategy) - -為確保資料被刪除後仍能辨識操作對象,**必須**在 `properties.snapshot` 中儲存關鍵識別資訊(如名稱、代號、類別名稱)。 - -**主要方式:使用 `tapActivity` (推薦)** +### 1.1 全域 ID 轉名稱邏輯 (Global ID Resolution) +為了讓管理者能直覺看懂日誌,所有的 ID(如 `warehouse_id`, `created_by`)在記錄時都應自動解析為名稱。此邏輯應統一在 Model 的 `tapActivity` 中實作。 +#### 關鍵實作參考: ```php public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) { - $properties = $activity->properties; + // 🚩 核心:轉換為陣列以避免 Indirect modification error + $properties = $activity->properties instanceof \Illuminate\Support\Collection + ? $activity->properties->toArray() + : $activity->properties; + + // 1. Snapshot 快照:用於主描述的上下文(例如:單號、名稱) $snapshot = $properties['snapshot'] ?? []; - - // 保存關鍵關聯名稱 (避免關聯資料刪除後 ID 失效) - $snapshot['category_name'] = $this->category ? $this->category->name : null; - $snapshot['po_number'] = $this->code; // 儲存單號 - - // 保存自身名稱 (Context) - $snapshot['name'] = $this->name; - + $snapshot['doc_no'] = $this->doc_no; + $snapshot['warehouse_name'] = $this->warehouse?->name; $properties['snapshot'] = $snapshot; + + // 2. 名稱解析:自動將 attributes 與 old 中的 ID 換成人名/物名 + $resolver = function (&$data) { + if (empty($data) || !is_array($data)) return; + + // 使用者 ID 轉換 + foreach (['created_by', 'updated_by', 'completed_by'] as $f) { + if (isset($data[$f]) && is_numeric($data[$f])) { + $data[$f] = \App\Modules\Core\Models\User::find($data[$f])?->name; + } + } + // 倉庫 ID 轉換 + if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) { + $data['warehouse_id'] = \App\Modules\Inventory\Models\Warehouse::find($data['warehouse_id'])?->name; + } + }; + + if (isset($properties['attributes'])) $resolver($properties['attributes']); + if (isset($properties['old'])) $resolver($properties['old']); + $activity->properties = $properties; } ``` -## 2. 顯示名稱映射 (UI Mapping) +### 1.2 複雜操作的日誌合併 (Log Consolidation) +當一個操作同時涉及「多個品項異動」與「單據狀態變更」時,**嚴禁**產生多筆重複日誌。 -### 2.1 對象名稱映射 (Mapping) - -需在 `ActivityLogController.php` 中設定 Model 與中文名稱的對應,讓前端列表能顯示中文對象(如「公共事業費」而非 `UtilityFee`)。 - -**位置**: `app/Http/Controllers/Admin/ActivityLogController.php` +* **策略**:在 Service 層手動發送主日誌,並使用 `saveQuietly()` 更新單據屬性以抑止 Trait 的自動日誌。 +* **格式**:主日誌應包含 `items_diff` (品項差異) 與 `attributes/old` (單據狀態變更)。 ```php -protected function getSubjectMap() -{ - return [ - 'App\Modules\Inventory\Models\Product' => '商品', - 'App\Modules\Finance\Models\UtilityFee' => '公共事業費', // ✅ 新增映射 - ]; -} +// Service 中的實作方式 +DB::transaction(function () use ($doc, $items) { + // 1. 更新品項 (記錄變動細節) + $updatedItems = $this->getUpdatedItems($doc, $items); + + // 2. 靜默更新單據狀態 (避免 Trait 產生冗餘日誌) + $doc->status = 'completed'; + $doc->saveQuietly(); + + // 3. 手動觸發單一合併日誌 + activity() + ->performedOn($doc) + ->withProperties([ + 'items_diff' => ['updated' => $updatedItems], + 'attributes' => ['status' => 'completed'], + 'old' => ['status' => 'counting'] + ]) + ->log('updated'); +}); ``` -### 2.2 欄位名稱中文化 (Field Translation) +--- -需在前端 `ActivityDetailDialog` 中設定欄位名稱的中文翻譯。 +## 2. 前端介面規範 (Frontend) -**位置**: `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` +### 2.1 標籤命名規範 (Field Labels) +前端顯示應完全移除「ID」字眼,提供最友善的閱讀體驗。 +**檔案位置**: `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` ```typescript const fieldLabels: Record = { - // ... 既有欄位 - 'transaction_date': '費用日期', - 'category': '費用類別', - 'amount': '金額', + warehouse_id: '倉庫', // ❌ 禁用「倉庫 ID」 + created_by: '建立者', // ❌ 禁用「建立者 ID」 + completed_by: '完成者', + status: '狀態', }; ``` -## 3. 前端顯示邏輯 (Frontend) +### 2.2 特殊結構顯示 +* **品項異動**:前端應能渲染 `items_diff` 結構,以「品項名稱 + 數值變動」的方式呈現表格(已在 `ActivityDetailDialog` 實作)。 -### 3.1 列表描述生成 (Description Generation) +--- -前端 `LogTable.tsx` 會依據 `properties.snapshot` 中的欄位自動組建描述(例如:「Admin 新增 電話費 公共事業費」)。 +## 3. 開發檢核清單 (Checklist) -若您的 Model 使用了特殊的識別欄位(例如 `category`),**必須**將其加入 `nameParams` 陣列中。 - -**位置**: `resources/js/Components/ActivityLog/LogTable.tsx` - -```typescript -const nameParams = [ - 'po_number', 'name', 'code', - 'category_name', - 'category' // ✅ 確保加入此欄位,前端才能抓到 $snapshot['category'] -]; -``` - -### 3.2 詳情過濾邏輯 - -前端 `ActivityDetailDialog` 已內建智慧過濾邏輯: -- **Created**: 顯示初始化欄位。 -- **Updated**: **僅顯示有變動的欄位** (由 `isChanged` 判斷)。 -- **Deleted**: 顯示刪除前的完整資料。 - -開發者僅需確保傳入的 `attributes` 與 `old` 資料結構正確,過濾邏輯會自動運作。 - -## 檢核清單 - -- [ ] **Backend**: Model 是否已設定 `logOnlyDirty` 或手動實作過濾? -- [ ] **Backend**: 是否已透過 `tapActivity` 或手動方式記錄 Snapshot(關鍵名稱)? -- [ ] **Backend**: 是否已在 `ActivityLogController` 加入 Model 中文名稱映射? -- [ ] **Frontend**: 是否已在 `ActivityDetailDialog` 加入欄位中文翻譯? -- [ ] **Frontend**: 若使用特殊識別欄位,是否已加入 `LogTable` 的 `nameParams`? +- [ ] **Model**: `tapActivity` 是否已處理 Collection 快照? +- [ ] **Model**: 是否已實作全域 ID 至名稱的自動解析? +- [ ] **Service**: 是否使用 `saveQuietly()` 避免產生重複的「單據已更新」日誌? +- [ ] **UI**: `fieldLabels` 是否已移除所有「ID」字樣? +- [ ] **UI**: 若有品項異動,是否已正確格式化傳入 `items_diff`?