---
title: "任務關卡 — 設計文件 (FR-148)"
brand: "Guidant AI · **FR-148** 任務關卡"
eyebrow: "FR-148 · 設計文件 · 2026-10-18（D1～D16 全數定案）"
h1: "一張任務卡走完「開始 → 1 → 2 → 3 → 結案」"
subtitle: "關卡是任務裡面的事；BPMN 流程圖一顆方塊一筆任務，不動圖"
lede: "管理員在系統設定建幾個**任務關卡樣板**（只是關卡骨架：名稱、說明、順序，不設負責人）；PM 在規劃頁替任務選樣板、**每一關各派部門＋人員（可多人）**；任務卡上多一條關卡進度，按鈕跟著「你是不是當下那關的人」變（送審／核可／退回），每換一關通知下一關的人；走完最後一關才呼叫既有的「完成任務」讓流程往下走。出廠預設是零關的「無關卡」＝現狀，沒設定的客戶什麼都不會變。"
chips: [
  {text: "D1～D16 全數定案（D3 作廢）", kind: ok},
  {text: "6 張實作卡，畫面先出", kind: accent},
  {text: "不動 BPMN 引擎、不動流程圖", kind: ok},
  {text: "動 jedi-flow-engine（任務表加兩欄）", kind: warn},
  {text: "陷阱：待辦清單的 view 只讀原指派表", kind: crit}
]
footer: "FR-148 · 設計文件 · 2026-10-18 · 討論稿：[discussion.html](discussion.html)（含四方案比較與被排除選項）· 雛形：[prototype/index.html](prototype/index.html) · 沿革見 LOG 與 git log"
---

> 狀態：**設計定案（D1～D16），待開卡**｜日期：2026-10-18
> 討論稿（含四方案比較、被排除選項）：[`discussion.html`](./discussion.html)｜雛形：[`prototype/index.html`](./prototype/index.html)
> 開發 branch：三個 repo 目前都在 `feature/FR-139`（開工前 runner 自己 `git branch --show-current` 確認，不對就停下問）

> 🔴 **這份是設計決策，不是現況。** 內文的行號停在 2026-10-18 定稿當時，實作改動的結果不回頭改寫這裡——現況看實作完成後的 FINAL-SPEC.md。

路徑慣例：BE 路徑相對 `compliance-manager-be/`；`pkg/` 代表 `~/Projects/Jedicogy/module/jedi-python-package/`；FE 路徑相對 `compliance-manager-fe/`。

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-18 | 初版設計定案：D1～D15、資料模型、六卡拆分、端到端驗收 | FR-148 |
| 2026-10-18 | §5.7／D7 改「任務建立時套公司預設」：runner 實查「啟動稽核」在任務做完之後，原設計的掛點不成立；決策者裁 A | CM-2885 |
| 2026-10-18 | 新增 D16 退回只到上一關；§5.4 退回彈窗、§5.6 樣板編輯彈窗改版（Steps 預覽、名稱說明分兩列、關卡表不放開始／結案） | 決策者裁示 |
| 2026-10-18 | 內建零關樣板改名「無關卡」 | 決策者裁示 |

---

## 1. 需求背景與端到端流程 {#background nav="背景"}

### 1.1 一句話

**一個任務要經過「交證據 → 主管審 → 顧問審」時，是同一張卡依序交給不同的人，不是三張卡。**

### 1.2 要解的問題

現在任務只有「未開始／進行中／已完成」三態。想讓一個任務經過主管、顧問兩道審核，只能在流程圖畫三個方塊——而流程圖的規則是**一個方塊＝一筆任務**（FR-112 定的，證據、留言、問卷、執行紀錄全掛在那一筆上），於是變成三張卡：證據在第一張、主管在第二張看不到證據、顧問在第三張看不到前兩張的留言。

### 1.3 給非工程讀者的三個名詞

- **任務關卡樣板**：系統設定裡的一份關卡清單，例如「標準三關：執行人交證據、主管審核、顧問審核」。只有關卡的名稱與順序，不寫誰負責。
- **關卡**：任務走到的某一站。畫面上長「開始 → 1 → 2 → 3 → 結案」，開始與結案只是兩個端點，中間的數字才是關卡。
- **零關**：一關都沒有的樣板（內建的「無關卡」），就是今天的做法——指派人員按完成就結束。

### 1.4 端到端流程

```{.mermaid cap="圖 1 — 一張卡從套樣板到結案（含退回）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant AD as 管理員
    participant PM as 專案管理者
    participant EX as 執行人（第 1 關）
    participant MG as 主管（第 2 關）
    participant CS as 顧問（第 3 關）
    participant BE as 後端
    participant WF as BPMN 引擎（不改）
    AD->>BE: 系統設定建樣板「標準三關」
    PM->>BE: 規劃頁替任務選樣板（複製一份到任務）＋每關派部門／人員
    Note over WF: 專案啟動，方塊實體化成進行中（既有）
    BE-->>EX: 通知（既有 notify_user_todo_job，收件人＝第 1 關）
    EX->>BE: 上傳證據、按「送審」
    BE->>BE: 目前關卡 1→2、寫歷程
    BE-->>MG: 通知「待你處理」
    MG->>BE: 看同一張卡的證據與留言，退回到第 1 關＋理由
    BE->>BE: 目前關卡 2→1、寫歷程
    BE-->>EX: 通知「被退回」＋理由
    EX->>BE: 補證據、再送審
    MG->>BE: 核可
    BE-->>CS: 通知
    CS->>BE: 核可並結案
    BE->>WF: complete_job()（既有）
    WF->>WF: 下一方塊實體化、通知其執行人（既有）
```

---

## 2. 分工概述（30 秒版） {#overview nav="概述"}

整案一句話：**任務身上帶一份從樣板複製來的關卡清單＋每關一組人，同一張卡依序換手，走完最後一關才讓流程往下；出廠預設零關＝現狀。**

| 卡 | 做什麼 | 產出 | 決策者檢查法 |
|---|---|---|---|
| **① 任務卡關卡進度＋送審／核可／退回＋歷程** | 任務卡標頭多關卡進度、狀態標籤右邊多時鐘圖示開歷程彈窗、底部按鈕跟著當下那關的人變、每換一關寄信 | 五張新表＋任務兩欄的 migration、推進／退回 service 與三支端點、FE 三個子元件 | 對照 [雛形頁 1](prototype/task-drawer.html)：DEV 手灌一個三關任務，用三個帳號各登入一次走送審→核可→核可並結案，歷程三筆、每步下一關的人收到信；主管退回時執行人收到信且卡回第 1 關 |
| **② 待辦任務頁** | 第 2 關以後的人也看得到任務；多「目前關卡」欄、「待我處理」段 | `vw_user_job_queue` 併入關卡指派表、待辦查詢加兩欄一條件、FE 欄與段 | 對照 [雛形頁 2](prototype/my-tasks.html)：主管帳號登入，「待我處理」看得到等他審的那筆；執行人帳號看到被退回的那筆 |
| **③ 系統設定「任務關卡樣板設定」** | 清單＋編輯 dialog（版型同專案列表）；建／改／刪樣板；內建「無關卡」 | CRUD 端點、能力點、FE 清單頁與 dialog、`ui_routes` 選單 | 對照 [雛形頁 4](prototype/settings.html)：新增一個兩關樣板、改順序、設預設，重開頁面還在；「無關卡」刪除鈕是灰的 |
| **④ 規劃頁選樣板＋每關指派＋套用到其他任務** | 任務表單的「部門＋人員」兩格換成一行摘要＋「設定」；彈窗內選樣板、每關一組部門→人員、套用到其他任務；開始後樣板鎖、人可改 | 套樣板端點、每關指派端點、批次套用端點、FE 摘要列與彈窗子元件 | 對照 [雛形頁 3](prototype/planning.html)：選「標準三關」、三關各派人、存檔重開還在；已開始任務換不了樣板但能換人，新加的人（若是當下那關）收到信 |
| **⑤ 啟動預設、退回重開、批次推進、啟動前提醒** | 專案啟動時沒選樣板的任務帶公司預設；管理員退回重開時回第 1 關；批次完成改成推進當下那一關；啟動前軟提醒加「有關卡沒派人」 | 啟動掛點、兩處重開 hook、批次完成改寫、`PlanningReadinessChecker` 加一項 | 已結案任務退回重開 → 回第 1 關、執行人收到信；待辦勾三筆按批次完成，三筆各前進一關；有關卡沒派人時按啟動會跳確認視窗列出 |
| **⑥ 任務配置 Excel 對齊** | 指派／部門兩欄改為第 1 關；加唯讀「任務關卡」摘要欄；填寫說明加一句 | `job_import_service.py`、`task_guidance_workbook` 填寫說明、`JobImportDialog.vue` 預覽加唯讀欄 | 匯出一份含三關任務的檔，看到摘要欄；改第 1 關的人匯入，第 2、3 關的人不變 |

依賴：① 是地基（表與 service），先做；②③ 只依賴 ①，可平行；④ 依賴 ①③；⑤ 依賴 ①④；⑥ 依賴 ①④。**卡的排序以「決策者何時看得到畫面」為準**——① 做完就能用 DEV 手灌資料看到任務卡走完整條。

```{.mermaid cap="圖 2 — 六張卡的依賴"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
    C1[① 任務卡關卡進度<br/>表＋service＋任務卡] --> C2[② 待辦任務頁]
    C1 --> C3[③ 任務關卡樣板設定]
    C3 --> C4[④ 規劃頁選樣板<br/>每關指派]
    C4 --> C5[⑤ 啟動預設／退回重開<br/>批次推進／啟動提醒]
    C4 --> C6[⑥ 任務配置 Excel]
```

---

## 3. 決策定案（D1～D16） {#decisions nav="決策定案"}

| # | 定案 | 理由 | 被排除 |
|---|---|---|---|
| D1 | 樣板**不設負責人**；每關由誰做是 PM 在規劃頁指派，跟現在指派任務人員同一套操作（部門→人員），每關可多人。第 1 關＝原本的指派人員 | 樣板是公司層範本、誰做是專案的事；沿用現有操作 | 樣板上填專案角色、跑到那關查角色——多一層間接，PM 看不出某關是誰 |
| D2 | 畫面呈現「開始 → 1 → 2 → … → 結案」，開始／結案只是端點，不存資料、不編號、沒有人與按鈕 | 讓流程看起來完整；零關就是「開始 → 結案」，一眼看得出是直通 | — |
| D3 | 作廢（D1 後不需系統找部門主管） | — | — |
| D4 | 同一關多人時，**任一人**核可或退回就算數；歷程記是誰 | 全部都要簽＝會簽，要多一層「已簽幾個」，第一版不做 | 會簽 |
| D5 | **任務開始後樣板不能換、關卡結構不能改，但每關的人可以改**（D15 放寬）；系統改樣板不影響已套用的任務（任務拿的是複製品） | 避免「主管正在審、第 2 關被刪」；換人不改結構、常見（請假、換顧問） | 開始後全鎖——沒派齊的關卡會卡死無人能補 |
| D6 | 退回不清證據，執行人在原有證據上補 | 證據本來可以多筆，歷程記退回理由 | 標記「上一輪」證據 |
| D7 | 系統出廠內建零關樣板「**無關卡**」並設為公司預設（seed migration 寫入；不可刪、不可改名、不可加關）。**任務建立時**沒選樣板就帶公司預設（三個建立點；不在啟動稽核套，見 §5.7）；已在跑的舊專案不回頭套 | 預設＝現狀，沒設定的客戶什麼都不會變；管理員自己改預設才會變 | 「標準三關」當預設 |
| D8 | 待辦頁批次完成：**推進每筆當下那一關**（第 1 關＝送審、審核關＝核可、零關＝完成）；只有當下那關有你的任務勾得到；退回仍要逐筆 | 執行人一次交十筆、主管一次核可十筆都常見；批次沒有理由欄 | 審核關不能批次 |
| D9 | 問卷任務第一版**不能套樣板**，維持問卷自己的審核（待審核／待補充說明、`approval_required`、`is_approver`） | 一起併要動 jedi-survey 發版＋FE 12 檔 | 一次併 |
| D10 | 資源庫任務範本不預設樣板 | FR-147 剛做完那頁 | — |
| D11 | 樣板建在**系統層**（帶 `tenant_id`），各專案在規劃頁自己決定哪些任務套哪一個、可「套用到其他任務」 | 公司建一次、全部專案能選；放系統層或專案層工作量相同 | 專案層——每個新專案要重建 |
| D12 | 不做單一任務微調關卡（加減關、改名）；要不同關卡就套不同樣板（不審＝零關的「無關卡」） | 微調讓「這個任務走哪套」無從追查 | 任務層微調 |
| D13 | 規劃頁「任務配置」Excel 只管第 1 關：指派人員／參與部門兩欄＝第 1 關；加一欄**唯讀**「任務關卡」摘要；第 2 關以後與樣板不進 Excel | 關數不一，攤成欄會大半空格且欄位會跑；審核關通常固定幾人，彈窗設就夠 | 每關攤成欄 |
| D14 | 改善計畫（POA&M）任務不套樣板，改善計畫頁與其 Excel 不動 | 稽核後才產生、走另一條流程 | — |
| D15 | 啟動前「有關卡沒派人」是**軟提醒**，併入既有 `PlanningReadinessChecker`（按確認仍可啟動）；搭配 D5 放寬讓開始後還能補人 | 與既有啟動檢查做法一致；硬擋會讓 PM 為一個顧問還沒定就整個專案動不了 | 硬擋 |
| D16 | **退回只能退回上一關**，第 1 關不能退；樣板不設退回目標。規則由後端算，存的 `back_to_seqs` 不採用（舊樣板與進行中任務一併適用） | 每關各勾「可退到哪幾關」設定太複雜，實務上幾乎都是退給上一手 | 每關自訂可退回的關卡 |

**用詞**：系統設定選單「任務關卡樣板設定」、每筆「任務關卡樣板」、任務卡上那條「關卡進度」、每一站「關卡」。「流程」只留給 BPMN 流程圖。

---

## 4. 現況接入點盤點 {#inventory nav="現況盤點"}

| 元件 | 現況（2026-10-18 核對） | 本案動作 |
|---|---|---|
| 任務狀態 `JobStatus` | `pkg/jedi-flow-engine/jedi_flow_engine/common/enum/job_code.py`：TODO／PROCESSING／COMPLETED／CANCEL | **不動**。關卡在 PROCESSING 期間換手 |
| 任務表 `compliance.job_executions` | ORM 在 `pkg/jedi-flow-engine/jedi_flow_engine/infra/models/job_execution.py`；FR-137／FR-140 已加週期、期限欄 | 加 `review_step_no`、`review_template_id` 兩欄（ORM 同步改套件，走 path dependency） |
| 完成任務 | `api/flow_engine/routes/flow_engine_route.py:42` → `workflow_execution_service.complete_job()`（`:623`）把下一方塊實體化並 `notify_user_todo_job` | 有關卡且未到最後一關時，**此端點要擋**（否則繞過關卡）；推進 service 走到最後一關時由 BE 呼叫它 |
| 批次完成 | `app/flow_control/service/job_batch_complete_service.py:43` `batch_complete`（210 行） | D8：有關卡的任務改走推進當下那一關 |
| 退回重開 | `app/flow_engine/service/job_reopen.py:10` `reopen_job_execution`（24 行）；`app/flow_control/service/job_restart_service.py:63` `restart_from_first_station`（週期重跑） | 兩處結束時把目前關卡重設為第 1 關、寫一筆歷程 |
| 通知 | `workflow_execution_service.py:1209` `notify_user_todo_job(job_id, assignees=None)`：只在任務 PROCESSING 時寄；沒給 assignees 就查 `task_assignees` | 換關時把該關的人當 `assignees` 傳入；不新開管線 |
| 換人通知 | `job_service.py:503` `_notify_newly_added_assignees`：比對新舊名單，**只寄給新加的人** | 每關換人比照：只寄新加的人、且只在該關是當下那關時寄 |
| 指派表 `compliance.task_assignees` | `pkg/jedi-task-platform/jedi_task_platform/participant/infra/model/task_assignee.py`：`is_approver`（問卷審查人員專用）、`role` | **不動**。第 1 關＝這張表；第 2 關以後另存新表 |
| 🔴 待辦清單 | `app/readmodel/service/my_jobs_app_service.py:51` → `infra/readmodel/tasks/my_grc_jobs_query.py` → view `public.vw_user_job_queue`（`scripts/sql/view/vw_user_job_queue.sql`，**FROM `compliance.task_assignees`**） | view 要併入關卡指派表，不然第 2 關以後的人看不到任務 |
| 啟動前檢查 | `pkg/jedi-compliance-audit/jedi_compliance_audit/app/service/planning_readiness_checker.py`（128 行）兩項軟提醒；接線 `di_containers/flow_control/flow_control_containers.py:699`；由 `oscal_stage_handlers.py` `LaunchAuditOnCompleteHandler` 呼叫，force 放行 | 加第三項「有任務的關卡沒派人」；**動 jedi-compliance-audit 套件** |
| 規劃頁任務表單 | FE `src/components/grc/planning/AoTaskForm.vue`（**749 行，接近 800 上限**）：部門 MultiSelect（:436）→ 人員 MultiSelect 依部門篩（:443，`usersInDepts`／`keepAssigneesInDepts`）；問卷任務多一排審核者開關（:448） | 非問卷任務的部門＋人員兩格換成摘要列＋「設定」；彈窗另開子元件 |
| 任務卡 | FE `src/components/grc/JobExecutionDrawer.vue`（**2,344 行，已超標**）；標頭 `JobDrawerHeader` | 關卡進度、歷程圖示＋彈窗、動作區**各開子元件**，抽屜只插幾行 |
| 待辦頁 | FE `src/views/project/MyTasksView.vue`（760 行） | 加欄與段 |
| 任務配置 Excel | `app/flow_control/service/job_import_service.py`（492 行）：前九欄共用（`task_guidance_workbook.py`）＋任務類型／指派人員／參與部門／設備；`JobImportDialog.vue`（443 行） | D13 |
| 系統設定頁慣例 | FE `src/views/` 下 `notify-config`、`system-menu-manage` 等；版型參考 `src/views/project/ProjectListView.vue`（標題列＋搜尋＋表格卡＋編輯 dialog） | 新頁照專案列表版型 |

---

## 5. 詳細設計 {#design nav="詳細設計"}

### 5.1 資料模型

```{.mermaid cap="圖 3 — 新表與任務表"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
erDiagram
    review_step_templates ||--o{ review_step_template_items : "一個樣板多關"
    job_executions ||--o{ job_review_steps : "套樣板時複製"
    job_review_steps ||--o{ job_review_step_assignees : "每關的人（可多人）"
    job_executions ||--o{ job_review_step_logs : "每次推進／退回"
    review_step_templates {
        int id PK
        string uid
        int tenant_id "RLS"
        string name
        text description
        bool is_default "公司預設，每租戶最多一筆"
        bool is_builtin "內建無關卡"
    }
    review_step_template_items {
        int id PK
        int template_id FK
        int seq "1 起算"
        string name
        text description
        int[] back_to_seqs "退回目標，固定為上一關（D16）"
    }
    job_executions {
        int review_step_no "目前第幾關；NULL＝零關"
        int review_template_id "來源樣板，只記錄"
    }
    job_review_steps {
        int id PK
        int job_execution_id FK
        int seq "1 起算"
        string name
        text description
        int[] back_to_seqs
    }
    job_review_step_assignees {
        int job_execution_id FK
        int seq "2 起算；第 1 關用 task_assignees"
        int user_id
        int org_unit_id "部門，可空"
    }
    job_review_step_logs {
        int id PK
        int job_execution_id FK
        int seq "在哪一關"
        string action "submit / approve / reject / reopen"
        int to_seq "退回到哪關"
        int user_id
        text note
        timestamptz created_at
    }
```

要點：

- 表名用 `review_step_*`（程式識別），中文介面叫「任務關卡」。全部放 `compliance` schema。
- **`review_step_no` 為 NULL＝零關**（直通），行為與現況完全相同；非 NULL 時 1..N 是目前所在關卡，走完最後一關核可時呼叫 `complete_job`、欄位停在 N。
- **第 1 關的人＝`task_assignees`**（原指派人員，含問卷的 `is_approver` 不動）；**第 2 關以後**存 `job_review_step_assignees`。所以零關任務、問卷任務完全不碰新表。
- 部門：第 1 關的部門沿用既有任務部門關聯（`job_execution_org_unit`）；第 2 關以後的部門存在 `job_review_step_assignees.org_unit_id`（同一關選多部門時，每個「人×部門」一列；只選部門沒選人不算已指派）。
- `is_default`：每租戶最多一筆，用部分唯一索引 `WHERE is_default` 保證。內建「無關卡」由 seed migration 為每個租戶寫一筆並設為預設；`is_builtin=true` 的不可刪、不可改名、不可加關。
- RLS：`review_step_templates` 帶 `tenant_id`，`FOR ALL` 條件「超管或 `app_tenant_allowed_for_session(tenant_id)`」；其餘四張無租戶欄，跟隨父表（`review_step_template_items` 跟隨樣板、三張 `job_*` 跟隨 `job_executions` 所屬專案），並加進 `test/security/database/test_project_scoped_tables_rls.py` 清單。每張表 `GRANT ... TO cm_app` ＋ sequence 權限。
- 新表 ORM：樣板兩張放主專案 `infra/`；任務三張跟 `job_executions` 同層（主專案 `infra/flow_control/`，不放套件——套件只加兩個欄位，避免把關卡業務塞進流程引擎套件）。

### 5.2 推進與退回（核心 service）

新開 `app/flow_control/service/review_step_service.py`（不塞進已近千行的既有 service）：

| 方法 | 誰能叫 | 做什麼 |
|---|---|---|
| `submit(job_uid)` | 第 1 關的人（`task_assignees`） | 目前關卡 1→2；寫歷程 `submit`；通知第 2 關的人 |
| `approve(job_uid, note)` | 當下那關的人 | 若不是最後一關：關卡 +1、寫歷程、通知下一關；若是最後一關：寫歷程 `approve`、呼叫 `workflow_execution_service.complete_job()` |
| `reject(job_uid, to_seq, note)` | 當下那關的人；`note` 必填；`to_seq` 必須在該關 `back_to_seqs` 內 | 關卡改成 `to_seq`、寫歷程 `reject`、通知 `to_seq` 那關的人 |
| `reset_to_first(job_execution)` | 內部（重開 hook 呼叫） | 關卡回 1、寫歷程 `reopen`；caller 在 `@transaction` 內 |

規則：

- 守門在 app service：先 resolve 任務 → 專案，`assert_project_participant`；再判「你是否在當下那關的名單」，不是就 `ForbiddenError`（新錯誤碼 `GRC_403070` 起）。
- 任務不是 PROCESSING、或零關任務呼叫 `submit／approve／reject` → `PreconditionFailedError`（`GRC_412070` 起）。
- 同一關多人任一人動作即算數（D4）；併發時用條件式 UPDATE（`WHERE review_step_no = :expected`），更新 0 列就回 `ConflictError`（`GRC_409048` 起），避免兩人同時核可推進兩格。
- **既有完成端點要擋**：`complete_job` 進入點對「有關卡且 `review_step_no` 不是最後一關」的任務拋錯；推進 service 自己呼叫時帶內部旗標放行。否則前端舊按鈕或批次完成能繞過關卡。
- 通知：呼叫 `notify_user_todo_job(job_id, assignees=<該關的人>)`；文案沿用既有「待你處理」，被退回時多帶理由（若既有通知不支援理由欄，第一版只寄既有文案，理由看歷程）。

端點（`api/flow_control/routes/` 新檔）：

- `POST /grc/jobs/<job_uid>/review-steps/submit`
- `POST /grc/jobs/<job_uid>/review-steps/approve` body `{note}`
- `POST /grc/jobs/<job_uid>/review-steps/reject` body `{to_seq, note}`
- `GET  /grc/jobs/<job_uid>/review-steps` → `{template_name, current_seq, steps:[{seq, name, description, back_to_seqs, assignees:[{user_uid, name, dept_name}]}], logs:[{created_at, seq, step_name, action, to_seq, user_name, note}]}`（歷程的人名照「審計欄位回傳規範」在 app service 批次轉 nickname）

### 5.3 任務卡（FE）

- `JobExecutionDrawer.vue` 已超標，只插三行：在 `JobDrawerHeader` 下掛 `<JobReviewStepBar>`；標頭狀態標籤**右邊**掛時鐘圖示按鈕（角標筆數），點開 `<JobReviewHistoryDialog>`；描述區上方掛 `<JobReviewActionBox>`。底部原「完成任務」按鈕：有關卡時換成 `<JobReviewActions>`（送審／核可／核可並結案／退回）。
- 關卡進度：開始、結案為虛線端點圖示（play／flag），中間數字節點下方列該關的人；當下那關高亮，若含目前使用者加「待你處理」小標。不能出現水平捲軸（節點依內容寬、連線吃剩餘空間）。
- 退回 dialog：退回目標固定為上一關（D16）＋理由必填。
- 零關任務：關卡進度只畫「開始 → 結案」，按鈕維持「完成任務」，其餘不變。問卷任務：不顯示關卡進度。
- UI 禁令：不准左側粗色邊框。字級、配色走 dark-card 與全站 token，不自訂字型。

### 5.4 待辦清單（view 與 FE）

- `vw_user_job_queue` 改為 `task_assignees` UNION `job_review_step_assignees` 兩個來源（後者 `user_id` 帶出同欄位、`is_approver=false`），並多帶 `review_step_no`、`review_step_name`、`review_step_count`、`is_current_step_assignee`（這位使用者是否在當下那關）。改 view 時**整支 CREATE OR REPLACE、欄位只加在尾端**（FR-140 先例 `scripts/sql/2026-10-15-fr140-job-executions-due-at.sql:111` 的 GRANT 要重下）。
- 同一使用者同時在兩關（例如第 1 關又是第 3 關）不得重複列，view 要 DISTINCT ON 任務。
- 「待我處理」＝`job_status = PROCESSING AND is_current_step_assignee`（零關任務：在 `task_assignees` 即算）。
- FE `MyTasksView.vue`：狀態欄右邊加「目前關卡」（`2／3 主管審核`，待你處理時橘色小標）；分段加「待我處理（N）」放最前。

### 5.5 系統設定「任務關卡樣板設定」

- 能力點：新增 `review_step_template.manage`（宣告在 `core/host_capabilities.py`，跑 `scripts/deliverables/gen_permission_matrix.py` 產 FE 常數）；列表 GET 只要登入（規劃頁下拉要讀）。寫入是全站共用設定，另過 `require_tenant_hq()`。
- `ui_routes` 一列＋`route_capabilities` 一顆（seed migration），選單掛在系統設定底下。
- 頁面照 `ProjectListView.vue`：標題列（名稱＋「共 N 筆樣板」）＋右邊「新增任務關卡樣板」；搜尋框＋重設；表格欄：樣板名稱（內建者加「系統內建」標）／說明／關卡數／公司預設／已套用任務數／操作（鉛筆、垃圾桶）。**不放關卡預覽**。點一列或新增開編輯 dialog，表單由上而下單欄排列：樣板名稱、說明（多行）、公司預設勾選、關卡預覽（PrimeVue `Steps`，開始／結案用圖示、中間關卡編號對齊關卡表）、關卡表（只列實際關卡：編號、名稱、說明、上下移、刪；不列開始／結案，沒有退回設定）。
- 刪除：已套用的任務不受影響（拿的是複製品）；內建者刪除鈕停用。

### 5.6 規劃頁

- `AoTaskForm.vue` 只剩 51 行額度：非問卷任務的「部門＋人員」兩格整段換成 `<TaskReviewStepSummary>`（一行：樣板名、關數、「N 關已指派、M 關未指派 ⚠」＋「設定」按鈕；已開始顯示「檢視／換人」）；問卷任務維持原兩格與審核者開關。
- 彈窗 `<TaskReviewStepDialog>`：樣板下拉（含零關）→ 關卡預覽 → 每關一列「部門 MultiSelect → 人員 MultiSelect（依部門篩，重用 `usersInDepts`／`keepAssigneesInDepts`）」，第 1 關標「＝原本的指派人員」；零關只有一列；左下「套用到其他任務」、右下取消／儲存。
- 換樣板：同位置已派的人保留，多出來的關空著；少掉的關的人刪除。
- **任務開始後**：樣板下拉與關卡結構唯讀，**人仍可改**（D5）。
- **換人時的通知**（比照現況：`notify_user_todo_job` 只在任務進行中才寄；`job_service.py:355` 先記舊名單、`:503` `_notify_newly_added_assignees` 只寄新加的人）：

  | 換人的是哪一關 | 寄給誰 |
  |---|---|
  | 任務還沒啟動 | 不寄；啟動時寄給第 1 關**當下**的名單 |
  | 任務進行中，換**當下那關** | 只寄**新加**的人 |
  | 任務進行中，換**還沒走到的關** | 不寄；走到那關時寄給那時的名單 |
  | 任務進行中，換**已走過的關** | 不寄；之後被退回到那關時寄給那關當下的名單 |

  例：第 2 關原本派雷門，雷門離職改派阿好——不論何時改，信只會在輪到第 2 關時寄給阿好。**被移除的人不收任何通知**（現況也沒有），第一版不做移除通知。
- 套用到其他任務：比照流程圖「套用到其他任務」動線（`flow_apply_to_tasks_service.py` 的候選列法：同專案同輪），只列未開始、非問卷任務；套的是**樣板**（人保留同位置，新關空著）。
- 端點：`PUT /grc/jobs/<job_uid>/review-steps`（body `{template_uid, steps_assignees:[{seq, dept_uids, user_uids}]}`）、`POST /grc/jobs/review-steps/apply`（body `{template_uid, job_uids}`）。守門：專案 manager。

### 5.7 啟動、重開、批次、啟動前提醒

- 帶公司預設：**任務建立的當下就套**（決策者 2026-10-18 裁 A）——三個建立點：專案成立產生任務（`prep_job_generation_service.py`）、規劃頁新增任務（`job_service.create_job`）、覆核輪產生任務（`reverify_inheritance_service`）。只對「非問卷、還沒有 `job_review_steps`」的任務套；公司預設是零關就什麼都不做；舊專案不回頭套。**不在「啟動稽核」套**：啟動稽核是規劃階段結束、任務都做完才按的（它的既有前置就是「證據任務未完成」提醒），那時套關卡沒意義。排除 B「PM 按開始執行任務才套」：套上時第 2 關以後都沒人、一送審就卡住，且要改任務平台的啟動掛鉤。
- 重開：`reopen_job_execution` 與 `restart_from_first_station` 結束時，對有關卡的任務呼叫 `review_step_service.reset_to_first`。
- 批次完成：`job_batch_complete_service.batch_complete` 對有關卡的任務改呼叫 `submit`／`approve`（當下那關有你才處理，否則計入 skipped 並回原因）。
- 啟動前提醒：`PlanningReadinessChecker` 加 `WARN_REVIEW_STEPS_UNASSIGNED = "review_steps_unassigned"`，context `{count, jobs:[{uid, name, missing_seqs}]}`；FE 確認視窗的提醒清單加一種文案（i18n key 同 code）。檢查要讀主專案的新表 → 用注入的查詢介面，不讓套件 import 主專案。

### 5.8 任務配置 Excel（D13）

- 匯出：`EXTRA_COLUMNS` 尾端加一欄「任務關卡」（唯讀灰底），值例：「標準三關（3 關，2 關已指派）」／「無關卡」；「指派人員」「參與部門」欄標頭不改字（相容舊檔），填寫說明頁加一句「多關卡任務：指派人員／參與部門是第 1 關（執行人）；審核關卡的人員請到規劃頁設定」。
- 匯入：讀到第 1 關，寫回 `task_assignees` 的邏輯不變；新欄**不讀**；舊檔沒有這欄照樣能匯入（欄位對應依位置，新欄放最後）。任務已開始時換第 1 關的人照舊放行。
- `JobImportDialog.vue` 預覽表加唯讀欄。

---

## 6. 拆分（6 張實作卡） {#breakdown nav="拆分"}

| 卡 | 範圍 | 依賴 | 驗收 |
|---|---|---|---|
| ① 地基＋任務卡 | migration（五表兩欄、RLS、GRANT、內建樣板 seed）、ORM（套件兩欄＋主專案新表）、`review_step_service`、四支端點、既有完成端點擋關卡、FE 任務卡三個子元件 | — | DEV 手灌一個三關任務，三帳號走完含退回；既有完成端點對有關卡任務被擋；零關任務行為不變 |
| ② 待辦頁 | `vw_user_job_queue` 改寫、待辦查詢、FE 欄與段 | ① | 第 2 關的人在待辦看得到；「待我處理」正確；同人兩關不重複 |
| ③ 樣板設定頁 | CRUD 端點、能力點、`ui_routes`、FE 清單＋dialog | ① | 建／改／刪／設預設；內建不可刪 |
| ④ 規劃頁 | 套樣板／每關指派／套用到其他任務端點、FE 摘要列＋彈窗；開始後可換人並通知 | ①③ | 對照雛形頁 3 |
| ⑤ 啟動／重開／批次／提醒 | 啟動帶預設、兩處重開 hook、批次完成、`PlanningReadinessChecker`（動 jedi-compliance-audit） | ①④ | 見 §2 表 |
| ⑥ 任務配置 Excel | 匯出欄、填寫說明、匯入預覽 | ①④ | 見 §2 表 |

**套件異動**：① 動 jedi-flow-engine（`job_executions` 兩欄 ORM）、⑤ 動 jedi-compliance-audit（readiness checker）。都走 path dependency（`pyproject.toml` 已是 path 形式），不發版；發版時機等決策者。

---

## 7. 端到端驗收 {#acceptance nav="端到端驗收"}

1. 裝好不設定：所有任務零關，規劃頁摘要寫「無關卡」，任務卡只有「開始 → 結案」，按鈕「完成任務」——與上線前完全相同。
2. 系統設定建「標準三關」，規劃頁替一個任務套上、三關各派一人，啟動專案。
3. 執行人收到信、上傳證據、送審 → 主管收到信、在待辦「待我處理」看到、打開看得到證據與留言、退回 → 執行人收到信、卡回第 1 關 → 再送審 → 主管核可 → 顧問核可並結案 → 流程往下一個任務走。
4. 時鐘圖示的歷程彈窗有五筆。
5. 任務進行中，PM 把第 2 關換人：只有新加的人收到信（若第 2 關是當下那關）；樣板下拉是鎖的。
6. 有關卡沒派人時啟動專案：確認視窗列出「有幾個任務的關卡沒派人」，按確定仍可啟動。
7. 管理員退回重開一個已結案任務：回第 1 關、歷程多一筆 `reopen`。
8. 待辦勾三筆（一筆第 1 關、一筆審核關、一筆零關）按批次完成：各前進一格。
9. 匯出任務配置 Excel：多「任務關卡」唯讀欄；改第 1 關的人匯入，其他關的人不變；用上線前匯出的舊檔匯入仍成功。
10. 問卷任務、改善計畫任務：完全不受影響。

**回歸範圍**：既有完成任務、批次完成、退回重開、週期重跑、待辦清單、任務配置 Excel 匯入匯出、啟動稽核。

---

## 8. 這份文件的定位 {#positioning nav="定位"}

| 你想知道 | 看這份 |
|---|---|
| 為什麼不用 BPMN、四個方案怎麼比 | [討論稿](discussion.md) |
| 畫面長怎樣 | [雛形](prototype/index.html) |
| 為什麼這樣決定、要做哪些 | 本頁 |
| 做完的現況 | FINAL-SPEC.md（實作完成後產出） |
