# C5 — 專案規劃頁加 SSP tab（FE）

> **級別**：中
> **依賴**：C2.1（party / ssp-resources / system-characteristic / current-ssp-uid endpoints）+ C1（角色 dropdown）+ C8（role label）
> **被依賴**：C6（FE 詳情頁 deep link）

---

## 1. 目標

在「專案規劃」頁加上「SSP」tab，讓 manager 能在專案內直接編輯該專案 current AP 對應的 SSP 內容（基本資料 / 受評範圍 / parties / leveraged）。

控制項實作 + 程序書 **已有既有 UI**（A4/A5 階段），C5 不重做，但要從 SSP tab 內加 link 跳該頁。

## 2. 範圍

### In scope（**全 FE**）

- 新 tab：「SSP」在專案規劃頁
- AP 切換 dropdown
- 五個子區塊（accordion 折疊）：
  1. 基本資料（system_characteristic）
  2. 受評範圍 — 設備 / 資訊系統（ssp-resources）
  3. 外部利用服務（leveraged）
  4. 參與人員（parties，OSCAL 角色）
  5. （link）控制項實作 + 程序書（既有頁面）
- Lazy load：tab 切到 SSP 才 fetch；accordion 展開才 fetch 子區塊
- Deep link：`?tab=ssp&section=scope|parties|...` 自動展開對應 accordion
- 權限：non-manager 進來看 readonly + 編輯按鈕 disabled
- AP closed 狀態：所有 section 切 readonly + 顯示「此 AP 已關閉，無法編輯」hint

### Out of scope

- 控制項實作 UI（既有，A4/A5 已 ship）
- 程序書 UI（既有）
- Excel 匯入 UI（C2.2 後補，可後續加進 SSP tab）

## 3. UX 設計

### 3.1 整體 Tab 結構

```
專案規劃頁
├── Tab: 概覽
├── Tab: AP 列表
├── Tab: 參與人員  (project_participants — 既有)
└── Tab: SSP     ← C5 新加
    ├── [AP 選擇器: ▼ 第 1 期 (2026 Q1) - active]
    ├── [warning if AP closed: "此 AP 已關閉，僅可檢視"]
    │
    ├── Accordion: 基本資料            [展開]
    │   └── name / system_identifier / sensitivity / scope_description / status / system_owner
    │
    ├── Accordion: 受評範圍 — 設備      [折疊]
    │   └── DataTable: name / device picker / description
    │
    ├── Accordion: 受評範圍 — 資訊系統   [折疊]
    │   └── DataTable: name / info_system picker / description
    │
    ├── Accordion: 外部利用服務          [折疊]
    │   └── DataTable: service_name / provider / purpose
    │
    ├── Accordion: 參與人員 (SSP)        [折疊]
    │   └── DataTable: name / email / role(OSCAL dropdown) / user picker
    │
    └── Section: 控制項與程序書          [link]
        └── "→ 前往控制項實作頁" / "→ 前往程序書管理頁"
```

### 3.2 AP 切換邏輯

- 預設選 latest active AP（從 `GET /projects/<uid>/current-ssp-uid` 取）
- Dropdown 顯示所有 AP（API：`GET /projects/<uid>/assessment-plans/menu`，既有）
- 切 AP → 觸發 SSP detail re-fetch
- 切到非 current 的 AP（歷史 AP）→ 強制 readonly + hint「此為歷史 AP，僅可檢視」

### 3.3 Lazy load 策略

| 觸發點 | 動作 |
|--------|------|
| user click「SSP」tab | call `GET /projects/<uid>/current-ssp-uid` 取 ssp_uid + ap 狀態 |
| 預設展開「基本資料」accordion | call `GET /ssp/<ssp_uid>/system-characteristic` |
| user 展開其他 accordion | call 對應 endpoint：`GET /ssp/<ssp_uid>/parties` / `/ssp-resources` / `/leveraged` |
| 切換 AP | re-fetch system-characteristic（其他 accordion 折疊重置）|

### 3.4 權限狀態圖

| 角色 | AP 狀態 | UI 行為 |
|------|---------|---------|
| manager | active | 全 editable |
| manager | closed | readonly + hint「此 AP 已關閉」 |
| reviewer / auditor / viewer | active | readonly + 編輯按鈕 disabled |
| reviewer / auditor / viewer | closed | readonly |

判斷：FE 從 `current-ssp-uid` response 拿 `is_editable` + project_participants role 雙條件決定。

### 3.5 Deep link 處理

URL pattern：`/projects/<project_uid>/planning?tab=ssp&section=<section>`

```javascript
// router.beforeEach 或 component mounted
const { tab, section } = route.query
if (tab === 'ssp') {
  activeTab.value = 'ssp'
  if (section) {
    // 自動展開對應 accordion + scroll into view
    expandedAccordions.value = [section]
    nextTick(() => {
      document.querySelector(`#section-${section}`)?.scrollIntoView({ behavior: 'smooth' })
    })
  }
}
```

支援的 section value：
- `scope` → 「受評範圍 — 設備」+「受評範圍 — 資訊系統」（同時展開）
- `parties` → 參與人員
- `basic` → 基本資料
- `leveraged` → 外部利用服務

## 4. 元件設計

### 4.1 新元件清單

**Container**：
- `SspTabPanel.vue`（root，整個 SSP tab 內容）

**Section components**：
- `SspBasicSection.vue` — 基本資料（system_characteristic）
- `SspScopeDevicesSection.vue` — 設備清單
- `SspScopeInfoSystemsSection.vue` — 資訊系統清單
- `SspLeveragedSection.vue` — 外部利用服務（C2.2 後補可後續加）
- `SspPartiesSection.vue` — 參與人員（OSCAL 角色）

**Sub-components**（可考慮複用既有 MF 元件）：
- `SspDevicePickerCell.vue` — 設備 picker (仿 MF version)
- `SspInfoSystemPickerCell.vue` — 資訊系統 picker
- `SspPartyEditDialog.vue` — 編輯 party 的 dialog

### 4.2 元件複用（從 MF 版本搬）

| MF 元件 | SSP 版本作法 |
|---------|-------------|
| `ModuleFramePartiesPanel.vue` | 複製 → 改 props 接 ssp_uid 而非 mf_uid，API 改吃 `/ssp/<uid>/parties` |
| `ModuleFrameSspDevicesPanel.vue` | 同上 |
| `ModuleFrameSspInfoSystemsPanel.vue` | 同上 |
| `useModuleFrameParties` composable | 抽 base `useSspParties(ssp_uid)`，MF wrapper 複用 |

### 4.3 Composable 設計

```javascript
// src/composables/useSspContext.js (新)
export function useSspContext(projectUid) {
  const sspUid = ref(null)
  const apUid = ref(null)
  const apStatus = ref(null)
  const isEditable = ref(false)

  async function loadCurrent() {
    const res = await axios.get(`/projects/${projectUid.value}/current-ssp-uid`)
    sspUid.value = res.data.ssp_uid
    apUid.value = res.data.ap_uid
    apStatus.value = res.data.ap_status
    isEditable.value = res.data.is_editable
  }

  return { sspUid, apUid, apStatus, isEditable, loadCurrent }
}

// src/composables/useSspParties.js (新)
export function useSspParties(sspUid) { ... }

// src/composables/useSspResources.js (新)
export function useSspResources(sspUid) { ... }
```

## 5. 資料流

```
[User click SSP tab]
   ↓
useSspContext.loadCurrent(projectUid)
   ↓ ┌─ GET /projects/<uid>/current-ssp-uid → { ssp_uid, ap_uid, ap_status, is_editable }
   ↓ └─ 設定 reactive state
   ↓
[SspTabPanel 顯示 AP selector + sections]
   ↓
[Default expand: 基本資料 accordion]
   ↓
useSystemCharacteristic.fetch(ssp_uid) → GET /ssp/<uid>/system-characteristic
   ↓
[User click expand: 受評範圍-設備]
   ↓
useSspResources.fetchDevices(ssp_uid) → GET /ssp/<uid>/ssp-resources
   ↓
[User click edit on a row]
   ↓
useSspResources.updateItem(ssp_uid, item_uid, payload)
   ↓ ┌─ PUT /ssp/<uid>/ssp-resources/items/<item_uid>
   ↓ └─ if 200: refresh local cache
   ↓
[Display toast: "更新成功"]
```

## 6. 邊界條件

| 情境 | UI 行為 |
|------|---------|
| 專案還沒啟動 AP | `current-ssp-uid` 回 404 / null → 顯示「請先啟動 AP」+ 引導 button |
| 切到歷史 AP | readonly + 「歷史 AP，僅可檢視」hint |
| Manager 編輯時 AP 突然 closed | API 回 412，FE 顯示 toast「AP 已關閉」+ refresh state |
| Network 斷線 | section 內顯示 retry button + error state |
| Concurrent edit（兩個 manager 同時改）| API 層 last-write-wins（A5 既有模式），不做樂觀鎖 |

## 7. 待 user 拍板的小決策

| 編號 | 問題 | 我建議 |
|------|------|--------|
| C5-D1 | AP selector 預設選 latest active 還是 last viewed？ | **latest active**（直觀） |
| C5-D2 | 五個 accordion 預設展開哪個？ | **基本資料** + **受評範圍-設備**（最常編輯的兩個） |
| C5-D3 | 控制項實作 / 程序書 link 跳新頁還是側邊面板？ | **跳新頁**（既有頁面已是 fullpage 設計） |
| C5-D4 | leveraged section 是否要等 C2.2 後再做？ | **是**，C5 first ship 不含 leveraged（accordion 內顯示「尚未開放」） |
| C5-D5 | non-manager 看 readonly 時，編輯按鈕是 hide 還是 disable？ | **disable + tooltip 顯示「需 manager 權限」**（user 知道有此功能但無權） |

---

## 8. 開發後狀態

- 「專案規劃」頁多一個 SSP tab
- 五個 section accordion lazy load
- AP 切換 dropdown
- 權限 + AP 狀態雙判斷的 readonly mode
- Deep link 從專案詳情頁可跳直接展開特定 section
- 複用 MF 元件節省開發時間

## 9. UI Mock（建議）

執行前先做 wireframe（PrimeVue 元件層級）：
- Tab + AP selector：PrimeVue `TabView` + `Dropdown`
- Accordion：PrimeVue `Accordion`
- 表格：PrimeVue `DataTable`
- 編輯 dialog：PrimeVue `Dialog`

可用 `figma-use` 出 mockup（如果需要），或直接照既有 MF template-edit 的視覺風格做。

---

## 10. Implementation Reality / Reconciliation（2026-05-23 Phase D 修正）

C5 first ship (commit FE `57caca2` + `dd28658`, 2026-05-23 上午) 走原 design 的
Accordion 5-section 結構。User 測試後反饋「UI 不如預期，能不能參考合規資源庫編輯頁
不用手風琴一樣用 Tab」→ 同日下午整個 redesign 為 Phase D 結構。實際 ship 跟原 design
偏離點：

### 10.1 容器：Accordion → TabView

| 項目 | 原 design | 實際 |
|------|----------|------|
| 容器 | `Accordion` 多 section 折疊 | `TabView` 6 個 sub-tab 平行 |
| 進入體驗 | 折疊 → 點開 → fetch lazy | 進入即 mount 全部 (`:lazy="false"`)，counts 立即正確 |
| AP selector | 有 dropdown | 拿掉，apUid 從 URL `params.apUid` 直接拿 |

理由：user 反饋 accordion UX 拖泥帶水（counts 滯後、層級感不對），TabView 對齊既有
MF template-edit 視覺風格。

### 10.2 Sub-tab 結構：5 + link → 6 純 sub-tabs

原 design 5 個 section：
1. 基本資料（system_characteristic）
2. 受評範圍 — 設備 / 資訊系統
3. 外部利用服務
4. 參與人員
5. (link) 控制項實作 + 程序書

實際 6 個 sub-tab：
1. **受評標的**（前「基本資料」/「範圍概要」兩輪改名）
2. **責任單位**（OSCAL organization parties）
3. **責任人員**（OSCAL person parties）
4. **設備**
5. **資訊系統**
6. **外部利用服務**

差異：
- **拿掉「適用控制項」+「程序書文件池」兩個入口**：user 反饋這兩個不該放專案規劃；
  程序書管理改為專案規劃**頂層** tab（同 Project Settings 整合）
- **parties 拆成「責任單位 / 責任人員」兩個 sub-tab**：對齊 OSCAL responsible-parties
  按 party_type 分離的 spec，視覺更清楚
- **設備 / 資訊系統** 從 section 升 sub-tab，跟 MF 編輯頁對齊

### 10.3 「受評標的」UX（前「基本資料」）

原 design 只列「system_characteristic」一句帶過。實際做了 UX 重塑：

- Tab 名稱：基本資料 → 範圍概要 → **受評標的**（user 兩輪迭代後拍板）
- 加 intro block（4 條 bullet：是什麼 / 多敏感 / 誰負責 / 邊界在哪）
- 各欄位加 placeholder / hint，且**中性化**系統 → 範圍（為後續多框架支援鋪路）
- 預設值：FIPS 分類 = `high` / status = `active`（BE `_empty_placeholder` 同步）
- 範圍負責人改 **Dropdown** picker（從 `menuStore.userMenu` 拉），不再手填 UID

### 10.4 Phase B — Generalize MF panels（C5 §2.3 共用 panel 落地）

原 design §2.3 寫「複用 MF 元件節省開發時間」目標達成路徑：

4 個 MF panel 加 `apiBase` ('module-frame' | 'ssp') + `scopeUid` props：
- ModuleFramePartiesPanel
- ModuleFrameSspDevicesPanel
- ModuleFrameSspInfoSystemsPanel
- ModuleFrameDocumentPoolPanel

同一份元件兩個 context（MF / SSP）共用，service 80% 可複用。

### 10.5 Project Settings 整合進專案規劃頁（超出原 C5 scope）

原 design 沒寫，user 後續要求把專案設定也走 SSP 模式整進專案規劃頁，避免兩個入口。

抽 3 個 panel（`src/components/grc/project/{ProjectBasicInfoPanel,
ProjectParticipantsPanel, ProjectCloudIntegrationsPanel}.vue`），加入頂層 tab 結構：

```
專案規劃頁
├── 控制項實作（既有）
├── 系統安全計畫（C5 + Phase D, 6 sub-tabs）
├── 專案基本資訊（C5 後續）
├── 專案參與人員
├── 程序書管理
└── 雲端整合
```

各 panel card 70% 水平置中。拿掉右上角「專案設定」按鈕（重複入口）。
舊 `/project/projects/<id>/settings` route 保留可用（外部連結相容）。

### 10.6 i18n / 命名統一（超出原 C5 scope）

- 全 zh-tw「AP」→「稽核計畫」（en 保留 AP）
- 全 zh-tw「SSP」→「系統安全計畫」（en 保留 SSP）
- 「單位 / 參與人員」→「責任單位 / 責任人員」（對齊 OSCAL responsible-parties）
- 專案層 tab：「參與人員」→「專案參與人員」（區分 SSP 層 OSCAL parties）
- MF template-edit 7 個 tab header i18n 化（hardcoded → t() call）

### 10.7 C5-D 系列小決策 final

| 編號 | 問題 | 原建議 | 實際 |
|------|------|--------|------|
| C5-D1 | AP selector 預設 | latest active | **拿掉 selector**（apUid 從 URL） |
| C5-D2 | accordion 預設展開 | 基本資料 + 設備 | **TabView 預設第一個 sub-tab 受評標的** |
| C5-D3 | 控制項實作 link 跳新頁還側邊面板 | 跳新頁 | **拿掉 link**（控制項實作不放 SSP tab）|
| C5-D4 | leveraged 等 C2.2 | 是 (first ship 不含) | **含**（C2.2 已 ship, 直接做 leveraged sub-tab + Phase C 結構化欄位）|
| C5-D5 | non-manager readonly 按鈕 | disable + tooltip | 維持原建議 |

## 11. Track A + C Reconciliation（2026-05-23）

Track A (MF / SSP 結構對齊) + Track C (專案 SSP tab Excel round-trip) 完成後的 deviation 紀錄。

### 11.1 Track A — section component generalize（A3/A4）

`SspBasicSection.vue` + `SspLeveragedSection.vue` 加 `apiBase: 'ssp' | 'module-frame'` + `scopeUid` 2 props。`scopeUid` 優先、`sspUid` fallback — 既有 SSP-tab caller 不破壞。

`SspService` 對應 method 加 `apiBase` 參數，自動切 `/ssp` ↔ `/module-frame` 路徑前綴。`useSspSystemCharacteristic` / `useSspLeveraged` composable cache key 改 `${apiBase}:${scopeUid}` 隔離兩端 scope。

MF template-edit 新增「受評標的」+「外部利用服務」2 tab（後因 user 反饋順序調整，「受評標的」移到「責任單位」前）。

### 11.2 Track A — MF endpoint mirror SSP pattern（A1/A2）

抽 `SspSystemCharacteristicContextService`（mirror C2.1 `SspResourcesContextService` 抽法）— SSP-side AppService 改 thin wrapper（perm + delegate），shape 不變。

新 MF endpoint：
- `GET / PUT /module-frame/<uid>/system-characteristic`
- `GET / POST /module-frame/<uid>/leveraged`
- `PUT / DELETE /module-frame/<uid>/leveraged/<itemUid>`

兩端 endpoint 共用同一個 context service — 確保 response shape 一致。

### 11.3 Track A — 啟動專案 clone SC（A7）

`oscal_project_service._setup_ssp_system_implementation` 擴 `_clone_system_characteristic_from_template`：在 Step 2.5（找到 template_ssp 後、clone items 前）clone SC from template SSP 到 new SSP。MF template-edit 內 user 設的 SC「樣板預設值」隨樣板繼承到專案。

同時補 leveraged clone 漏的結構化欄位（`provider` / `category`）— Phase C `8de7991` 加進 jedi-oscal entity 後 clone 流程沒同步補。

### 11.4 Track C — SSP-scoped Excel template download

新 `SspImportTemplateAppService.generate_for_ssp(ssp_uid, mode, locale)`：
- Resolve chain：SSP→profile→MF（MF lookup data / ref docs / controls 結構走 MF）
- Filled mode 從 SSP 自身資料讀（`context_type='ssp'` parties / `system_security_plan_id` items / SC by ssp_id）
- 缺 MF 時 graceful fallback

新 route `/ssp/<uid>/excel-template?mode=blank|filled`，註冊在 `api/oscal/__init__.py` (SSP 路徑與 C2.2 T9 既有 `/ssp/<uid>/excel-import/*` 並列)。

### 11.5 Track C — FE 接通 SSP-scoped excel-import upload

C2.2 T9 ship 時 BE endpoint 已有但 FE 沒接通；Track C 補上：
- `SspTabPanel.vue` 加「下載」+「匯入」button (absolute 疊在 sub-tab nav row 右側)
- 匯入 dialog 用 dropzone 樣式 mirror `ImportExcelPage.vue`
- Upload via SSP-scoped endpoint (require_manager 權限 gate)
- 預覽走既有 non-scoped `/ssp-excel-import/<parse_uid>` endpoint（parse_uid 全 unique）
- Confirm 完成後判 `?return_to=ssp` query 跳回 `project-planning-ap` route

### 11.6 Track C ship 時挖出來的 BE bug

- **`session_scope` 缺**：`ssp_scoped_excel_import_route` 4 個 method 直接 call `permission_checker.require_manager`，但 perm 內部 query DB 需要 session_scope。C2.2 T9 ship 時就有此 bug，FE 沒接通所以未觸發。Fix: 4 個 method 各包 `with session_scope():`。
- **`ControlWriteStrategy` upsert key**：原本 `(ssp_id, catalog_control_id)`，但 DB unique 是 `(ssp_id, control_identifier)`。SSP-scoped flow collision — project 啟動已用 MF profile 的 catalog_control_id 建好 row，Excel reconciler 配到不同 catalog_control_id (different framework version 的 DB id)。Fix: 改用 `control_identifier`（OSCAL 標準 ID）。

### 11.7 Track A/C 跟原 design 的差異

| 議題 | 原 design | Track A/C 實際 |
|------|----------|----------------|
| MF template-edit 是否有 leveraged tab | 沒提 | **有**（mirror SSP tab 結構） |
| MF leveraged endpoint | 沒提 | **新增**（reuse SspLeveragedContextService） |
| 啟動專案是否 clone SC | 沒提（C3 PR1 拿掉 SC snapshot） | **clone**（從 template SSP，A7 加） |
| SSP tab 下載匯入 Excel | 沒提 | **新增**（Track C）|
| Excel sheet 名前綴 | `01_基本資料` 等 | **拿掉前綴**（v2.3.0, parser 加 legacy alias） |
| Excel leveraged 欄位 | category / service_name / provider / status / purpose | **加 date_authorized + description**（v2.4.0, 對齊 SSP UI dialog） |
