Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Test

on:
push:
branches:
- '**'

jobs:
test:
name: Run Tests & Coverage
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: vehicle-management/package-lock.json

- name: Install dependencies
working-directory: vehicle-management
run: npm ci

- name: Run tests with coverage
working-directory: vehicle-management
run: npm run test:coverage

- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: vehicle-management/coverage/
retention-days: 30
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ Thumbs.db
# Claude Code — 保留進版控
# .claude/
# openspec/

# PR review draft
pr-review.md
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
testPathIgnorePatterns: ['/node_modules/', '/vehicle-management/'],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-04
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
## Context

VMS 目前缺乏稽核軌跡(audit trail)功能,管理者無法追蹤使用者在系統內執行了哪些操作(例如登入、車輛新增/修改/刪除、員工資料異動)。此變更新增一個僅限 admin 存取的「使用者紀錄」頁面,以表格形式呈現所有操作日誌,並支援篩選與分頁。

由於專案沒有真實後端,所有資料均由 MSW mock。

## Goals / Non-Goals

**Goals:**
- 在 `/activity-logs` 新增受保護的管理者頁面
- 提供 MSW mock API `GET /api/activity-logs`,回傳分頁日誌資料
- 支援依使用者與操作類型篩選
- 與現有 Layout / ProtectedRoute 整合,不改變 auth 流程

**Non-Goals:**
- 真實後端儲存或即時更新
- 日誌匯出(CSV / PDF)
- 使用者自助查詢自己的紀錄

## Decisions

### 1. 路由與存取控制:使用現有 ProtectedRoute + requiredRole

- **決定**:新增 `/activity-logs` 路由,套用 `<ProtectedRoute requiredRole="admin">`
- **理由**:與 `/employees` 的保護方式一致,不需引入新的守衛機制
- **替代方案**:在 Layout 層做 role check — 較脆弱且不符合現有模式

### 2. Mock 資料結構:獨立的 `activityLogs.ts` mock 資料檔

- **決定**:將 mock 日誌資料放在 `src/mocks/activityLogs.ts`,handler 從該檔 import
- **理由**:與 `data.ts` 的分離模式一致,handler 保持薄薄的(thin);資料易於維護
- **替代方案**:直接在 handler 內 inline — 難以閱讀,未來不易擴充

### 3. API 設計:支援 query string 篩選與分頁

- **決定**:`GET /api/activity-logs?username=&action=&page=1&pageSize=20`
- **理由**:由 MSW handler 在記憶體中對 mock 陣列做過濾,行為與真實 API 一致
- **替代方案**:前端全量取回後再過濾 — 不符合 API 設計慣例,未來難以換成真實後端

### 4. 日誌資料模型

```ts
type ActivityAction = 'login' | 'logout' | 'vehicle_create' | 'vehicle_update' | 'vehicle_delete' | 'employee_create' | 'employee_update' | 'employee_delete'

interface ActivityLog {
id: string
timestamp: string // ISO 8601
username: string
role: 'admin' | 'user'
action: ActivityAction
target?: string // e.g., 車牌號碼或員工姓名
description: string // 人類可讀描述(繁體中文)
}
```

### 5. UI 元件:表格 + 篩選列

- 使用 shadcn/ui Table 元件(與車輛/員工頁一致)
- 篩選列:使用者下拉選單 + 操作類型下拉選單 + 重設按鈕
- 分頁元件(上/下頁按鈕 + 目前頁數顯示)

## Risks / Trade-offs

- **靜態 mock 資料** → 新增/修改/刪除車輛或員工時不會自動產生新日誌。已知限制,屬 Non-goal。
- **分頁在前端計算** → mock handler 對陣列做 slice,行為符合預期但非資料庫分頁。可接受。
- **無法測試真實時序** → 日誌時間戳為靜態假資料,不反映實際操作順序。

## Migration Plan

無需資料遷移。新增路由與 MSW handler 不影響現有功能。部署步驟:
1. 新增 mock 資料與 handler
2. 新增頁面元件與路由
3. 更新 Layout 導覽列

## Open Questions

- 是否需要顯示 IP 位址欄位?(目前 mock 資料不包含,可後續擴充)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Why

管理者目前無法追蹤系統內的使用者操作行為,缺乏可視性與稽核能力。新增使用者紀錄頁面,讓管理者能夠查看所有帳號的登入、登出及資料操作等活動日誌,提升系統安全性與可管理性。

## What Changes

- 新增「使用者紀錄」頁面(限 admin 角色存取)
- 在側邊導覽列新增「使用者紀錄」入口
- 紀錄列表支援依時間排序、依使用者與操作類型篩選
- 新增 MSW mock API `/api/activity-logs` 回傳活動日誌資料

## Capabilities

### New Capabilities

- `user-activity-log`: 使用者活動日誌頁面,顯示所有帳號的操作紀錄,支援篩選與分頁,限 admin 存取

### Modified Capabilities

- `auth`: 登入與登出事件需記錄至活動日誌(新增 mock 資料中的 auth 事件類型)

## Non-goals

- 不實作真實後端儲存,所有資料皆為 MSW mock
- 不支援日誌匯出(CSV / PDF)
- 不實作即時(WebSocket)更新

## Impact

- **新增檔案**:`vehicle-management/src/pages/activity-logs.tsx`、`vehicle-management/src/mocks/activityLogs.ts`
- **修改檔案**:`vehicle-management/src/mocks/handlers.ts`(新增 `/api/activity-logs` handler)、`vehicle-management/src/App.tsx`(新增路由)、`vehicle-management/src/components/Layout.tsx`(新增導覽連結)
- **受影響角色**:僅 admin
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## ADDED Requirements

### Requirement: 管理者可存取使用者紀錄頁面
系統 SHALL 在路由 `/activity-logs` 提供一個使用者活動紀錄頁面,且僅允許角色為 `admin` 的使用者存取。

#### Scenario: 管理者存取使用者紀錄頁面
- **WHEN** 角色為 `admin` 的使用者導航至 `/activity-logs`
- **THEN** 系統顯示使用者紀錄頁面,包含活動日誌列表

#### Scenario: 一般使用者嘗試存取使用者紀錄頁面
- **WHEN** 角色為 `user` 的使用者導航至 `/activity-logs`
- **THEN** 系統重定向至 `/`(儀表板),不顯示紀錄內容

### Requirement: 系統顯示活動日誌列表
系統 SHALL 以表格形式顯示所有使用者的操作紀錄,每筆紀錄包含:時間、使用者名稱、角色、操作類型、操作對象(若有)、描述。

#### Scenario: 日誌列表正常載入
- **WHEN** 管理者進入使用者紀錄頁面
- **THEN** 系統顯示活動日誌表格,按時間由新至舊排序,每頁最多顯示 20 筆

#### Scenario: 無符合紀錄
- **WHEN** 篩選條件下無任何符合的日誌
- **THEN** 系統顯示「目前沒有符合的紀錄」提示訊息,不顯示表格列

### Requirement: 管理者可依使用者與操作類型篩選日誌
系統 SHALL 提供篩選列,允許管理者依使用者名稱與操作類型縮小日誌範圍。

#### Scenario: 依使用者篩選
- **WHEN** 管理者從使用者下拉選單選擇特定使用者
- **THEN** 列表僅顯示該使用者的活動紀錄

#### Scenario: 依操作類型篩選
- **WHEN** 管理者從操作類型下拉選單選擇特定類型(如「登入」)
- **THEN** 列表僅顯示該操作類型的紀錄

#### Scenario: 重設篩選條件
- **WHEN** 管理者點擊「重設」按鈕
- **THEN** 所有篩選條件清空,列表回復顯示全部紀錄

### Requirement: 日誌列表支援分頁
系統 SHALL 提供分頁功能,允許管理者在多頁日誌間切換。

#### Scenario: 切換至下一頁
- **WHEN** 管理者點擊「下一頁」按鈕,且後續有更多紀錄
- **THEN** 系統顯示下一頁的紀錄,頁碼更新

#### Scenario: 已在最後一頁
- **WHEN** 管理者在最後一頁
- **THEN** 「下一頁」按鈕為禁用狀態

#### Scenario: 已在第一頁
- **WHEN** 管理者在第一頁
- **THEN** 「上一頁」按鈕為禁用狀態

### Requirement: 系統提供活動日誌 API
系統 SHALL 在 `GET /api/activity-logs` 提供活動日誌資料,支援 query string 參數:`username`、`action`、`page`(預設 1)、`pageSize`(預設 20)。

#### Scenario: 取得全部日誌
- **WHEN** 呼叫 `GET /api/activity-logs` 無任何 query 參數
- **THEN** API 回傳第一頁的所有日誌資料及總筆數

#### Scenario: 依條件篩選日誌
- **WHEN** 呼叫 `GET /api/activity-logs?username=admin&action=login`
- **THEN** API 回傳符合 username=admin 且 action=login 的紀錄,並套用分頁
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## 1. Mock 資料與 API

- [x] 1.1 建立 `vehicle-management/src/mocks/activityLogs.ts`,定義 `ActivityLog` 型別與至少 30 筆涵蓋各 action 類型的靜態 mock 資料
- [x] 1.2 在 `vehicle-management/src/mocks/handlers.ts` 新增 `GET /api/activity-logs` handler,支援 `username`、`action`、`page`、`pageSize` query 參數,回傳分頁結果與 `total` 欄位

## 2. 頁面元件

- [x] 2.1 建立 `vehicle-management/src/pages/activity-logs.tsx`,實作頁面骨架(標題、篩選列、表格、分頁)
- [x] 2.2 實作篩選列:使用者下拉選單(從 mock 資料中取得不重複 username)+ 操作類型下拉選單 + 重設按鈕
- [x] 2.3 實作活動日誌表格,欄位:時間、使用者、角色、操作類型、操作對象、描述
- [x] 2.4 實作分頁控制列:「上一頁」/「下一頁」按鈕 + 目前頁數 / 總頁數顯示,邊界條件下按鈕禁用

## 3. 路由與導覽整合

- [x] 3.1 在 `vehicle-management/src/App.tsx` 新增 `/activity-logs` 路由,套用 `<ProtectedRoute requiredRole="admin">`
- [x] 3.2 在 `vehicle-management/src/components/Layout.tsx` 導覽列新增「使用者紀錄」連結(僅 admin 角色可見)

## 4. 驗證與收尾

- [x] 4.1 啟動 dev server(`npm run dev`),以 admin 帳號測試頁面存取、篩選、分頁功能
- [x] 4.2 以 user 帳號確認 `/activity-logs` 正確重定向至 `/`
- [x] 4.3 執行 `npm run lint` 確認無 ESLint 錯誤
70 changes: 70 additions & 0 deletions openspec/specs/user-activity-log/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# User Activity Log

## Purpose

提供管理者查看所有使用者操作活動日誌的頁面,支援依使用者與操作類型篩選及分頁瀏覽,僅限 `admin` 角色存取。

## Requirements

### Requirement: 管理者可存取使用者紀錄頁面
系統 SHALL 在路由 `/activity-logs` 提供一個使用者活動紀錄頁面,且僅允許角色為 `admin` 的使用者存取。

#### Scenario: 管理者存取使用者紀錄頁面
- **WHEN** 角色為 `admin` 的使用者導航至 `/activity-logs`
- **THEN** 系統顯示使用者紀錄頁面,包含活動日誌列表

#### Scenario: 一般使用者嘗試存取使用者紀錄頁面
- **WHEN** 角色為 `user` 的使用者導航至 `/activity-logs`
- **THEN** 系統重定向至 `/`(儀表板),不顯示紀錄內容

### Requirement: 系統顯示活動日誌列表
系統 SHALL 以表格形式顯示所有使用者的操作紀錄,每筆紀錄包含:時間、使用者名稱、角色、操作類型、操作對象(若有)、描述。

#### Scenario: 日誌列表正常載入
- **WHEN** 管理者進入使用者紀錄頁面
- **THEN** 系統顯示活動日誌表格,按時間由新至舊排序,每頁最多顯示 20 筆

#### Scenario: 無符合紀錄
- **WHEN** 篩選條件下無任何符合的日誌
- **THEN** 系統顯示「目前沒有符合的紀錄」提示訊息,不顯示表格列

### Requirement: 管理者可依使用者與操作類型篩選日誌
系統 SHALL 提供篩選列,允許管理者依使用者名稱與操作類型縮小日誌範圍。

#### Scenario: 依使用者篩選
- **WHEN** 管理者從使用者下拉選單選擇特定使用者
- **THEN** 列表僅顯示該使用者的活動紀錄

#### Scenario: 依操作類型篩選
- **WHEN** 管理者從操作類型下拉選單選擇特定類型(如「登入」)
- **THEN** 列表僅顯示該操作類型的紀錄

#### Scenario: 重設篩選條件
- **WHEN** 管理者點擊「重設」按鈕
- **THEN** 所有篩選條件清空,列表回復顯示全部紀錄

### Requirement: 日誌列表支援分頁
系統 SHALL 提供分頁功能,允許管理者在多頁日誌間切換。

#### Scenario: 切換至下一頁
- **WHEN** 管理者點擊「下一頁」按鈕,且後續有更多紀錄
- **THEN** 系統顯示下一頁的紀錄,頁碼更新

#### Scenario: 已在最後一頁
- **WHEN** 管理者在最後一頁
- **THEN** 「下一頁」按鈕為禁用狀態

#### Scenario: 已在第一頁
- **WHEN** 管理者在第一頁
- **THEN** 「上一頁」按鈕為禁用狀態

### Requirement: 系統提供活動日誌 API
系統 SHALL 在 `GET /api/activity-logs` 提供活動日誌資料,支援 query string 參數:`username`、`action`、`page`(預設 1)、`pageSize`(預設 20)。

#### Scenario: 取得全部日誌
- **WHEN** 呼叫 `GET /api/activity-logs` 無任何 query 參數
- **THEN** API 回傳第一頁的所有日誌資料及總筆數

#### Scenario: 依條件篩選日誌
- **WHEN** 呼叫 `GET /api/activity-logs?username=admin&action=login`
- **THEN** API 回傳符合 username=admin 且 action=login 的紀錄,並套用分頁
1 change: 1 addition & 0 deletions vehicle-management/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
coverage
*.local

# Editor directories and files
Expand Down
Loading
Loading