From 6b6e840f3529099fedd868b6e792964333fa4323 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 6 Mar 2026 10:43:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=95=B4=E5=90=88=E8=88=87=E5=84=AA?= =?UTF-8?q?=E5=8C=96=20Agent=20Skills=20=E8=A6=8F=E7=AF=84=E5=8F=8A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8A=80=E8=83=BD=E8=A7=B8=E7=99=BC=E6=BA=96?= =?UTF-8?q?=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/rules/activity-logging.md | 115 ------- .agents/rules/framework.md | 8 +- .agents/rules/permission-management.md | 144 --------- .agents/rules/skill-triggers.md | 52 ++++ .agents/rules/ui-consistency.md | 105 ------- .agents/skills/activity-logging/SKILL.md | 285 ++++++++++++++++++ .../cross-module-communication/SKILL.md} | 37 +-- .../git-workflows/SKILL.md} | 5 +- .agents/skills/permission-management/SKILL.md | 206 +++++++++++++ .../skills/ui-consistency/SKILL.md | 49 ++- .../skills/activity-logging/SKILL.md | 158 ---------- .../skills/permission-management/SKILL.md | 140 --------- 12 files changed, 596 insertions(+), 708 deletions(-) delete mode 100644 .agents/rules/activity-logging.md delete mode 100644 .agents/rules/permission-management.md create mode 100644 .agents/rules/skill-triggers.md delete mode 100644 .agents/rules/ui-consistency.md create mode 100644 .agents/skills/activity-logging/SKILL.md rename .agents/{rules/cross-module-communication.md => skills/cross-module-communication/SKILL.md} (64%) rename .agents/{rules/git-workflows.md => skills/git-workflows/SKILL.md} (90%) create mode 100644 .agents/skills/permission-management/SKILL.md rename {.gemini/antigravity => .agents}/skills/ui-consistency/SKILL.md (94%) delete mode 100644 .gemini/antigravity/skills/activity-logging/SKILL.md delete mode 100644 .gemini/antigravity/skills/permission-management/SKILL.md diff --git a/.agents/rules/activity-logging.md b/.agents/rules/activity-logging.md deleted file mode 100644 index 0ea846f..0000000 --- a/.agents/rules/activity-logging.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -trigger: always_on ---- - ---- -name: 操作紀錄實作規範 -description: 規範系統內 Activity Log 的實作標準,包含自動名稱解析、複雜單據合併記錄、與前端顯示優化。 ---- - -# 操作紀錄實作規範 (Activity Logging Skill) - -本文件定義了 Star ERP 系統中操作紀錄的最高實作標準,旨在確保每筆日誌都具有「高度可讀性」與「單一性」。 - ---- - -## 1. 後端實作核心 (Backend) - -### 1.1 全域 ID 轉名稱邏輯 (Global ID Resolution) -為了讓管理者能直覺看懂日誌,所有的 ID(如 `warehouse_id`, `created_by`)在記錄時都應自動解析為名稱。此邏輯應統一在 Model 的 `tapActivity` 中實作。 - -#### 關鍵實作參考: -```php -public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) -{ - // 🚩 核心:轉換為陣列以避免 Indirect modification error - $properties = $activity->properties instanceof \Illuminate\Support\Collection - ? $activity->properties->toArray() - : $activity->properties; - - // 1. Snapshot 快照:用於主描述的上下文(例如:單號、名稱) - $snapshot = $properties['snapshot'] ?? []; - $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; -} -``` - -### 1.2 複雜操作的日誌合併 (Log Consolidation) -當一個操作同時涉及「多個品項異動」與「單據狀態變更」時,**嚴禁**產生多筆重複日誌。 - -* **策略**:在 Service 層手動發送主日誌,並使用 `saveQuietly()` 更新單據屬性以抑止 Trait 的自動日誌。 -* **格式**:主日誌應包含 `items_diff` (品項差異) 與 `attributes/old` (單據狀態變更)。 - -```php -// 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. 前端介面規範 (Frontend) - -### 2.1 標籤命名規範 (Field Labels) -前端顯示應完全移除「ID」字眼,提供最友善的閱讀體驗。 - -**檔案位置**: `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` -```typescript -const fieldLabels: Record = { - warehouse_id: '倉庫', // ❌ 禁用「倉庫 ID」 - created_by: '建立者', // ❌ 禁用「建立者 ID」 - completed_by: '完成者', - status: '狀態', -}; -``` - -### 2.2 特殊結構顯示 -* **品項異動**:前端應能渲染 `items_diff` 結構,以「品項名稱 + 數值變動」的方式呈現表格(已在 `ActivityDetailDialog` 實作)。 - ---- - -## 3. 開發檢核清單 (Checklist) - -- [ ] **Model**: `tapActivity` 是否已處理 Collection 快照? -- [ ] **Model**: 是否已實作全域 ID 至名稱的自動解析? -- [ ] **Service**: 是否使用 `saveQuietly()` 避免產生重複的「單據已更新」日誌? -- [ ] **UI**: `fieldLabels` 是否已移除所有「ID」字樣? -- [ ] **UI**: 若有品項異動,是否已正確格式化傳入 `items_diff`? diff --git a/.agents/rules/framework.md b/.agents/rules/framework.md index 421698d..e2e4021 100644 --- a/.agents/rules/framework.md +++ b/.agents/rules/framework.md @@ -78,10 +78,4 @@ trigger: always_on * **執行 PHP 指令**: `./vendor/bin/sail php -v` * **執行 Artisan 指令**: `./vendor/bin/sail artisan route:list` * **執行 Composer**: `./vendor/bin/sail composer install` -* **執行 Node/NPM**: `./vendor/bin/sail npm run dev` - -## 10. 日期處理 (Date Handling) - -- 前端顯示日期時預設使用 `resources/js/lib/date.ts` 提供的 `formatDate` 工具。 -- 避免直接顯示原始 ISO 字串(如 `...Z` 結尾的格式)。 -- **智慧格式切換**:`formatDate` 會自動判斷原始資料,若時間部分為 `00:00:00` 則僅顯示 `YYYY-MM-DD`,否則顯示 `YYYY-MM-DD HH:mm:ss`。 \ No newline at end of file +* **執行 Node/NPM**: `./vendor/bin/sail npm run dev` \ No newline at end of file diff --git a/.agents/rules/permission-management.md b/.agents/rules/permission-management.md deleted file mode 100644 index d4de0b4..0000000 --- a/.agents/rules/permission-management.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -trigger: always_on ---- - ---- -name: 權限管理與實作規範 -description: 為新功能實作權限控制的完整流程規範,包含後端 Seeder 設定、Middleware 路由保護與前端權限判斷。 ---- - -# 權限管理與實作規範 - -本文件說明如何在新增功能時,一併實作完整的權限控制機制。專案採用 `spatie/laravel-permission` 套件進行權限管理。 - -## 1. 定義權限 (Backend) - -所有權限皆定義於 `database/seeders/PermissionSeeder.php`。 - -### 步驟: - -1. 開啟 `database/seeders/PermissionSeeder.php`。 -2. 在 `$permissions` 陣列中新增功能對應的權限字串。 - * **命名慣例**:`{resource}.{action}` (例如:`system.view_logs`, `products.create`) - * 常用動作:`view`, `create`, `edit`, `delete`, `publish`, `export` -3. 在下方「角色分配」區段,將新權限分配給適合的角色。 - * `super-admin`:通常擁有所有權限(程式碼中 `Permission::all()` 自動涵蓋,無需手動新增)。 - * `admin`:通常擁有大部分權限。 - * 其他角色 (`warehouse-manager`, `purchaser`, `viewer`):依業務邏輯分配。 - -### 範例: - -```php -// 1. 新增權限字串 -$permissions = [ - // ... 現有權限 - 'system.view_logs', // 新增:檢視系統日誌 -]; - -// ... - -// 2. 分配給角色 -$admin->givePermissionTo([ - // ... 現有權限 - 'system.view_logs', -]); -``` - -## 2. 套用資料庫變更 - -修改 Seeder 後,必須重新執行 Seeder 以將權限寫入資料庫。 - -```bash -# 對於所有租戶執行 Seeder (開發環境) -php artisan tenants:seed --class=PermissionSeeder -``` - -## 3. 路由保護 (Backend Middleware) - -在 `routes/web.php` 中,使用 `permission:{name}` middleware 保護路由。 - -### 範例: - -```php -// 單一權限保護 -Route::get('/logs', [LogController::class, 'index']) - ->middleware('permission:system.view_logs') - ->name('logs.index'); - -// 路由群組保護 -Route::middleware('permission:products.view')->group(function () { - // ... -}); - -// 多重權限 (OR 邏輯:有其一即可) -Route::middleware('permission:products.create|products.edit')->group(function () { - // ... -}); -``` - -## 4. 前端權限判斷 (React Component) - -使用自訂 Hook `usePermission` 來控制 UI 元素的顯示(例如:隱藏沒有權限的按鈕)。 - -### 引入 Hook: - -```tsx -import { usePermission } from "@/hooks/usePermission"; -``` - -### 使用方式: - -```tsx -export default function ProductIndex() { - const { can } = usePermission(); - - return ( -
-

商品列表

- - {/* 只有擁有 create 權限才顯示按鈕 */} - {can('products.create') && ( - - )} - - {/* 組合判斷 */} - {can('products.edit') && } -
- ); -} -``` - -### 權限 Hook 介面說明: - -- `can(permission: string)`: 檢查當前使用者是否擁有指定權限。 -- `canAny(permissions: string[])`: 檢查當前使用者是否擁有陣列中**任一**權限。 -- `hasRole(role: string)`: 檢查當前使用者是否擁有指定角色。 - -## 5. 配置權限群組名稱 (Backend UI Config) - -為了讓新權限在「角色與權限」管理介面中顯示正確的中文分組標題,需修改 Controller 設定。 - -### 步驟: - -1. 開啟 `app/Http/Controllers/Admin/RoleController.php`。 -2. 找到 `getGroupedPermissions` 方法。 -3. 在 `$groupDefinitions` 陣列中,新增 `{resource}` 對應的中文名稱。 - -### 範例: - -```php -$groupDefinitions = [ - 'products' => '商品資料管理', - // ... - 'utility_fees' => '公共事業費管理', // 新增此行 -]; -``` - -## 檢核清單 - -- [ ] `PermissionSeeder.php` 已新增權限字串。 -- [ ] `PermissionSeeder.php` 已將新權限分配給對應角色。 -- [ ] 已執行 `php artisan tenants:seed --class=PermissionSeeder` 更新資料庫。 -- [ ] `RoleController.php` 已新增權限群組的中文名稱映射。 -- [ ] 後端路由 (`routes/web.php`) 已加上 middleware 保護。 -- [ ] 前端頁面/按鈕已使用 `usePermission` 進行顯示控制。 diff --git a/.agents/rules/skill-triggers.md b/.agents/rules/skill-triggers.md new file mode 100644 index 0000000..5c24340 --- /dev/null +++ b/.agents/rules/skill-triggers.md @@ -0,0 +1,52 @@ +--- +trigger: always_on +glob: +description: +--- + +# 技能觸發規範 (Skill Trigger Rules) + +本文件確保 AI 助手在對話中能**主動辨識**需要參照技能 (Skill) 的時機。 +Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。 +**若對話內容命中以下任一觸發條件,必須先使用 `view_file` 讀取對應的 `SKILL.md` 後再進行作業。** + +--- + +## 觸發對照表 + +| 觸發詞 / 情境 | 對應 Skill | 路徑 | +|---|---|---| +| 操作紀錄、Activity Log、日誌、`tapActivity`、`LogsActivity`、`saveQuietly`、`activity()`、`items_diff` | **操作紀錄實作規範** | `.agents/skills/activity-logging/SKILL.md` | +| 權限、permission、角色、role、`usePermission`、``、`PermissionSeeder`、middleware protection | **權限管理與實作規範** | `.agents/skills/permission-management/SKILL.md` | +| 跨模組、Service Interface、`Contracts`、模組間通訊、`ServiceProvider` 綁定、禁止跨模組引用 | **跨模組調用與通訊規範** | `.agents/skills/cross-module-communication/SKILL.md` | +| 按鈕樣式、表格規範、圖標、分頁、Badge、Toast、表單、UI 統一、頁面佈局、`button-filled-*`、`button-outlined-*`、`lucide-react`、色彩系統 | **客戶端後台 UI 統一規範** | `.agents/skills/ui-consistency/SKILL.md` | +| Git 分支、commit、合併、部署、`feature/`、`hotfix/`、`develop`、`main` | **Git 分支管理與開發規範** | `.agents/skills/git-workflows/SKILL.md` | + +--- + +## 強制觸發場景 + +以下場景**無論對話中是否出現觸發詞**,都必須主動載入對應 Skill: + +### 🔴 新增功能或頁面時 +必須同時讀取: +1. **permission-management** — 設定權限 +2. **ui-consistency** — 遵循 UI 規範 +3. **activity-logging** — 若涉及 Model CRUD,需加上操作紀錄 + +### 🔴 新增或修改 Model 時 +必須讀取: +1. **activity-logging** — `tapActivity` 實作 +2. **cross-module-communication** — 確認是否涉及跨模組引用 + +### 🔴 Git 操作時 +必須讀取: +1. **git-workflows** — 分支命名與 commit 格式 + +--- + +## 注意事項 + +> [!IMPORTANT] +> 即使你「記得」Skill 的大致內容,仍必須重新讀取 `SKILL.md`。 +> 因為 Skill 文件可能已經更新,且記憶中的內容可能不完整。 diff --git a/.agents/rules/ui-consistency.md b/.agents/rules/ui-consistency.md deleted file mode 100644 index 2dfe0eb..0000000 --- a/.agents/rules/ui-consistency.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -trigger: always_on ---- - ---- -name: 客戶端後台 UI 統一規範 -description: 確保 Star ERP 客戶端(租戶端)後台所有頁面的 UI 元件保持統一的樣式與行為 ---- - -## 適用範圍 - -本規範適用於租戶端後台(使用 `AuthenticatedLayout` 的頁面),**不適用於**中央管理後台(`LandlordLayout`)。 - -## 核心禁止事項 - -- ❌ **禁止 Hardcode 色碼**(如 `text-[#01ab83]`),必須使用 `*-primary-main` 等 Tailwind Class 或 CSS 變數 -- ❌ **禁止使用非 `lucide-react` 的圖標庫**(如 FontAwesome、Material Icons) -- ❌ **禁止操作按鈕不包裹 `` 權限元件** - ---- - -## 1. 色彩系統 - -### 主題色(動態租戶品牌色,由 `AuthenticatedLayout` 自動注入) - -| Tailwind Class | 用途 | -|---|---| -| `*-primary-main` | 主色:按鈕、連結、強調 | -| `*-primary-dark` | Hover 狀態 | -| `*-primary-light` | 次要強調 | -| `*-primary-lightest` | 背景底色、Active 狀態 | - -### 灰階與狀態色 - -直接參考 `resources/css/app.css` 中定義的 `--grey-0` ~ `--grey-5` 與 `--other-success/error/warning/info` 變數。 - ---- - -## 2. 按鈕規範 - -樣式定義於 `resources/css/app.css`,按鈕必須使用以下類別: - -| 類型 | 類別 | 用途 | -|---|---|---| -| Filled | `button-filled-primary` | 主要操作(新增、儲存) | -| Filled | `button-filled-success/info/warning/error` | 各狀態操作 | -| Outlined | `button-outlined-primary` | 次要操作(編輯、檢視) | -| Outlined | `button-outlined-error` | 刪除按鈕 | -| Text | `button-text-primary` | 文字連結式按鈕 | - -**尺寸**:表格操作列用 `size="sm"`,一般操作用 `size="default"`,主要 CTA 用 `size="lg"`。 - -**返回按鈕**:放置於標題上方,使用 `variant="outline"` + `className="gap-2 button-outlined-primary"`,搭配 `` 圖標。 - ---- - -## 3. 圖標規範 - -統一使用 `lucide-react`。 - -| 尺寸 | 用途 | -|---|---| -| `h-4 w-4` | 按鈕內、表格操作 | -| `h-5 w-5` | 側邊欄選單 | -| `h-6 w-6` | 頁面標題 | - -常用映射:`Plus`(新增)、`Pencil`(編輯)、`Trash2`(刪除)、`Eye`(檢視)、`Search`(搜尋)、`ArrowLeft`(返回)。 -其餘請參考 `AuthenticatedLayout.tsx` 中的 `allMenuItems` 定義。 - ---- - -## 4. 頁面佈局規範 - -所有頁面遵循以下結構,參考範例:`Pages/Product/Create.tsx`、`Pages/PurchaseOrder/Create.tsx`。 - -**關鍵規則**: -- **外層容器**:`className="container mx-auto p-6 max-w-7xl"` -- **標題樣式**:`text-2xl font-bold text-grey-0 flex items-center gap-2` -- **說明文字**:`text-gray-500 mt-1` -- **麵包屑**:使用 `BreadcrumbItemType`(屬性為 `label`, `href`, `isPage`),不需要包含「首頁」 -- **Input 元件**:不額外設定 focus 樣式,直接使用 `@/Components/ui/input` 的內建樣式 -- **日期顯示**:使用 `resources/js/lib/date.ts` 的 `formatDate` 工具 -- **數字顯示**: - - 所有的金額、數量等數值,應視情境使用千分位格式化(如 `1,234.56`)。 - - **精確顯示**:數值應按實際數值顯示,避免在小數點後出現多餘的零(例如:應顯示 `10.5` 而非 `10.500`)。 - - 數值應與單位(如 `元`、`kg`)保持適當間距或清晰呈現。 - - 負數應視情境明確標示(如使用紅色 `text-other-error` 或負號)。 - - 列表中的數值建議靠右對齊 (text-right),以便於視覺比較。 - ---- - -## 5. 表格規範 - -**容器**:`bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden` -**標題列**:`bg-gray-50`,序號欄 `w-[50px] text-center`,操作欄置中 -**空狀態**:`text-center py-8 text-gray-500`,顯示「無符合條件的資料」 -**操作欄**:`flex items-center justify-center gap-2` - -### 排序(三態切換) - -- 未排序:`ArrowUpDown`(`text-muted-foreground`) -- 升冪:`ArrowUp`(`text-primary`) -- 降冪:`ArrowDown`(`text-primary`) -- 後端必須處理 `sort_by` 與 `sort_order` 參數 -- 參考實作:`Pages/Product/Index.tsx` 的 `handleSort` \ No newline at end of file diff --git a/.agents/skills/activity-logging/SKILL.md b/.agents/skills/activity-logging/SKILL.md new file mode 100644 index 0000000..b7361af --- /dev/null +++ b/.agents/skills/activity-logging/SKILL.md @@ -0,0 +1,285 @@ +--- +name: 操作紀錄實作規範 (Activity Logging Skill) +description: 規範系統內 Activity Log 的實作標準,包含自動名稱解析、複雜單據合併記錄、與前端顯示優化。 +--- + +# 操作紀錄實作規範 (Activity Logging Skill) + +本技能定義了 Star ERP 系統中操作紀錄的最高實作標準,旨在確保每筆日誌都具有「高度可讀性」與「單一性」。 + +--- + +## 1. 啟用 Activity Log (Model 基本設定) + +在 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(); // 若無變動則不記錄 + } +} +``` + +--- + +## 2. `tapActivity` 實作規範 (Backend 核心) + +### 2.1 型別宣告:統一使用 `Contracts\Activity` + +```php +// ✅ 正確:使用介面 +public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) + +// ❌ 禁止:使用具體類別 +public function tapActivity(\Spatie\Activitylog\Models\Activity $activity, string $eventName) +``` + +### 2.2 必須 `toArray()` 避免 Indirect modification error + +```php +// 🚩 核心:轉換為陣列以避免 Indirect modification error +$properties = $activity->properties instanceof \Illuminate\Support\Collection + ? $activity->properties->toArray() + : $activity->properties; + +// ... 操作 $properties ... + +$activity->properties = $properties; // 最後整體回寫 +``` + +### 2.3 Snapshot 快照策略 + +為確保資料被刪除後仍能辨識操作對象,**必須**在 `properties.snapshot` 中儲存關鍵識別資訊。 + +```php +$snapshot = $properties['snapshot'] ?? []; +$snapshot['doc_no'] = $this->doc_no; // 單號 +$snapshot['name'] = $this->name; // 名稱 +$snapshot['warehouse_name'] = $this->warehouse?->name; // 關聯名稱 +$properties['snapshot'] = $snapshot; +``` + +### 2.4 全域 ID 轉名稱邏輯 (ID Resolution) + +所有的 ID(如 `warehouse_id`, `created_by`)在記錄時應自動解析為名稱。 + +#### 模組內 Model:可直接查詢 + +```php +$resolver = function (&$data) { + if (empty($data) || !is_array($data)) return; + + // 同模組內的 Model 可以直接查詢 + 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; + } + } +}; +``` + +#### 跨模組 Model:必須透過 Service Interface + +> [!IMPORTANT] +> 依據跨模組通訊規範,若需解析其他模組的 ID(例如在 `Procurement` 模組中解析 `warehouse_id`), +> **禁止**直接 `Warehouse::find()`,必須透過 Service Interface。 + +```php +// ✅ 正確:透過 Service Interface 取得跨模組資料 +if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) { + $warehouse = app(\App\Modules\Inventory\Contracts\InventoryServiceInterface::class) + ->getWarehouse($data['warehouse_id']); + $data['warehouse_id'] = $warehouse?->name ?? $data['warehouse_id']; +} +``` + +> [!NOTE] +> `Core` 模組的 `User`, `Role`, `Tenant` 屬於全域例外,其他模組可直接查詢。 +> 詳見 [跨模組通訊規範](file:///home/mama/projects/star-erp/.agents/skills/cross-module-communication/SKILL.md)。 + +### 2.5 完整 `tapActivity` 範例(參考 PurchaseOrder) + +```php +public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) +{ + // 🚩 轉換為陣列 + $properties = $activity->properties instanceof \Illuminate\Support\Collection + ? $activity->properties->toArray() + : $activity->properties; + + // 1. Snapshot 快照 + $snapshot = $properties['snapshot'] ?? []; + $snapshot['po_number'] = $this->code; + $snapshot['vendor_name'] = $this->vendor?->name; + $properties['snapshot'] = $snapshot; + + // 2. ID 轉名稱 + $resolver = function (&$data) { + if (empty($data) || !is_array($data)) return; + + // 全域例外:User 可直接查 + foreach (['user_id', 'created_by', 'updated_by'] as $f) { + if (isset($data[$f]) && is_numeric($data[$f])) { + $data[$f] = \App\Modules\Core\Models\User::find($data[$f])?->name ?? $data[$f]; + } + } + // 同模組:可直接查 + if (isset($data['vendor_id']) && is_numeric($data['vendor_id'])) { + $data['vendor_id'] = Vendor::find($data['vendor_id'])?->name ?? $data['vendor_id']; + } + // 跨模組:必須透過 Service Interface + if (isset($data['warehouse_id']) && is_numeric($data['warehouse_id'])) { + $warehouse = app(\App\Modules\Inventory\Contracts\InventoryServiceInterface::class) + ->getWarehouse($data['warehouse_id']); + $data['warehouse_id'] = $warehouse?->name ?? $data['warehouse_id']; + } + }; + + if (isset($properties['attributes'])) $resolver($properties['attributes']); + if (isset($properties['old'])) $resolver($properties['old']); + + // 3. 合併 activityProperties (手動傳入的 items_diff 等) + if (!empty($this->activityProperties)) { + $properties = array_merge($properties, $this->activityProperties); + } + + $activity->properties = $properties; +} +``` + +--- + +## 3. 複雜操作的日誌合併 (Log Consolidation) + +當一個操作同時涉及「多個品項異動」與「單據狀態變更」時,**嚴禁**產生多筆重複日誌。 + +### 3.1 手動記錄必須自行過濾差異 + +```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'); +} +``` + +### 3.2 `saveQuietly()` + 手動日誌 合併策略 + +```php +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'); +}); +``` + +> [!WARNING] +> 使用 `saveQuietly()` 會繞過 Model Events(如自動單號產生)。 +> 若 Model 有 `creating`/`updating` 事件產生單號,需在 Service 中手動處理。 + +--- + +## 4. 後端 Controller 映射 (Subject Map) + +新增 Model 時,必須同步在 `ActivityLogController::getSubjectMap()` 加入中文映射。 + +**位置**: `app/Modules/Core/Controllers/ActivityLogController.php` + +```php +private function getSubjectMap() +{ + return [ + 'App\Modules\Inventory\Models\Product' => '商品', + 'App\Modules\Finance\Models\UtilityFee' => '公共事業費', + // ... 新增此行 + ]; +} +``` + +--- + +## 5. 前端介面規範 (Frontend) + +### 5.1 標籤命名規範 (Field Labels) + +前端顯示應完全移除「ID」字眼,提供最友善的閱讀體驗。 + +**位置**: `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` + +```typescript +const fieldLabels: Record = { + warehouse_id: '倉庫', // ❌ 禁用「倉庫 ID」 + created_by: '建立者', // ❌ 禁用「建立者 ID」 + completed_by: '完成者', + status: '狀態', + // 新增 Model 的欄位翻譯 ... +}; +``` + +### 5.2 `nameParams` 必須在兩處同步更新 + +> [!IMPORTANT] +> `nameParams` 在 `LogTable.tsx` 和 `ActivityDetailDialog.tsx` 中各有一份, +> 新增時**必須兩處同步更新**,否則會導致列表與詳情頁顯示不一致。 + +| 檔案 | 用途 | +|---|---| +| `resources/js/Components/ActivityLog/LogTable.tsx` | 列表頁的描述文字 | +| `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` | 對話框標題 | + +### 5.3 特殊結構顯示 + +* **品項異動**:前端已能渲染 `items_diff` 結構,以「品項名稱 + 數值變動」方式呈現表格。 +* **顯示過濾邏輯**(已內建於 `ActivityDetailDialog`): + - **Created**: 顯示初始化欄位 + - **Updated**: 僅顯示有變動的欄位 (`isChanged` 判斷) + - **Deleted**: 顯示刪除前的完整資料 + +--- + +## 6. 開發檢核清單 (Checklist) + +- [ ] **Model**: 是否已設定 `logOnlyDirty` + `dontSubmitEmptyLogs`? +- [ ] **Model**: `tapActivity` 型別是否使用 `Contracts\Activity`? +- [ ] **Model**: `tapActivity` 是否已使用 `toArray()` 處理 Collection? +- [ ] **Model**: 是否已實作 Snapshot(關鍵識別資訊)? +- [ ] **Model**: ID 轉名稱是否遵守跨模組規範(Core 例外,其餘需透過 Interface)? +- [ ] **Service**: 是否使用 `saveQuietly()` 搭配手動 `activity()` 避免重複日誌? +- [ ] **Controller**: `ActivityLogController::getSubjectMap()` 是否已新增 Model 中文映射? +- [ ] **UI**: `fieldLabels` 是否已新增欄位中文翻譯? +- [ ] **UI**: `nameParams` 是否已在 `LogTable` 和 `ActivityDetailDialog` 兩處同步? diff --git a/.agents/rules/cross-module-communication.md b/.agents/skills/cross-module-communication/SKILL.md similarity index 64% rename from .agents/rules/cross-module-communication.md rename to .agents/skills/cross-module-communication/SKILL.md index 9d2ae8a..1c83b74 100644 --- a/.agents/rules/cross-module-communication.md +++ b/.agents/skills/cross-module-communication/SKILL.md @@ -1,7 +1,3 @@ ---- -trigger: always_on ---- - --- name: 跨模組調用與通訊規範 (Cross-Module Communication) description: 規範 Laravel Modular Monolith 架構下,不同業務模組中如何彼此調用資料與邏輯,包含禁止項目、Interface 實作、與 Service 綁定規則。 @@ -14,12 +10,8 @@ description: 規範 Laravel Modular Monolith 架構下,不同業務模組中 ## 🚫 絕對禁止的行為 (Strict Prohibitions) * **禁止跨模組 Eloquent 關聯(例外除外)** - * **錯誤**:在 `app/Modules/Sales/Models/Order.php` 中撰寫 `public function product() { return $this->belongsTo(\App\Modules\Inventory\Models\Product::class); }`。 - * **原因**:這會造成資料庫查詢的強耦合。如果 `Inventory` 模組修改了 Schema,`Sales` 模組會無預警崩壞。 * **禁止跨模組直接引入 (use) Model** - * **錯誤**:在 `app/Modules/Procurement/Controllers/PurchaseOrderController.php` 頂端寫 `use App\Modules\Inventory\Models\Warehouse;`。 * **禁止跨模組直接實例化 (new) Service** - * **錯誤**:`$inventoryService = new \App\Modules\Inventory\Services\InventoryService();` --- @@ -28,9 +20,9 @@ description: 規範 Laravel Modular Monolith 架構下,不同業務模組中 雖然我們嚴格禁止跨模組直接相依,但為了開發效率與框架機制的完整性,**`Core` 模組下的特定基礎設施模型 (Infrastructure Models) 被視為全域例外**。 其他業務模組 **可以** 透過 Eloquent (`belongsTo` / `hasMany`) 直接關聯以下 Model: -1. **`App\Modules\Core\Models\User`**:因為幾乎所有表都有 `created_by` / `updated_by`,直接關聯可保留 `with('creator')` 等便利性。 -2. **`App\Modules\Core\Models\Role`**:權限判定已深度整合至系統底層。 -3. **`App\Modules\Core\Models\Tenant`**:多租戶架構 (Tenancy) 的核心基石,底層查詢會頻繁依賴。 +1. **`App\Modules\Core\Models\User`** +2. **`App\Modules\Core\Models\Role`** +3. **`App\Modules\Core\Models\Tenant`** > **⚠️ 注意**:這項例外是單向的。`Core` 模組內的業務邏輯(如 `DashboardController`)**絕對不能**反過來直接 `use` 外部業務模組的 Model,仍必須透過外部模組的 Service Interface 來索取資料。 @@ -45,18 +37,12 @@ description: 規範 Laravel Modular Monolith 架構下,不同業務模組中 如果 `Inventory` 模組需要提供功能給外部使用,請在 `app/Modules/Inventory/Contracts/` 建立 Interface 檔案。 ```php -// app/Modules/Inventory/Contracts/InventoryServiceInterface.php namespace App\Modules\Inventory\Contracts; use Illuminate\Support\Collection; interface InventoryServiceInterface { - /** - * 取得可用的倉庫清單 - * - * @return Collection 包含每個倉庫的 id, name, code 等基本資料 - */ public function getActiveWarehouses(): Collection; } ``` @@ -66,7 +52,6 @@ interface InventoryServiceInterface 由 `Inventory` 模組自己的 Service 來實作上述介面。 ```php -// app/Modules/Inventory/Services/InventoryService.php namespace App\Modules\Inventory\Services; use App\Modules\Inventory\Contracts\InventoryServiceInterface; @@ -77,8 +62,6 @@ class InventoryService implements InventoryServiceInterface { public function getActiveWarehouses(): Collection { - // 建議只取出需要的欄位,或者轉換為 DTO / 陣列 - // 避免將完整的 Eloquent Model 實例拋出模組外 return Warehouse::where('is_active', true) ->select(['id', 'name', 'code']) ->get(); @@ -89,7 +72,6 @@ class InventoryService implements InventoryServiceInterface 然後進入 `app/Modules/Inventory/InventoryServiceProvider.php` 完成綁定: ```php -// app/Modules/Inventory/InventoryServiceProvider.php namespace App\Modules\Inventory; use Illuminate\Support\ServiceProvider; @@ -100,7 +82,6 @@ class InventoryServiceProvider extends ServiceProvider { public function register(): void { - // 綁定介面與實體 $this->app->bind(InventoryServiceInterface::class, InventoryService::class); } } @@ -108,10 +89,9 @@ class InventoryServiceProvider extends ServiceProvider ### Step 3: 調用方透過依賴注入 (DI) 使用服務 -當 `Procurement` 模組需要取得倉庫資料時,禁止直接 new 服務或呼叫倉庫 Model。必須透過**建構子注入**或**方法注入**取得 `InventoryServiceInterface`。 +當 `Procurement` 模組需要取得倉庫資料時,必須透過**建構子注入**或**方法注入**取得 `InventoryServiceInterface`。 ```php -// app/Modules/Procurement/Controllers/PurchaseOrderController.php namespace App\Modules\Procurement\Controllers; use App\Http\Controllers\Controller; @@ -120,14 +100,12 @@ use Inertia\Inertia; class PurchaseOrderController extends Controller { - // 透過建構子注入介面 public function __construct( protected InventoryServiceInterface $inventoryService ) {} public function create() { - // 僅能呼叫介面有定義的方法 $warehouses = $this->inventoryService->getActiveWarehouses(); return Inertia::render('Procurement/PurchaseOrder/Create', [ @@ -141,14 +119,11 @@ class PurchaseOrderController extends Controller ## ⚠️ 跨模組資料回傳的注意事項 (Data Hydration) -* **回傳純粹資料**:為了防止其他模組意外觸發 Lazy Loading (`$item->product->name`),請盡量在 Service 中就用 `with()` 載入好關聯,或者直接轉為原生的 Array、`stdClass`、或具體的 DTO。 -* **手動組合 (Manual Hydration)**:若某個頁面需要合併兩個模組的資料,這也是被允許的,但必須在 Controller 層級呼叫兩個不同的 Service Interface 後,手動合併。 +* **回傳純粹資料**:建議在 Service 中用 `with()` 載入好關聯,或者直接轉為原生的 Array 或有具體結構的 DTO,避免依賴 Lazy Loading。 +* **手動組合 (Manual Hydration)**:若某個頁面需要合併兩個模組的資料,必須在 Controller 層級呼叫兩個不同的 Service Interface 後,手動合併。 ### 範例:手動合併資料 ```php -// 錯誤示範(禁止在 OrderService 中去查使用者的關聯) -$orders = Order::with('user')->get(); // 如果 user 表在 Core 模組,這是不允許的 - // 正確示範:在各自模組取資料,並手動組裝 $orders = $this->orderService->getOrders(); $userIds = $orders->pluck('user_id')->unique()->toArray(); diff --git a/.agents/rules/git-workflows.md b/.agents/skills/git-workflows/SKILL.md similarity index 90% rename from .agents/rules/git-workflows.md rename to .agents/skills/git-workflows/SKILL.md index e0edfc0..388a709 100644 --- a/.agents/rules/git-workflows.md +++ b/.agents/skills/git-workflows/SKILL.md @@ -1,5 +1,6 @@ --- -trigger: always_on +name: Git 分支管理與開發規範 (Git Workflow) +description: 規範開發過程中的 Git 分支架構、合併限制、環境部署流程以及提交訊息格式。 --- # Git 分支管理與開發規範 (Git Workflow) @@ -46,4 +47,4 @@ trigger: always_on --- > [!IMPORTANT] -> 身為 AI 助手 (Antigravity),我會監控合併對象與當前時間。若您的命令涉及合併至 `main` 且不在允許時段內,我會優先進行安全提醒。 \ No newline at end of file +> 身為 AI 助手 (Antigravity),我會監控合併對象與當前時間。若您的命令涉及合併至 `main` 且不在允許時段內,我會優先進行安全提醒。 diff --git a/.agents/skills/permission-management/SKILL.md b/.agents/skills/permission-management/SKILL.md new file mode 100644 index 0000000..b1a9796 --- /dev/null +++ b/.agents/skills/permission-management/SKILL.md @@ -0,0 +1,206 @@ +--- +name: 權限管理與實作規範 +description: 為新功能實作權限控制的完整流程規範,包含後端 Seeder 設定、Middleware 路由保護與前端權限判斷。 +--- + +# 權限管理與實作規範 + +本文件說明如何在新增功能時,一併實作完整的權限控制機制。專案採用 `spatie/laravel-permission` 套件進行權限管理。 + +--- + +## 1. 定義權限 (Backend Seeder) + +所有權限皆定義於 `database/seeders/PermissionSeeder.php`。 + +### 步驟: + +1. 開啟 `database/seeders/PermissionSeeder.php`。 +2. 在 `$permissions` 關聯陣列中新增功能對應的權限。 + * **命名慣例**:`{resource}.{action}`(例如:`system.view_logs`, `products.create`) + * **格式**:`'權限字串' => '中文動作名稱'` + * 常用動作:`view`, `create`, `edit`, `delete`, `approve`, `cancel`, `export` +3. 在下方「角色分配」區段,將新權限分配給適合的角色。 + +### 範例: + +```php +// 1. 新增權限(注意:是 key => value 格式) +$permissions = [ + // ... 現有權限 + 'utility_fees.view' => '檢視', + 'utility_fees.create' => '建立', + 'utility_fees.edit' => '編輯', + 'utility_fees.delete' => '刪除', +]; + +// 2. 分配給角色 +$admin->givePermissionTo([ + // ... 現有權限 + 'utility_fees.view', 'utility_fees.create', 'utility_fees.edit', 'utility_fees.delete', +]); +``` + +### 現有角色定義: + +| 角色 | 說明 | 權限範圍 | +|---|---|---| +| `super-admin` | 系統管理員 | 自動擁有所有權限(`Permission::all()`) | +| `admin` | 一般管理員 | 大部分權限(除角色管理外) | +| `warehouse-manager` | 倉庫管理員 | 庫存、盤點、調撥、進貨、門市叫貨 | +| `purchaser` | 採購人員 | 商品檢視、採購單、退貨、供應商、進貨 | +| `viewer` | 檢視人員 | 僅限各模組的 `.view` 權限 | + +--- + +## 2. 套用資料庫變更 (Multi-tenancy) + +修改 Seeder 後,必須在**中央與所有租戶**同步執行。 + +```bash +# 對所有租戶執行 Seeder +./vendor/bin/sail php artisan tenants:seed --class=PermissionSeeder +``` + +> [!WARNING] +> 僅執行 `db:seed` 只會更新中央資料庫。務必使用 `tenants:seed` 確保所有租戶同步。 + +--- + +## 3. 路由保護 (Backend Middleware) + +路由保護定義在各模組自己的 `app/Modules/{ModuleName}/Routes/web.php` 中。 + +> [!IMPORTANT] +> 路由檔在各模組內(如 `app/Modules/Finance/Routes/web.php`),**不是**全域的 `routes/web.php`。 + +### 範例: + +```php +// 單一權限保護 +Route::middleware('permission:utility_fees.view')->group(function () { + Route::get('/utility-fees', [UtilityFeeController::class, 'index'])->name('utility-fees.index'); + Route::get('/utility-fees/{utilityFee}', [UtilityFeeController::class, 'show'])->name('utility-fees.show'); +}); + +// 巢狀權限群組 +Route::middleware('permission:utility_fees.create')->group(function () { + Route::get('/utility-fees/create', [UtilityFeeController::class, 'create'])->name('utility-fees.create'); + Route::post('/utility-fees', [UtilityFeeController::class, 'store'])->name('utility-fees.store'); +}); + +// 單行 middleware +Route::delete('/utility-fees/{utilityFee}', [UtilityFeeController::class, 'destroy']) + ->middleware('permission:utility_fees.delete') + ->name('utility-fees.destroy'); +``` + +--- + +## 4. 配置權限群組名稱 (Backend UI Config) + +為了讓新權限在「角色與權限」管理介面中正確分組並顯示中文標題,需修改 Controller。 + +**位置**: `app/Modules/Core/Controllers/RoleController.php` → `getGroupedPermissions()` + +```php +$groupDefinitions = [ + 'products' => '商品資料管理', + 'warehouses' => '倉庫管理', + 'inventory' => '庫存資料管理', + // ... + 'utility_fees' => '公共事業費管理', // ✅ 新增此行 +]; +``` + +> [!NOTE] +> 未加入 `$groupDefinitions` 的權限群組仍會顯示,但標題會以原始 key(英文)呈現。 + +--- + +## 5. 前端權限判斷 (React) + +### 5.1 方式一:`usePermission` Hook(在邏輯中判斷) + +**位置**: `resources/js/hooks/usePermission.ts` + +```tsx +import { usePermission } from "@/hooks/usePermission"; + +export default function ProductIndex() { + const { can, canAny, isSuperAdmin } = usePermission(); + + return ( +
+ {can('products.create') && } + {canAny(['products.edit', 'products.delete']) && } +
+ ); +} +``` + +#### Hook 完整介面: + +| 方法 | 說明 | +|---|---| +| `can(permission)` | 檢查是否擁有**指定**權限 | +| `canAny(permissions[])` | 檢查是否擁有**任一**權限 | +| `canAll(permissions[])` | 檢查是否擁有**所有**權限 | +| `hasRole(role)` | 檢查是否擁有**指定**角色 | +| `hasAnyRole(roles[])` | 檢查是否擁有**任一**角色 | +| `hasAllRoles(roles[])` | 檢查是否擁有**所有**角色 | +| `isSuperAdmin()` | 是否為超級管理員 | + +> 所有方法對 `super-admin` 角色自動回傳 `true`。 + +### 5.2 方式二:`` / `` / `` 元件(在 JSX 中包裹) + +**位置**: `resources/js/Components/Permission/Can.tsx` + +```tsx +import { Can, HasRole, CanAll } from '@/Components/Permission/Can'; + +// 單一權限 + + + + +// 任一權限(OR 邏輯) + + + + +// 所有權限都必須有(AND 邏輯) + + + + +// 角色判斷 + + 管理後台 + + +// Fallback 支援 +無權限}> + + +``` + +> [!IMPORTANT] +> UI 規範要求:所有可操作按鈕(新增、編輯、刪除)**必須**包裹 `` 元件或使用 `can()` 判斷。 +> 詳見 [UI 統一規範](file:///home/mama/projects/star-erp/.agents/skills/ui-consistency/SKILL.md)。 + +--- + +## 6. 開發檢核清單 (Checklist) + +### 後端 +- [ ] `PermissionSeeder.php` 已新增權限字串(`'key' => '中文動作名稱'` 格式)。 +- [ ] `PermissionSeeder.php` 已將新權限分配給 `admin` 及其他適用角色。 +- [ ] 已執行 `./vendor/bin/sail php artisan tenants:seed --class=PermissionSeeder` 同步所有租戶。 +- [ ] `RoleController.php` 的 `$groupDefinitions` 已新增權限群組中文名稱。 +- [ ] 模組路由 (`app/Modules/{ModuleName}/Routes/web.php`) 已加上 `middleware('permission:...')` 保護。 + +### 前端 +- [ ] 頁面按鈕已使用 `usePermission` Hook 或 `` 元件進行權限控制。 +- [ ] 所有可操作按鈕都包裹於權限判斷中(符合 UI 統一規範)。 diff --git a/.gemini/antigravity/skills/ui-consistency/SKILL.md b/.agents/skills/ui-consistency/SKILL.md similarity index 94% rename from .gemini/antigravity/skills/ui-consistency/SKILL.md rename to .agents/skills/ui-consistency/SKILL.md index e5ef3bd..1df0d23 100644 --- a/.gemini/antigravity/skills/ui-consistency/SKILL.md +++ b/.agents/skills/ui-consistency/SKILL.md @@ -80,7 +80,7 @@ tooltip
...
// ❌ 錯誤:寫死色碼 (會導致租戶無法換色) -
...
+
...
``` ### 2.2 灰階 (Grey Scale) @@ -319,7 +319,7 @@ import { Plus, Pencil, Trash2, Users } from 'lucide-react'; // 頁面標題

- + 使用者管理

@@ -584,7 +584,7 @@ export default function ResourceIndex() {

- + 頁面標題

@@ -781,7 +781,44 @@ import { SearchableSelect } from "@/Components/ui/searchable-select"; - **Select / SearchableSelect**: 必須確保 Trigger 按鈕高度為 `h-9` - **禁止使用**: 除非有特殊設計需求,否則避免使用 `h-10` (40px) 或其他非標準高度。 -## 11.6 日期輸入框樣式 (Date Input Style) +## 11.6 日期顯示規範 (Date Display) + +前端顯示日期時**禁止直接顯示原始 ISO 字串**(如 `2024-03-06T08:30:00.000000Z`),必須使用 `resources/js/lib/date.ts` 提供的工具函式。 + +### 可用函式 + +| 函式 | 說明 | 輸出範例 | +|---|---|---| +| `formatDate(dateStr)` | **智慧格式**:自動判斷是否包含時間 | `2024-03-06` 或 `2024-03-06 08:30:00` | +| `formatDate(dateStr, 'yyyy-MM-dd')` | 指定格式輸出 | `2024-03-06` | +| `formatDateOnly(dateStr)` | 強制僅顯示日期 | `2024-03-06` | + +### 智慧格式切換邏輯 + +`formatDate` 會自動判斷原始資料: +- 若時間部分為 `00:00:00`(通常代表後端僅提供日期)→ 僅顯示 `YYYY-MM-DD` +- 若時間部分有值 → 顯示 `YYYY-MM-DD HH:mm:ss` +- 若輸入為 `null` / `undefined` / 無效字串 → 顯示 `"-"` + +### 使用範例 + +```tsx +import { formatDate, formatDateOnly } from "@/lib/date"; + +// ✅ 正確:使用 formatDate 自動判斷 +{formatDate(item.created_at)} // → "2024-03-06 08:30:00" +{formatDate(item.transaction_date)} // → "2024-03-06"(因為時間為 00:00:00) + +// ✅ 正確:強制只顯示日期 +{formatDateOnly(item.due_date)} // → "2024-03-06" + +// ❌ 禁止:直接顯示原始 ISO 字串 +{item.created_at} // → "2024-03-06T08:30:00.000000Z" 😱 +``` + +--- + +## 11.7 日期輸入框樣式 (Date Input Style) 日期輸入框應採用「**左側裝飾圖示 + 右側原生操作**」的配置,以保持視覺一致性並保留瀏覽器原生便利性。 @@ -805,7 +842,7 @@ import { Input } from "@/Components/ui/input";

``` -## 11.7 搜尋選單樣式 (SearchableSelect Style) +## 11.8 搜尋選單樣式 (SearchableSelect Style) `SearchableSelect` 元件在表單或篩選列中使用時,高度必須設定為 `h-9` 以與輸入框對齊。 @@ -816,7 +853,7 @@ import { Input } from "@/Components/ui/input"; /> ``` -## 11.8 篩選列規範 (Filter Bar Norms) +## 11.9 篩選列規範 (Filter Bar Norms) 列表頁面的篩選區域(Filter Bar)應遵循以下規範以節省空間並保持層級清晰: diff --git a/.gemini/antigravity/skills/activity-logging/SKILL.md b/.gemini/antigravity/skills/activity-logging/SKILL.md deleted file mode 100644 index 854df5a..0000000 --- a/.gemini/antigravity/skills/activity-logging/SKILL.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -name: 操作紀錄實作規範 -description: 規範系統內 Activity Log 的實作標準,包含後端資料過濾、快照策略、與前端顯示邏輯。 ---- - -# 操作紀錄實作規範 - -本文件說明如何在開發新功能時,依據系統規範實作 `spatie/laravel-activitylog` 操作紀錄,確保資料儲存效率與前端顯示一致性。 - -## 1. 後端實作標準 (Backend) - -所有 Model 之操作紀錄應遵循「僅儲存變動資料」與「保留關鍵快照」兩大原則。 - -### 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` (推薦)** - -```php -public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName) -{ - $properties = $activity->properties; - $snapshot = $properties['snapshot'] ?? []; - - // 保存關鍵關聯名稱 (避免關聯資料刪除後 ID 失效) - $snapshot['category_name'] = $this->category ? $this->category->name : null; - $snapshot['po_number'] = $this->code; // 儲存單號 - - // 保存自身名稱 (Context) - $snapshot['name'] = $this->name; - - $properties['snapshot'] = $snapshot; - $activity->properties = $properties; -} -``` - -## 2. 顯示名稱映射 (UI Mapping) - -### 2.1 對象名稱映射 (Mapping) - -需在 `ActivityLogController.php` 中設定 Model 與中文名稱的對應,讓前端列表能顯示中文對象(如「公共事業費」而非 `UtilityFee`)。 - -**位置**: `app/Http/Controllers/Admin/ActivityLogController.php` - -```php -protected function getSubjectMap() -{ - return [ - 'App\Modules\Inventory\Models\Product' => '商品', - 'App\Modules\Finance\Models\UtilityFee' => '公共事業費', // ✅ 新增映射 - ]; -} -``` - -### 2.2 欄位名稱中文化 (Field Translation) - -需在前端 `ActivityDetailDialog` 中設定欄位名稱的中文翻譯。 - -**位置**: `resources/js/Components/ActivityLog/ActivityDetailDialog.tsx` - -```typescript -const fieldLabels: Record = { - // ... 既有欄位 - 'transaction_date': '費用日期', - 'category': '費用類別', - 'amount': '金額', -}; -``` - -## 3. 前端顯示邏輯 (Frontend) - -### 3.1 列表描述生成 (Description Generation) - -前端 `LogTable.tsx` 會依據 `properties.snapshot` 中的欄位自動組建描述(例如:「Admin 新增 電話費 公共事業費」)。 - -若您的 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`? diff --git a/.gemini/antigravity/skills/permission-management/SKILL.md b/.gemini/antigravity/skills/permission-management/SKILL.md deleted file mode 100644 index 78c311d..0000000 --- a/.gemini/antigravity/skills/permission-management/SKILL.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -name: 權限管理與實作規範 -description: 為新功能實作權限控制的完整流程規範,包含後端 Seeder 設定、Middleware 路由保護與前端權限判斷。 ---- - -# 權限管理與實作規範 - -本文件說明如何在新增功能時,一併實作完整的權限控制機制。專案採用 `spatie/laravel-permission` 套件進行權限管理。 - -## 1. 定義權限 (Backend) - -所有權限皆定義於 `database/seeders/PermissionSeeder.php`。 - -### 步驟: - -1. 開啟 `database/seeders/PermissionSeeder.php`。 -2. 在 `$permissions` 陣列中新增功能對應的權限字串。 - * **命名慣例**:`{resource}.{action}` (例如:`system.view_logs`, `products.create`) - * 常用動作:`view`, `create`, `edit`, `delete`, `publish`, `export` -3. 在下方「角色分配」區段,將新權限分配給適合的角色。 - * `super-admin`:通常擁有所有權限(程式碼中 `Permission::all()` 自動涵蓋,無需手動新增)。 - * `admin`:通常擁有大部分權限。 - * 其他角色 (`warehouse-manager`, `purchaser`, `viewer`):依業務邏輯分配。 - -### 範例: - -```php -// 1. 新增權限字串 -$permissions = [ - // ... 現有權限 - 'system.view_logs', // 新增:檢視系統日誌 -]; - -// ... - -// 2. 分配給角色 -$admin->givePermissionTo([ - // ... 現有權限 - 'system.view_logs', -]); -``` - -## 2. 套用資料庫變更 - -修改 Seeder 後,必須重新執行 Seeder 以將權限寫入資料庫。 - -```bash -# 對於所有租戶執行 Seeder (開發環境) -php artisan tenants:seed --class=PermissionSeeder -``` - -## 3. 路由保護 (Backend Middleware) - -在 `routes/web.php` 中,使用 `permission:{name}` middleware 保護路由。 - -### 範例: - -```php -// 單一權限保護 -Route::get('/logs', [LogController::class, 'index']) - ->middleware('permission:system.view_logs') - ->name('logs.index'); - -// 路由群組保護 -Route::middleware('permission:products.view')->group(function () { - // ... -}); - -// 多重權限 (OR 邏輯:有其一即可) -Route::middleware('permission:products.create|products.edit')->group(function () { - // ... -}); -``` - -## 4. 前端權限判斷 (React Component) - -使用自訂 Hook `usePermission` 來控制 UI 元素的顯示(例如:隱藏沒有權限的按鈕)。 - -### 引入 Hook: - -```tsx -import { usePermission } from "@/hooks/usePermission"; -``` - -### 使用方式: - -```tsx -export default function ProductIndex() { - const { can } = usePermission(); - - return ( -
-

商品列表

- - {/* 只有擁有 create 權限才顯示按鈕 */} - {can('products.create') && ( - - )} - - {/* 組合判斷 */} - {can('products.edit') && } -
- ); -} -``` - -### 權限 Hook 介面說明: - -- `can(permission: string)`: 檢查當前使用者是否擁有指定權限。 -- `canAny(permissions: string[])`: 檢查當前使用者是否擁有陣列中**任一**權限。 -- `hasRole(role: string)`: 檢查當前使用者是否擁有指定角色。 - -## 5. 配置權限群組名稱 (Backend UI Config) - -為了讓新權限在「角色與權限」管理介面中顯示正確的中文分組標題,需修改 Controller 設定。 - -### 步驟: - -1. 開啟 `app/Http/Controllers/Admin/RoleController.php`。 -2. 找到 `getGroupedPermissions` 方法。 -3. 在 `$groupDefinitions` 陣列中,新增 `{resource}` 對應的中文名稱。 - -### 範例: - -```php -$groupDefinitions = [ - 'products' => '商品資料管理', - // ... - 'utility_fees' => '公共事業費管理', // 新增此行 -]; -``` - -## 檢核清單 - -- [ ] `PermissionSeeder.php` 已新增權限字串。 -- [ ] `PermissionSeeder.php` 已將新權限分配給對應角色。 -- [ ] 已執行 `php artisan tenants:seed --class=PermissionSeeder` 更新資料庫。 -- [ ] `RoleController.php` 已新增權限群組的中文名稱映射。 -- [ ] 後端路由 (`routes/web.php`) 已加上 middleware 保護。 -- [ ] 前端頁面/按鈕已使用 `usePermission` 進行顯示控制。