feat:實作 Admin Panel — 平台管理後台
後端: - AdminStatsController:總會員/教練/課程數統計 API - AdminUserController:會員與教練列表、詳情、啟用/停用、教練驗證(toggle 反轉語意) - AdminOfferController:全平台課程列表與刪除 - routes/api.php:新增 /api/admin/stats、members、providers、offers 等路由 前端(frontend/): - adminAuth store、adminAxios(第三套獨立認證) - /admin/* 路由群組 + requiresAdmin guard - AdminNavBar、AdminLayout - App.vue:isCoachPage → isBackofficePage(/coach/* 和 /admin/* 皆隱藏會員 NavBar) - LoginView、DashboardView(統計卡片) - MembersView、ProvidersView(含驗證操作)、OffersView(含刪除確認) OpenSpec: - 新增 specs:admin-auth / admin-user-management / admin-offer-management / admin-stats / admin-panel-ui - 歸檔:openspec/changes/archive/2026-05-10-admin-panel Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-09
|
||||
@@ -0,0 +1,125 @@
|
||||
## Context
|
||||
|
||||
後端 Admin Auth 方法(loginAdmin / logoutAdmin / adminProfile / updateAdminProfile)已存在於 AuthController,路由也已佔位。User model 有 `is_active` 欄位;ProviderProfile 有 `is_verified` 欄位,目前無 API 可修改。前端已有 memberAuth / coachAuth 兩套認證模式,Admin 循同一模式新增第三套。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 管理員可登入後台、查看平台數據
|
||||
- 管理員可列表/搜尋會員與教練,並啟用/停用帳號
|
||||
- 管理員可驗證教練(設定 ProviderProfile.is_verified)
|
||||
- 管理員可查看全平台課程並刪除違規內容
|
||||
- 前端 `/admin/*` 有獨立 Layout,不顯示會員 NavBar
|
||||
|
||||
**Non-Goals:**
|
||||
- Admin 帳號自助註冊(透過後端 seeder 或直接 DB 建立)
|
||||
- 細粒度角色權限(RBAC)
|
||||
- 操作日誌(Audit Log)
|
||||
- 批次操作(批量停用)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1:Admin Auth 沿用現有 AuthController,不新建 Controller
|
||||
|
||||
**決定**:`loginAdmin`、`logoutAdmin`、`adminProfile`、`updateAdminProfile` 直接沿用,不修改。
|
||||
|
||||
**理由**:方法已存在且邏輯完整,路由也已佔位。
|
||||
|
||||
---
|
||||
|
||||
### D2:業務邏輯拆到獨立 Controller
|
||||
|
||||
**決定**:新增 `AdminUserController`(用戶管理)、`AdminOfferController`(課程管理)、`AdminStatsController`(統計)。
|
||||
|
||||
**理由**:與 AuthController 職責分開,避免繼續膨脹。所有方法在開頭驗證 `auth()->user()->role === 'admin'`,非管理員回傳 403。
|
||||
|
||||
---
|
||||
|
||||
### D3:Toggle 語意(啟用/停用、驗證/取消驗證)
|
||||
|
||||
**決定**:`toggle-active` 和 `toggle-verified` 為 PUT 端點,後端直接反轉當前值(`is_active = !is_active`),不接受 body 傳入布林值。
|
||||
|
||||
**理由**:UI 是單一按鈕切換狀態,反轉語意最直覺,避免前端傳錯值。
|
||||
|
||||
---
|
||||
|
||||
### D4:adminAuth 獨立 Store,localStorage key `admin_token` / `admin_user`
|
||||
|
||||
**決定**:循 coachAuth 模式,新增第三套獨立 store。
|
||||
|
||||
**理由**:三種角色可能在不同 tab 同時使用,共用 store 會互相污染。
|
||||
|
||||
---
|
||||
|
||||
## Contracts
|
||||
|
||||
### API Schema
|
||||
|
||||
#### `POST /api/admin/login`(現有)
|
||||
```
|
||||
Body: { "email": "...", "password": "..." }
|
||||
Response 200: { "status": true, "data": { "user": {...}, "token": "...", "token_type": "Bearer" } }
|
||||
```
|
||||
|
||||
#### `GET /api/admin/stats`(需 Bearer token,role=admin)
|
||||
```
|
||||
Response 200:
|
||||
{
|
||||
"status": true,
|
||||
"data": {
|
||||
"total_members": 120,
|
||||
"total_providers": 18,
|
||||
"total_offers": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/admin/members`(需 Bearer token,role=admin)
|
||||
```
|
||||
Query: q(搜尋 name / email), page, per_page(default 15)
|
||||
Response 200: { "status": true, "data": [...users with memberProfile], "meta": {...} }
|
||||
```
|
||||
|
||||
#### `GET /api/admin/members/{id}`
|
||||
```
|
||||
Response 200: { "status": true, "data": { ...user, profile: {...} } }
|
||||
Response 404: { "status": false, "message": "用戶不存在" }
|
||||
```
|
||||
|
||||
#### `PUT /api/admin/members/{id}/toggle-active`
|
||||
```
|
||||
Response 200: { "status": true, "message": "帳號已停用" | "帳號已啟用", "data": { "is_active": false | true } }
|
||||
Response 404: { "status": false, "message": "用戶不存在" }
|
||||
```
|
||||
|
||||
#### `GET /api/admin/providers`(同 members 結構,含 providerProfile)
|
||||
#### `GET /api/admin/providers/{id}`
|
||||
#### `PUT /api/admin/providers/{id}/toggle-active`
|
||||
|
||||
#### `PUT /api/admin/providers/{id}/toggle-verified`
|
||||
```
|
||||
Response 200: { "status": true, "message": "教練已驗證" | "已取消驗證", "data": { "is_verified": true | false } }
|
||||
```
|
||||
|
||||
#### `GET /api/admin/offers`
|
||||
```
|
||||
Query: q(搜尋 title / location), page, per_page(default 15)
|
||||
Response 200: { "status": true, "data": [...offers with provider_id], "meta": {...} }
|
||||
```
|
||||
|
||||
#### `DELETE /api/admin/offers/{id}`
|
||||
```
|
||||
Response 200: { "status": true, "message": "課程已刪除" }
|
||||
Response 404: { "status": false, "message": "課程不存在" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 風險 | 緩解策略 |
|
||||
|------|----------|
|
||||
| toggle 反轉語意若網路重試,可能連按兩次回到原狀態 | MVP 接受,未來可改為明確 `{ is_active: true/false }` body |
|
||||
| Admin 帳號只能透過 DB 或 seeder 建立,無自助註冊 | 開發期間用 tinker 建立,正式環境透過 seeder |
|
||||
| `AdminUserController` 對 member / provider 各需重複驗證邏輯 | 用 private helper method 共用,避免複製貼上 |
|
||||
| `/admin/*` 頁面無額外安全層(任何人知道路徑都可訪問登入頁) | MVP 接受,route guard 在 frontend 層足夠 |
|
||||
@@ -0,0 +1,39 @@
|
||||
## Why
|
||||
|
||||
Member Portal 和 Coach Portal 已上線,但平台缺乏管理工具:教練驗證無法操作、問題帳號無法停用、課程品質無法把關。Admin Panel 補上這塊,讓平台可以實際營運。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **後端**:新增 `AdminUserController` 處理會員/教練的列表、詳情、啟用/停用、教練驗證
|
||||
- **後端**:新增 `AdminOfferController` 處理全平台課程列表與刪除
|
||||
- **後端**:新增 `AdminStatsController` 提供統計數據
|
||||
- **前端**:新增 `/admin/*` 路由群組,包含登入、儀表板、用戶管理、課程管理、個人資料
|
||||
- **前端**:`App.vue` 擴充隱藏邏輯,`/admin/*` 也不顯示會員 NavBar
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `admin-auth`:管理員登入/登出/個人資料(沿用現有 AuthController 方法,不需新增)
|
||||
- `admin-user-management`:管理員查看、啟用/停用會員與教練,驗證/取消驗證教練
|
||||
- `admin-offer-management`:管理員查看全平台課程並刪除違規內容
|
||||
- `admin-stats`:平台統計數據 API(會員數、教練數、課程數)
|
||||
- `admin-panel-ui`:管理後台前端介面(儀表板、用戶管理、課程管理)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(無)
|
||||
|
||||
## Impact
|
||||
|
||||
**後端**
|
||||
- 新增 `AdminUserController`、`AdminOfferController`、`AdminStatsController`
|
||||
- 現有 `AuthController` Admin 方法(login / logout / profile)直接沿用,路由已存在
|
||||
- 所有新 Admin API 套用 `auth:sanctum` middleware 並在 Controller 層驗證 `role === admin`
|
||||
|
||||
**前端(frontend/ 目錄)**
|
||||
- 新增 `src/stores/adminAuth.js`、`src/api/adminAxios.js`
|
||||
- 新增 `src/layouts/AdminLayout.vue`、`src/components/AdminNavBar.vue`
|
||||
- 新增 `src/views/admin/` 目錄下各頁面
|
||||
- `src/router/index.js` 新增 `/admin/*` 路由與 `requiresAdmin` guard
|
||||
- `App.vue` 隱藏邏輯擴充:`/admin/*` 同樣不顯示會員 NavBar
|
||||
@@ -0,0 +1,30 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員登入
|
||||
後端 SHALL 提供 `POST /api/admin/login`(現有 AuthController 方法),驗證 email/password 並確認 role=admin,回傳 Bearer token。
|
||||
|
||||
#### Scenario: 正確帳密登入
|
||||
- **WHEN** 管理員送出正確 email 與 password
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: { user, token, token_type: "Bearer" } }`
|
||||
|
||||
#### Scenario: 非 admin 角色帳號嘗試登入
|
||||
- **WHEN** role 非 admin 的帳號嘗試呼叫此端點
|
||||
- **THEN** 回傳 HTTP 401,`{ status: false, message: "電子郵件或密碼錯誤" }`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員登出
|
||||
後端 SHALL 提供 `POST /api/admin/logout`(需 Bearer token),撤銷當前 token。
|
||||
|
||||
#### Scenario: 登出成功
|
||||
- **WHEN** 已登入管理員送出登出請求
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, message: "..." }`,token 失效
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員個人資料
|
||||
後端 SHALL 提供 `GET /api/admin/profile`(需 Bearer token),回傳管理員基本資訊與 AdminProfile。
|
||||
|
||||
#### Scenario: 取得個人資料
|
||||
- **WHEN** 已登入管理員送出 GET 請求
|
||||
- **THEN** 回傳 HTTP 200,包含 name / email / role / adminProfile(position / department)
|
||||
@@ -0,0 +1,25 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員查看全平台課程列表
|
||||
後端 SHALL 提供 `GET /api/admin/offers`(需 Bearer token,role=admin),回傳所有課程,支援關鍵字搜尋與分頁。
|
||||
|
||||
#### Scenario: 取得全部課程列表
|
||||
- **WHEN** 管理員送出 GET 請求不帶參數
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: [...offers], meta: { total, per_page, current_page, last_page } }`,預設每頁 15 筆,含 provider_id
|
||||
|
||||
#### Scenario: 搜尋課程
|
||||
- **WHEN** 管理員送出 `?q=墾丁`
|
||||
- **THEN** 只回傳 title 或 location 包含「墾丁」的課程
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員刪除課程
|
||||
後端 SHALL 提供 `DELETE /api/admin/offers/{id}`(需 Bearer token,role=admin),可刪除任意課程,不受 provider_id 限制。
|
||||
|
||||
#### Scenario: 刪除存在的課程
|
||||
- **WHEN** 管理員送出有效 id 的 DELETE 請求
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, message: "課程已刪除" }`,資料庫記錄移除
|
||||
|
||||
#### Scenario: 課程不存在
|
||||
- **WHEN** 指定 id 的課程不存在
|
||||
- **THEN** 回傳 HTTP 404,`{ status: false, message: "課程不存在" }`
|
||||
@@ -0,0 +1,82 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員登入頁
|
||||
前端 SHALL 提供 `/admin/login` 頁面,供管理員以 email/password 登入,成功後導向 `/admin/dashboard`。
|
||||
|
||||
#### Scenario: 登入成功
|
||||
- **WHEN** 管理員填入正確帳密並送出
|
||||
- **THEN** 呼叫 `POST /api/admin/login`,token 存入 adminAuth store(localStorage key: admin_token),導向 `/admin/dashboard`
|
||||
|
||||
#### Scenario: 登入失敗
|
||||
- **WHEN** 帳密錯誤
|
||||
- **THEN** 頁面顯示錯誤訊息,不跳轉
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 儀表板(統計數據)
|
||||
前端 SHALL 提供 `/admin/dashboard` 頁面(需 admin 登入),顯示平台核心統計數據。
|
||||
|
||||
#### Scenario: 載入統計數據
|
||||
- **WHEN** 管理員訪問 Dashboard
|
||||
- **THEN** 呼叫 `GET /api/admin/stats`,顯示總會員數、總教練數、總課程數三個數字卡片
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 會員管理頁
|
||||
前端 SHALL 提供 `/admin/members` 頁面,列出所有會員,支援搜尋與啟用/停用操作。
|
||||
|
||||
#### Scenario: 載入會員列表
|
||||
- **WHEN** 管理員訪問此頁面
|
||||
- **THEN** 呼叫 `GET /api/admin/members`,以表格顯示姓名、email、註冊時間、帳號狀態
|
||||
|
||||
#### Scenario: 搜尋會員
|
||||
- **WHEN** 管理員在搜尋框輸入關鍵字
|
||||
- **THEN** 以 `?q=` 重新呼叫 API,列表更新
|
||||
|
||||
#### Scenario: 切換帳號狀態
|
||||
- **WHEN** 管理員點擊啟用/停用按鈕
|
||||
- **THEN** 呼叫 `PUT /api/admin/members/{id}/toggle-active`,成功後按鈕狀態更新
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 教練管理頁
|
||||
前端 SHALL 提供 `/admin/providers` 頁面,列出所有教練,支援搜尋、啟用/停用、驗證操作。
|
||||
|
||||
#### Scenario: 載入教練列表
|
||||
- **WHEN** 管理員訪問此頁面
|
||||
- **THEN** 呼叫 `GET /api/admin/providers`,顯示姓名、email、工作室名稱、驗證狀態、帳號狀態
|
||||
|
||||
#### Scenario: 切換驗證狀態
|
||||
- **WHEN** 管理員點擊驗證/取消驗證按鈕
|
||||
- **THEN** 呼叫 `PUT /api/admin/providers/{id}/toggle-verified`,成功後驗證狀態更新
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 課程管理頁
|
||||
前端 SHALL 提供 `/admin/offers` 頁面,列出全平台課程,支援搜尋與刪除。
|
||||
|
||||
#### Scenario: 載入課程列表
|
||||
- **WHEN** 管理員訪問此頁面
|
||||
- **THEN** 呼叫 `GET /api/admin/offers`,顯示課程標題、地點、教練 ID、價格
|
||||
|
||||
#### Scenario: 刪除課程(含確認)
|
||||
- **WHEN** 管理員點擊刪除按鈕後確認
|
||||
- **THEN** 呼叫 `DELETE /api/admin/offers/{id}`,成功後從列表移除
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin 路由守衛
|
||||
前端 SHALL 對所有 `/admin/*` 路由(login 除外)加上 navigation guard,未登入時導向 `/admin/login`。
|
||||
|
||||
#### Scenario: 未登入訪問後台頁面
|
||||
- **WHEN** 未登入使用者直接訪問 `/admin/dashboard`
|
||||
- **THEN** 自動導向 `/admin/login`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin Layout 與導覽
|
||||
前端 SHALL 提供 `AdminLayout`,包含 `AdminNavBar`(顯示管理員姓名、各功能連結、登出),所有 `/admin/*` protected 頁面套用此 Layout。`/admin/*` 路由不顯示會員 NavBar。
|
||||
|
||||
#### Scenario: 會員 NavBar 隱藏
|
||||
- **WHEN** 使用者訪問任何 `/admin/*` 路徑
|
||||
- **THEN** App.vue 不渲染會員 NavBar
|
||||
@@ -0,0 +1,12 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 平台統計數據 API
|
||||
後端 SHALL 提供 `GET /api/admin/stats`(需 Bearer token,role=admin),回傳平台核心數據。
|
||||
|
||||
#### Scenario: 取得統計數據
|
||||
- **WHEN** 管理員送出 GET /api/admin/stats
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: { total_members: N, total_providers: N, total_offers: N } }`
|
||||
|
||||
#### Scenario: 非管理員存取
|
||||
- **WHEN** 非 admin role 的 token 送出請求
|
||||
- **THEN** 回傳 HTTP 403,`{ status: false, message: "無權限存取" }`
|
||||
@@ -0,0 +1,69 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員查看會員列表
|
||||
後端 SHALL 提供 `GET /api/admin/members`(需 Bearer token,role=admin),回傳所有 role=member 的用戶,支援關鍵字搜尋與分頁。
|
||||
|
||||
#### Scenario: 取得全部會員列表
|
||||
- **WHEN** 管理員送出 GET 請求不帶參數
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: [...members with memberProfile], meta: { total, per_page, current_page, last_page } }`,預設每頁 15 筆
|
||||
|
||||
#### Scenario: 搜尋會員
|
||||
- **WHEN** 管理員送出 `?q=王小明`
|
||||
- **THEN** 只回傳 name 或 email 包含「王小明」的會員
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員查看會員詳情
|
||||
後端 SHALL 提供 `GET /api/admin/members/{id}`,回傳指定會員的完整資料。
|
||||
|
||||
#### Scenario: 取得存在的會員詳情
|
||||
- **WHEN** 管理員送出有效 id 的 GET 請求
|
||||
- **THEN** 回傳 HTTP 200,包含 user 資料與 memberProfile
|
||||
|
||||
#### Scenario: 會員不存在
|
||||
- **WHEN** 指定 id 的用戶不存在或 role 非 member
|
||||
- **THEN** 回傳 HTTP 404,`{ status: false, message: "用戶不存在" }`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員啟用/停用會員帳號
|
||||
後端 SHALL 提供 `PUT /api/admin/members/{id}/toggle-active`,反轉指定會員的 `is_active` 狀態。
|
||||
|
||||
#### Scenario: 停用啟用中的帳號
|
||||
- **WHEN** 管理員對 is_active=true 的會員發送請求
|
||||
- **THEN** 將 is_active 設為 false,回傳 HTTP 200,`{ status: true, message: "帳號已停用", data: { is_active: false } }`
|
||||
|
||||
#### Scenario: 啟用停用中的帳號
|
||||
- **WHEN** 管理員對 is_active=false 的會員發送請求
|
||||
- **THEN** 將 is_active 設為 true,回傳 HTTP 200,`{ status: true, message: "帳號已啟用", data: { is_active: true } }`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員查看教練列表
|
||||
後端 SHALL 提供 `GET /api/admin/providers`(需 Bearer token,role=admin),回傳所有 role=provider 的用戶,支援搜尋與分頁,含 providerProfile。
|
||||
|
||||
#### Scenario: 取得全部教練列表
|
||||
- **WHEN** 管理員送出 GET 請求
|
||||
- **THEN** 回傳 HTTP 200,含 providerProfile(包括 is_verified、business_name 等)與分頁 meta
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員啟用/停用教練帳號
|
||||
後端 SHALL 提供 `PUT /api/admin/providers/{id}/toggle-active`,行為同會員版本。
|
||||
|
||||
#### Scenario: 停用/啟用教練帳號
|
||||
- **WHEN** 管理員對教練帳號發送 toggle-active 請求
|
||||
- **THEN** 反轉 is_active,回傳對應訊息
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員驗證教練
|
||||
後端 SHALL 提供 `PUT /api/admin/providers/{id}/toggle-verified`,反轉 ProviderProfile.is_verified 狀態。
|
||||
|
||||
#### Scenario: 驗證教練
|
||||
- **WHEN** 管理員對 is_verified=false 的教練發送請求
|
||||
- **THEN** 將 is_verified 設為 true,回傳 HTTP 200,`{ status: true, message: "教練已驗證", data: { is_verified: true } }`
|
||||
|
||||
#### Scenario: 取消驗證教練
|
||||
- **WHEN** 管理員對 is_verified=true 的教練發送請求
|
||||
- **THEN** 將 is_verified 設為 false,回傳 HTTP 200,`{ status: true, message: "已取消驗證", data: { is_verified: false } }`
|
||||
@@ -0,0 +1,80 @@
|
||||
## 1. [後端] Admin Auth — 確認現有方法可用
|
||||
|
||||
- [x] 1.1 `loginAdmin()` ✅ 直接可用,確認 role=admin 驗證邏輯正確
|
||||
- [x] 1.2 `logoutAdmin()` ✅ 直接可用,不需改動
|
||||
- [x] 1.3 `adminProfile()` ✅ 直接可用,不需改動
|
||||
- [x] 1.4 用 Postman 建立測試用 admin 帳號:`docker exec cfdive-app php artisan tinker`,建立 role=admin 的 User + AdminProfile,測試 login → profile → logout
|
||||
|
||||
## 2. [後端] AdminStatsController
|
||||
|
||||
- [x] 2.1 建立 `AdminStatsController`,實作 `index()`:驗證 role=admin,查詢 `User::where('role','member')->count()`、`User::where('role','provider')->count()`、`DivingOffer::count()`,回傳統計數據
|
||||
- [x] 2.2 在 `routes/api.php` 的 admin middleware group 新增 `GET /stats` 路由
|
||||
|
||||
## 3. [後端] AdminUserController
|
||||
|
||||
- [x] 3.1 建立 `AdminUserController`,宣告 private `checkAdmin()` helper(驗證 role=admin,不符回傳 403)
|
||||
- [x] 3.2 實作 `members(Request $request)`:搜尋 role=member 用戶(q 參數 LIKE name/email),load memberProfile,分頁 15 筆
|
||||
- [x] 3.3 實作 `member(int $id)`:find role=member 用戶,不存在回 404,load memberProfile 後回傳
|
||||
- [x] 3.4 實作 `toggleMemberActive(int $id)`:find → 404,反轉 is_active,回傳新狀態與對應訊息
|
||||
- [x] 3.5 實作 `providers(Request $request)`:同 members,查 role=provider,load providerProfile
|
||||
- [x] 3.6 實作 `provider(int $id)`:同 member,查 role=provider,load providerProfile
|
||||
- [x] 3.7 實作 `toggleProviderActive(int $id)`:同 toggleMemberActive,查 role=provider
|
||||
- [x] 3.8 實作 `toggleProviderVerified(int $id)`:find role=provider → 404,取得 providerProfile,反轉 is_verified,儲存,回傳新狀態
|
||||
- [x] 3.9 在 `routes/api.php` admin group 新增路由:
|
||||
- `GET /members`、`GET /members/{id}`、`PUT /members/{id}/toggle-active`
|
||||
- `GET /providers`、`GET /providers/{id}`、`PUT /providers/{id}/toggle-active`、`PUT /providers/{id}/toggle-verified`
|
||||
|
||||
## 4. [後端] AdminOfferController
|
||||
|
||||
- [x] 4.1 建立 `AdminOfferController`,實作 `index()`:驗證 admin,搜尋所有課程(q 參數 LIKE title/location),分頁 15 筆
|
||||
- [x] 4.2 實作 `destroy(int $id)`:find → 404,刪除,回傳 200
|
||||
- [x] 4.3 在 routes 新增 `GET /offers`、`DELETE /offers/{id}`
|
||||
|
||||
## 5. [前端] Admin 基礎設施
|
||||
|
||||
- [x] 5.1 建立 `frontend/src/stores/adminAuth.js`:管理 `admin_token` / `admin_user`,實作 `init()` / `setAuth()` / `logout()`
|
||||
- [x] 5.2 建立 `frontend/src/api/adminAxios.js`:獨立 Axios instance,request interceptor 讀 `admin_token`
|
||||
- [x] 5.3 在 `frontend/src/router/index.js` 新增 `/admin/*` 路由:login(public)+ dashboard / members / providers / offers / profile(requiresAdmin)
|
||||
- [x] 5.4 router `beforeEach` 加入 `requiresAdmin` guard,未登入導向 `/admin/login`
|
||||
- [x] 5.5 在 `App.vue` 的 `onMounted` 加入 `adminAuth.init()`,並擴充 `isCoachPage` → `isBackofficePage`(涵蓋 `/coach/*` 和 `/admin/*`),會員 NavBar 在這兩個路徑下都不顯示
|
||||
|
||||
## 6. [前端] Admin Layout 與導覽
|
||||
|
||||
- [x] 6.1 建立 `frontend/src/components/AdminNavBar.vue`:顯示管理員姓名、「儀表板」、「會員管理」、「教練管理」、「課程管理」連結與登出按鈕
|
||||
- [x] 6.2 建立 `frontend/src/layouts/AdminLayout.vue`:包含 AdminNavBar + `<RouterView>`
|
||||
|
||||
## 7. [前端] 管理員登入頁
|
||||
|
||||
- [x] 7.1 建立 `frontend/src/views/admin/LoginView.vue`:email/password 表單,送出呼叫 `POST /api/admin/login`,成功存 token 至 adminAuth store 並導向 `/admin/dashboard`,失敗顯示錯誤
|
||||
|
||||
## 8. [前端] 儀表板
|
||||
|
||||
- [x] 8.1 建立 `frontend/src/views/admin/DashboardView.vue`:掛載時呼叫 `GET /api/admin/stats`,以三個數字卡片顯示總會員數、總教練數、總課程數
|
||||
|
||||
## 9. [前端] 會員管理頁
|
||||
|
||||
- [x] 9.1 建立 `frontend/src/views/admin/MembersView.vue`:掛載時呼叫 `GET /api/admin/members`,以表格顯示姓名、email、帳號狀態(啟用/停用 badge)
|
||||
- [x] 9.2 新增搜尋框,輸入後按 Enter 重新呼叫 API(帶 q 參數)
|
||||
- [x] 9.3 每列新增啟用/停用按鈕,呼叫 `PUT /api/admin/members/{id}/toggle-active`,成功後更新該列狀態
|
||||
|
||||
## 10. [前端] 教練管理頁
|
||||
|
||||
- [x] 10.1 建立 `frontend/src/views/admin/ProvidersView.vue`:掛載時呼叫 `GET /api/admin/providers`,顯示姓名、email、工作室名稱、驗證狀態、帳號狀態
|
||||
- [x] 10.2 新增搜尋框
|
||||
- [x] 10.3 每列新增啟用/停用按鈕(呼叫 toggle-active)與驗證/取消驗證按鈕(呼叫 toggle-verified),成功後更新對應欄位
|
||||
|
||||
## 11. [前端] 課程管理頁
|
||||
|
||||
- [x] 11.1 建立 `frontend/src/views/admin/OffersView.vue`:掛載時呼叫 `GET /api/admin/offers`,顯示課程標題、地點、地區、價格、provider_id
|
||||
- [x] 11.2 新增搜尋框
|
||||
- [x] 11.3 每列新增刪除按鈕:顯示確認 dialog,確認後呼叫 `DELETE /api/admin/offers/{id}`,成功後重新載入列表
|
||||
|
||||
## 12. [整合測試] 端對端驗證
|
||||
|
||||
- [x] 12.1 驗證管理員登入流程:tinker 建立 admin 帳號 → 登入 → 顯示 AdminNavBar → 登出
|
||||
- [x] 12.2 驗證 Dashboard 統計數據正確顯示
|
||||
- [x] 12.3 驗證會員管理:搜尋 → 停用 → 確認帳號無法登入 → 重新啟用
|
||||
- [x] 12.4 驗證教練驗證:切換 is_verified → 確認 /coach/profile 顯示狀態更新
|
||||
- [x] 12.5 驗證課程刪除:Admin 刪除課程 → 確認 /courses 列表消失
|
||||
- [x] 12.6 驗證 route guard:未登入訪問 `/admin/dashboard` 自動跳轉 `/admin/login`
|
||||
- [x] 12.7 驗證 `/admin/*` 路由不顯示會員 NavBar
|
||||
@@ -0,0 +1,30 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員登入
|
||||
後端 SHALL 提供 `POST /api/admin/login`(現有 AuthController 方法),驗證 email/password 並確認 role=admin,回傳 Bearer token。
|
||||
|
||||
#### Scenario: 正確帳密登入
|
||||
- **WHEN** 管理員送出正確 email 與 password
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: { user, token, token_type: "Bearer" } }`
|
||||
|
||||
#### Scenario: 非 admin 角色帳號嘗試登入
|
||||
- **WHEN** role 非 admin 的帳號嘗試呼叫此端點
|
||||
- **THEN** 回傳 HTTP 401,`{ status: false, message: "電子郵件或密碼錯誤" }`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員登出
|
||||
後端 SHALL 提供 `POST /api/admin/logout`(需 Bearer token),撤銷當前 token。
|
||||
|
||||
#### Scenario: 登出成功
|
||||
- **WHEN** 已登入管理員送出登出請求
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, message: "..." }`,token 失效
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員個人資料
|
||||
後端 SHALL 提供 `GET /api/admin/profile`(需 Bearer token),回傳管理員基本資訊與 AdminProfile。
|
||||
|
||||
#### Scenario: 取得個人資料
|
||||
- **WHEN** 已登入管理員送出 GET 請求
|
||||
- **THEN** 回傳 HTTP 200,包含 name / email / role / adminProfile(position / department)
|
||||
@@ -0,0 +1,25 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員查看全平台課程列表
|
||||
後端 SHALL 提供 `GET /api/admin/offers`(需 Bearer token,role=admin),回傳所有課程,支援關鍵字搜尋與分頁。
|
||||
|
||||
#### Scenario: 取得全部課程列表
|
||||
- **WHEN** 管理員送出 GET 請求不帶參數
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: [...offers], meta: { total, per_page, current_page, last_page } }`,預設每頁 15 筆,含 provider_id
|
||||
|
||||
#### Scenario: 搜尋課程
|
||||
- **WHEN** 管理員送出 `?q=墾丁`
|
||||
- **THEN** 只回傳 title 或 location 包含「墾丁」的課程
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員刪除課程
|
||||
後端 SHALL 提供 `DELETE /api/admin/offers/{id}`(需 Bearer token,role=admin),可刪除任意課程,不受 provider_id 限制。
|
||||
|
||||
#### Scenario: 刪除存在的課程
|
||||
- **WHEN** 管理員送出有效 id 的 DELETE 請求
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, message: "課程已刪除" }`,資料庫記錄移除
|
||||
|
||||
#### Scenario: 課程不存在
|
||||
- **WHEN** 指定 id 的課程不存在
|
||||
- **THEN** 回傳 HTTP 404,`{ status: false, message: "課程不存在" }`
|
||||
@@ -0,0 +1,82 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員登入頁
|
||||
前端 SHALL 提供 `/admin/login` 頁面,供管理員以 email/password 登入,成功後導向 `/admin/dashboard`。
|
||||
|
||||
#### Scenario: 登入成功
|
||||
- **WHEN** 管理員填入正確帳密並送出
|
||||
- **THEN** 呼叫 `POST /api/admin/login`,token 存入 adminAuth store(localStorage key: admin_token),導向 `/admin/dashboard`
|
||||
|
||||
#### Scenario: 登入失敗
|
||||
- **WHEN** 帳密錯誤
|
||||
- **THEN** 頁面顯示錯誤訊息,不跳轉
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 儀表板(統計數據)
|
||||
前端 SHALL 提供 `/admin/dashboard` 頁面(需 admin 登入),顯示平台核心統計數據。
|
||||
|
||||
#### Scenario: 載入統計數據
|
||||
- **WHEN** 管理員訪問 Dashboard
|
||||
- **THEN** 呼叫 `GET /api/admin/stats`,顯示總會員數、總教練數、總課程數三個數字卡片
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 會員管理頁
|
||||
前端 SHALL 提供 `/admin/members` 頁面,列出所有會員,支援搜尋與啟用/停用操作。
|
||||
|
||||
#### Scenario: 載入會員列表
|
||||
- **WHEN** 管理員訪問此頁面
|
||||
- **THEN** 呼叫 `GET /api/admin/members`,以表格顯示姓名、email、註冊時間、帳號狀態
|
||||
|
||||
#### Scenario: 搜尋會員
|
||||
- **WHEN** 管理員在搜尋框輸入關鍵字
|
||||
- **THEN** 以 `?q=` 重新呼叫 API,列表更新
|
||||
|
||||
#### Scenario: 切換帳號狀態
|
||||
- **WHEN** 管理員點擊啟用/停用按鈕
|
||||
- **THEN** 呼叫 `PUT /api/admin/members/{id}/toggle-active`,成功後按鈕狀態更新
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 教練管理頁
|
||||
前端 SHALL 提供 `/admin/providers` 頁面,列出所有教練,支援搜尋、啟用/停用、驗證操作。
|
||||
|
||||
#### Scenario: 載入教練列表
|
||||
- **WHEN** 管理員訪問此頁面
|
||||
- **THEN** 呼叫 `GET /api/admin/providers`,顯示姓名、email、工作室名稱、驗證狀態、帳號狀態
|
||||
|
||||
#### Scenario: 切換驗證狀態
|
||||
- **WHEN** 管理員點擊驗證/取消驗證按鈕
|
||||
- **THEN** 呼叫 `PUT /api/admin/providers/{id}/toggle-verified`,成功後驗證狀態更新
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 課程管理頁
|
||||
前端 SHALL 提供 `/admin/offers` 頁面,列出全平台課程,支援搜尋與刪除。
|
||||
|
||||
#### Scenario: 載入課程列表
|
||||
- **WHEN** 管理員訪問此頁面
|
||||
- **THEN** 呼叫 `GET /api/admin/offers`,顯示課程標題、地點、教練 ID、價格
|
||||
|
||||
#### Scenario: 刪除課程(含確認)
|
||||
- **WHEN** 管理員點擊刪除按鈕後確認
|
||||
- **THEN** 呼叫 `DELETE /api/admin/offers/{id}`,成功後從列表移除
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin 路由守衛
|
||||
前端 SHALL 對所有 `/admin/*` 路由(login 除外)加上 navigation guard,未登入時導向 `/admin/login`。
|
||||
|
||||
#### Scenario: 未登入訪問後台頁面
|
||||
- **WHEN** 未登入使用者直接訪問 `/admin/dashboard`
|
||||
- **THEN** 自動導向 `/admin/login`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin Layout 與導覽
|
||||
前端 SHALL 提供 `AdminLayout`,包含 `AdminNavBar`(顯示管理員姓名、各功能連結、登出),所有 `/admin/*` protected 頁面套用此 Layout。`/admin/*` 路由不顯示會員 NavBar。
|
||||
|
||||
#### Scenario: 會員 NavBar 隱藏
|
||||
- **WHEN** 使用者訪問任何 `/admin/*` 路徑
|
||||
- **THEN** App.vue 不渲染會員 NavBar
|
||||
@@ -0,0 +1,12 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 平台統計數據 API
|
||||
後端 SHALL 提供 `GET /api/admin/stats`(需 Bearer token,role=admin),回傳平台核心數據。
|
||||
|
||||
#### Scenario: 取得統計數據
|
||||
- **WHEN** 管理員送出 GET /api/admin/stats
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: { total_members: N, total_providers: N, total_offers: N } }`
|
||||
|
||||
#### Scenario: 非管理員存取
|
||||
- **WHEN** 非 admin role 的 token 送出請求
|
||||
- **THEN** 回傳 HTTP 403,`{ status: false, message: "無權限存取" }`
|
||||
@@ -0,0 +1,69 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 管理員查看會員列表
|
||||
後端 SHALL 提供 `GET /api/admin/members`(需 Bearer token,role=admin),回傳所有 role=member 的用戶,支援關鍵字搜尋與分頁。
|
||||
|
||||
#### Scenario: 取得全部會員列表
|
||||
- **WHEN** 管理員送出 GET 請求不帶參數
|
||||
- **THEN** 回傳 HTTP 200,`{ status: true, data: [...members with memberProfile], meta: { total, per_page, current_page, last_page } }`,預設每頁 15 筆
|
||||
|
||||
#### Scenario: 搜尋會員
|
||||
- **WHEN** 管理員送出 `?q=王小明`
|
||||
- **THEN** 只回傳 name 或 email 包含「王小明」的會員
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員查看會員詳情
|
||||
後端 SHALL 提供 `GET /api/admin/members/{id}`,回傳指定會員的完整資料。
|
||||
|
||||
#### Scenario: 取得存在的會員詳情
|
||||
- **WHEN** 管理員送出有效 id 的 GET 請求
|
||||
- **THEN** 回傳 HTTP 200,包含 user 資料與 memberProfile
|
||||
|
||||
#### Scenario: 會員不存在
|
||||
- **WHEN** 指定 id 的用戶不存在或 role 非 member
|
||||
- **THEN** 回傳 HTTP 404,`{ status: false, message: "用戶不存在" }`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員啟用/停用會員帳號
|
||||
後端 SHALL 提供 `PUT /api/admin/members/{id}/toggle-active`,反轉指定會員的 `is_active` 狀態。
|
||||
|
||||
#### Scenario: 停用啟用中的帳號
|
||||
- **WHEN** 管理員對 is_active=true 的會員發送請求
|
||||
- **THEN** 將 is_active 設為 false,回傳 HTTP 200,`{ status: true, message: "帳號已停用", data: { is_active: false } }`
|
||||
|
||||
#### Scenario: 啟用停用中的帳號
|
||||
- **WHEN** 管理員對 is_active=false 的會員發送請求
|
||||
- **THEN** 將 is_active 設為 true,回傳 HTTP 200,`{ status: true, message: "帳號已啟用", data: { is_active: true } }`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員查看教練列表
|
||||
後端 SHALL 提供 `GET /api/admin/providers`(需 Bearer token,role=admin),回傳所有 role=provider 的用戶,支援搜尋與分頁,含 providerProfile。
|
||||
|
||||
#### Scenario: 取得全部教練列表
|
||||
- **WHEN** 管理員送出 GET 請求
|
||||
- **THEN** 回傳 HTTP 200,含 providerProfile(包括 is_verified、business_name 等)與分頁 meta
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員啟用/停用教練帳號
|
||||
後端 SHALL 提供 `PUT /api/admin/providers/{id}/toggle-active`,行為同會員版本。
|
||||
|
||||
#### Scenario: 停用/啟用教練帳號
|
||||
- **WHEN** 管理員對教練帳號發送 toggle-active 請求
|
||||
- **THEN** 反轉 is_active,回傳對應訊息
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員驗證教練
|
||||
後端 SHALL 提供 `PUT /api/admin/providers/{id}/toggle-verified`,反轉 ProviderProfile.is_verified 狀態。
|
||||
|
||||
#### Scenario: 驗證教練
|
||||
- **WHEN** 管理員對 is_verified=false 的教練發送請求
|
||||
- **THEN** 將 is_verified 設為 true,回傳 HTTP 200,`{ status: true, message: "教練已驗證", data: { is_verified: true } }`
|
||||
|
||||
#### Scenario: 取消驗證教練
|
||||
- **WHEN** 管理員對 is_verified=true 的教練發送請求
|
||||
- **THEN** 將 is_verified 設為 false,回傳 HTTP 200,`{ status: true, message: "已取消驗證", data: { is_verified: false } }`
|
||||
Reference in New Issue
Block a user