FR-144 · 設計定案 v1 · 2026-10-17
左側選單「稽核計畫」、一個稽核次一列的表格、後端分頁、整列直達 FR-141 填寫頁
左側選單「待辦任務」底下多一項「稽核計畫」,旁邊帶「待填寫」筆數。進去是一張表格:一個稽核次一列,狀態分段鈕帶筆數、一個搜尋框、欄頭排序、後端分頁、整列可點直達該份稽核計畫。版式與 FR-142「我的稽核」、FR-143「改善計畫」入口同一套。不加表、不動套件:一支清單讀模型+端點、一筆選單登記、一支 FE 頁。本文件寫定案規則、為什麼這樣定、踩到會怎樣。
🔴 這份是設計決策,不是現況。 內文停在 2026-10-17 定稿當時,實作改動不回頭改寫這裡。
狀態:設計定案,待開卡派工|討論稿:
discussion.md|雛形:prototype/my-audit-plans.html
檔案路徑與行號以 2026-10-17 為準,查不到時以函式名重新定位。路徑慣例:BE 路徑相對
compliance-manager-be/;FE 路徑相對compliance-manager-fe/;三個 repo 都在feature/FR-139。
名詞:稽核次=compliance.audit_round_passes 一筆,一輪裡每做一次稽核一筆、各自一份稽核計畫;稽核計畫(AP)=FR-141 的計畫概要+查核項目規劃;評估目標(AO)=控制項底下的檢查項目,鍵是 part_id;填寫站=流程範本「稽核計畫填寫」站,稽核次狀態 audit_planning,對應 job 的 template_job_id 是 UserTask_ap_authoring/UserTask_internal_ap_authoring/UserTask_external_ap_authoring;參與者=compliance.project_participants。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-17 | v1:D1~D3 定案;資料模型、清單查詢、端點契約、FE 頁面、兩張卡、端到端驗收 | 討論稿定案 |
整案一句話:左側選單多一項「稽核計畫」,點進去一張表格列跟我有關的每一份稽核計畫,點列直達 FR-141 的填寫頁。
| 卡 | 做什麼 | 產出 | 完成怎麼判定(決策者親手做什麼、看到什麼) |
|---|---|---|---|
| 1 | 清單端點+選單登記 | 讀模型、app service、route、schema、ui_routes seed migration、count 端點 |
Swagger 用顧問帳號打 POST /audit-plans/my/list,回的列數=DEV 裡該人是 auditor/manager/reviewer、且稽核次到填寫站以後的筆數,每列有專案、稽核次、狀態、百分比、期限、稽核日期、稽核人員、專案經理;meta.status_counts 五個狀態都有數字;用 viewer 帳號打到 403;DB 查 ui_routes 多一筆 audit-plan-list sort 25 綁 grc.audit.read |
| 2 | 清單頁+選單徽章 | FE 新頁、路由、api 常數、i18n、側邊選單數字 | 登入顧問帳號,左側選單「待辦任務」底下看到「稽核計畫 N」;進去對照雛形:分段鈕切狀態筆數會變、搜「外稽」只剩外稽列、點欄頭排序、翻頁、點列跳到 FR-141 頁且麵包屑顯示對的稽核次;專案經理帳號看到外稽未指派那列是「未指派」橘字、最右「去指派」跳專案規劃頁;覆核者帳號看得到列但只有「檢視」 |
依賴:1 → 2。卡 1 可先用 Swagger 驗完資料再開卡 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[卡 1 清單端點+選單登記] --> C2[卡 2 清單頁+徽章]
C2 -.點列.-> P[FR-141 稽核計畫頁(不動)]
| 決策 | 定案 | 理由 | 被排除方案 |
|---|---|---|---|
| D1 選單名稱與位置 | 「稽核計畫」(不帶「我的」),群組「專案與任務」內 sort 25,排在「待辦任務」(20)與「專案列表」(30)之間 | 填計畫的是顧問、判定的是稽核人員,常同一人但階段不同,分開不用猜;FR-143「改善計畫」入口定案後三個並列:稽核計畫(填)→ 我的稽核(判)→ 改善計畫(排) | 把「我的稽核」改成兩個頁籤 |
| D2 規劃中不列 | 清單只列稽核次狀態 audit_planning 以後(白名單 audit_planning, auditing, remediation, pending_reverify, closed) |
與「我的稽核」同一條白名單邏輯;規劃中的點進去是空的,會被當壞掉 | 列出並標「尚未開始」 |
| D3 外稽未指派 | 稽核次到填寫站但 AP 沒有稽核人員、且登入者是該專案 manager → 稽核人員欄「未指派」、最右鈕「去指派」導到 project-planning-round;登入者只是 auditor → 該列不出現 |
外稽人員多是外部顧問,先指派才有人填;在清單就看得到「這份沒人」 | 一律顯示「檢視」 |
| 歸屬判準 | 只看 project_participants:role in (auditor, manager, reviewer)。auditor/manager 可編輯(待填寫時),reviewer 只能檢視,viewer 不列 |
AP 的稽核人員 party 只有姓名與 email、沒連登入帳號,系統無法確定「計畫裡的周雅婷」是不是登入者;「我的稽核」也是用參與者表,兩頁一致 | 用 AP 稽核人員名單判 |
| 粒度 | 一個稽核次一列(「我的稽核」是一輪一列) | 每個稽核次各自一份計畫,一輪一列會把內稽第 2 次、外稽藏起來 | 一輪一列、列內再展開 |
| 進度 | 只列查核項目:有填方法的 AO 數/AO 總數,印百分比;計畫概要六段不列 | 決策者裁:進度條要滑鼠才看得到、概要不用 | 進度條;兩個百分比 |
| 版面 | 一格一件事,最多一行小字;日期完整、有時間帶時間、起迄各一行;不列「我的角色」欄 | 決策者裁(看稽核人員或專案經理欄哪個有自己的名字加粗就知道角色) | 複合欄塞小字;「我的角色」欄 |
| 分頁 | 後端分頁,走 jedi-common RequestMetaSchema/EnvelopeSchema;分段鈕筆數由 meta.status_counts 回 |
資料會成長;筆數若只回當頁算不出來 | 前端一次拉全部自己分頁 |
| 搜尋 | 一個框:專案名、框架、稽核人員、專案經理、稽核性質(打「外稽」「內稽」「外部」「內部」都命中) | 與 FR-142 做法一致,工具列少三個下拉 | 狀態/性質/角色三個下拉 |
| 元件 | 現況 | 本案動作 |
|---|---|---|
public.ui_routes 群組 42「專案與任務」 |
專案儀表板 10/待辦任務 20/專案列表 30/我的稽核 40;route_capabilities 綁能力點,我的稽核綁 grc.audit.read ALL |
卡 1 seed audit-plan-list,url /project/audit-plans,icon pi pi-list(primeicons 6.0.1 沒有 pi-list-check),sort 25,is_menu true,綁 grc.audit.read ALL |
「我的稽核」POST /grc/audits/my/list(套件 jedi-compliance-audit route → 主專案 app/readmodel/service/auditor_dashboard_service.py → infra/readmodel/audit/auditor_dashboard_query.py) |
raw SQL 讀模型、.mappings() 不外流、回 list[dict];FR-142 線正在改它(加 people/risk 兩支) |
不改。卡 1 照它的檔案形狀另寫一支;等 FR-142 那條線的改動 commit 後再開工,免得對著變動中的樣板抄 |
compliance.audit_round_passes |
round_id、pass_no、audit_nature、nature_seq、status、assessment_plan_id(外稽未開始為 NULL)、updated_at |
清單主表 |
compliance.project_audit_rounds |
round_no、current_pass_id、workflow_execution_uid、project_id |
取輪號、是否目前稽核次、連 job |
compliance.job_executions |
template_job_id、status、due_at、workflow_execution_id;填寫站 job 的 template_job_id 是三個 *_ap_authoring 之一;DEV 樣本 due_at 多為 NULL |
取填寫期限;NULL 顯示「—」 |
oscal.ap_reviewed_controls.control_objective_selections |
FR-141 B1 寫入,形狀 [{"include-objectives":[{"objective-id":"<part_id>"},…]}];jsonb_array_length 外層恆為 1,分母要數內層 |
AO 總數 |
oscal.ap_assessment_activities |
一條 AO 一筆,steps[].props[{name:"method"}] |
已填數=steps 內任一 props 有 method 的 activity 數 |
oscal.ap_tasks.timing |
{"within-date-range":{"start","end"}} 日期時間字串 |
稽核日期 |
oscal.parties(AP metadata 下) |
person、props responsible-role=assessor |
稽核人員名單;空=未指派 |
FE src/views/project/MyAuditsView.vue(245 行)、FR-142 雛形 prototype/my-audits.html |
卡片式;FR-142 提案改表格 | 卡 2 新頁 AuditPlanListView.vue,照 FR-142 雛形版式刻;若 FR-142 先做完表格版「我的稽核」,兩頁抽一個共用表格殼 |
FE 路由 project-ap-authoring-round(src/config/router/index.js:1118) |
帶 ?pass=<uid> 看非目前稽核次(唯讀) |
點列:目前稽核次不帶 pass,其他帶 |
FE 側邊選單 src/layout/AppMenuItem.vue |
label 走 lang.menu.<route name>,無徽章機制 |
卡 2 加徽章:選單項可選配 badge 計數來源;與 FR-143 改善計畫入口共用 |
檔:infra/readmodel/audit/audit_plan_list_query.py(新)。照 auditor_dashboard_query.py:@property session、raw SQL、.mappings() 只在函式內讀、回 list[dict]+total+status_counts。
主查詢(一句 SQL,分頁與排序在 SQL 做):
WITH mine AS ( -- 我在哪些專案、什麼角色
SELECT project_id, array_agg(role) AS roles
FROM compliance.project_participants
WHERE user_id = :uid AND role IN ('auditor','manager','reviewer')
GROUP BY project_id
), base AS (
SELECT ps.id AS pass_id, ps.uid AS pass_uid, ps.pass_no, ps.audit_nature, ps.nature_seq,
ps.status, ps.assessment_plan_id, ps.updated_at AS pass_updated_at,
r.uid AS round_uid, r.round_no, (r.current_pass_id = ps.id) AS is_current,
r.workflow_execution_uid,
p.uid AS project_uid, p.name AS project_name, p.framework_name, -- 框架欄依專案表實際欄位
m.roles AS my_roles
FROM compliance.audit_round_passes ps
JOIN compliance.project_audit_rounds r ON r.id = ps.round_id
JOIN compliance.projects p ON p.id = r.project_id AND p.deleted_at IS NULL
JOIN mine m ON m.project_id = r.project_id
WHERE ps.status = ANY(:visible_statuses) -- D2 白名單
)
SELECT b.*,
-- 進度
(SELECT count(*) FROM oscal.ap_assessment_activities a
WHERE a.assessment_plan_id = b.assessment_plan_id
AND EXISTS (SELECT 1 FROM jsonb_array_elements(a.steps) s, jsonb_array_elements(s->'props') pr
WHERE pr->>'name' = 'method' AND coalesce(pr->>'value','') <> '')) AS filled_ao,
(SELECT count(*) FROM oscal.ap_reviewed_controls rc,
jsonb_array_elements(rc.control_objective_selections) s,
jsonb_array_elements(s->'include-objectives') o
WHERE rc.assessment_plan_id = b.assessment_plan_id) AS total_ao,
-- 稽核日期
(SELECT t.timing->'within-date-range'->>'start' FROM oscal.ap_tasks t WHERE t.assessment_plan_id = b.assessment_plan_id LIMIT 1) AS audit_start,
(SELECT t.timing->'within-date-range'->>'end' FROM oscal.ap_tasks t WHERE t.assessment_plan_id = b.assessment_plan_id LIMIT 1) AS audit_end,
-- 填寫期限:該輪 workflow 下、填寫站那張 job
(SELECT j.due_at FROM compliance.job_executions j
JOIN compliance.workflow_executions w ON w.id = j.workflow_execution_id
WHERE w.uid = b.workflow_execution_uid
AND j.template_job_id IN ('UserTask_ap_authoring','UserTask_internal_ap_authoring','UserTask_external_ap_authoring')
ORDER BY j.id DESC LIMIT 1) AS plan_due
FROM base b
WHERE (:kw IS NULL OR b.project_name ILIKE :kw OR b.framework_name ILIKE :kw
OR (:kw_nature IS NOT NULL AND b.audit_nature = :kw_nature)
OR EXISTS (稽核人員名單 ILIKE :kw) OR EXISTS (manager nickname ILIKE :kw))
AND (:status IS NULL OR b.status = :status)
ORDER BY <排序> LIMIT :size OFFSET :offset;稽核人員與專案經理名單用第二、三句批次查(WHERE assessment_plan_id = ANY(:ap_ids)、WHERE project_id = ANY(:project_ids) AND role='manager')再在 Python 組回每列,不在主查詢 string_agg(名單要保留順序與「是不是我」)。
status_counts:同一個 base+搜尋條件、不套 :status,GROUP BY status。
預設排序:CASE status WHEN 'audit_planning' THEN 0 … END, plan_due NULLS LAST, pass_updated_at DESC。可排欄:project(name)、pass(round_no, nature, nature_seq)、status、progress(filled_ao/total_ao)、due、audit(audit_start)、manager、updated。
搜尋詞對稽核性質:「外稽」「外部」→ external;「內稽」「內部」→ internal;在 app service 層翻成 :kw_nature。
app/readmodel/service/audit_plan_list_service.py(新):@transaction list_my_audit_plans(user_id, status, keyword, page, size, sort) 回 {"items":[…], "total", "status_counts"};@transaction count_my_pending(user_id) 只數 audit_planning 且非 reviewer-only。專案經理 nickname 走 common/util/audit_nickname.py 的既有 enrich。di_containers/flow_control/flow_control_containers.py 加 Factory(照 auditor_dashboard_service 那段)。api/project/routes/audit_plan_list_route.py(新,主專案,不進套件)。POST /audit-plans/my/list、GET /audit-plans/my/count。守門 @jwt_required + @require_license("audit") + @capability_required("grc.audit.read")(與「我的稽核」同,從 common.authz 取)。api/project/serializers/audit_plan_list.py(新)。Request 繼承 RequestMetaSchema+filters{status, keyword};Response 繼承 EnvelopeSchema,data 是 item 陣列,meta 多一個 status_counts: {audit_planning, auditing, remediation, pending_reverify, closed}。每列欄位:
| 欄 | 型別 | 來源 |
|---|---|---|
| project_uid, project_name, framework | str | projects |
| round_uid, round_no | str, int | rounds |
| pass_uid, pass_no, audit_nature, nature_seq, is_current | str, int, str, int, bool | passes |
| status | str | passes.status |
| ap_uid | str/null | AP;null=尚未建立 |
| filled_ao, total_ao, progress_pct | int, int, int/null | 上述子查詢;ap_uid null 時三者 null |
| plan_due | iso/null | job due_at |
| audit_start, audit_end | iso/null | ap_tasks.timing |
| auditors | [{uuid, name}] | AP parties assessor |
| managers | [{login_name, nickname}] | participants manager |
| my_roles | [str] | participants(登入者) |
| can_edit | bool | status=audit_planning and is_current and ('auditor' in my_roles or 'manager' in my_roles) |
| needs_assign | bool | ap_uid null or auditors 空,且 'manager' in my_roles |
| updated_at | iso | greatest(pass.updated_at, AP 各表 updated_at) |
scripts/sql/2026-10-xx-fr144-audit-plan-list-route.sql,主線 phase=seed,照 2026-10-15-fr139-version-history-routes.sql 四段:① ui_routes INSERT(WHERE NOT EXISTS name='audit-plan-list';root 0、pid 42、name audit-plan-list、url /project/audit-plans、icon pi pi-list(primeicons 6.0.1 沒有 pi-list-check)、sort 25、is_menu true、description 「稽核計畫清單(登入者相關的每一份)」);② route_capabilities 綁 grc.audit.read ALL;③ DO 區塊驗「存在、pid=42、恰一顆 ALL」;④ schema_migrations 登記。只套 DEV;寫完回報「出貨基線待重產」。
/project/audit-plans,name audit-plan-list,component @/views/project/AuditPlanListView.vue,meta breadcrumb label audit-plan-list。AUDIT_PLANS_MY_LIST: getUrl('/audit-plans/my/list')、AUDIT_PLANS_MY_COUNT: getUrl('/audit-plans/my/count')。src/config/locales/i18n/{zh-tw,en}/menu.json 加 audit-plan-list(「稽核計畫」/"Audit Plans")與描述;新檔 audit-plan-list.json 放頁內字串(分段鈕、欄名、空狀態「當你被列為某份稽核計畫的稽核人員,或你管理的專案進到稽核計畫填寫站時,會出現在這裡」、按鈕 tooltip)。SelectButton 或 Button 群當分段鈕(帶 meta.status_counts)、IconField 搜尋框(300ms debounce)、PrimeVue DataTable lazy 模式(:lazy="true" @page @sort 打後端),欄位與格式照雛形「一格一件事」;整列 @row-click 導頁;最右 icon-only Button 必配 v-tooltip.top;loading 配 :loading。router.push({name:'project-ap-authoring-round', params:{id: project_uid, roundUid: round_uid}, query: is_current ? {} : {pass: pass_uid}});needs_assign → {name:'project-planning-round', params:{id, roundUid}}。AppMenuItem.vue 支援 item 可選 badgeCount;選單載入時與從本頁離開時打 count 端點一次;FR-143 改善計畫入口沿用同機制。| 卡 | 範圍 | 動哪裡 | 套件 | 依賴 | 驗收 |
|---|---|---|---|---|---|
| 1 清單端點+選單登記 | 讀模型、app service、DI、route、schema、count 端點、seed migration | BE 新檔 infra/readmodel/audit/audit_plan_list_query.py、app/readmodel/service/audit_plan_list_service.py、api/project/routes/audit_plan_list_route.py、api/project/serializers/audit_plan_list.py、scripts/sql/2026-10-xx-fr144-audit-plan-list-route.sql;改 di_containers/flow_control/flow_control_containers.py、route 註冊 |
無 | 等 FR-142 線對 auditor_dashboard_ 的改動 commit 後再開* | 驗收 2~4(API 部分)、9、10 |
| 2 清單頁+徽章 | 新頁、路由、api 常數、i18n、側邊選單徽章 | FE 新檔 src/views/project/AuditPlanListView.vue、src/config/locales/i18n/{zh-tw,en}/audit-plan-list.json;改 router/index.js、api.js、menu.json×2、layout/AppMenuItem.vue |
無 | 卡 1 | 驗收 1~10 |
| # | 坑 | 怎麼避 |
|---|---|---|
| 1 | control_objective_selections 外層陣列長度恆為 1,拿它當分母全部變 1 |
分母數內層 include-objectives |
| 2 | 目前稽核次帶 ?pass= 會讓 FR-141 頁進「檢視過去那次」唯讀 |
is_current 為真不帶 pass |
| 3 | 外稽未開始 assessment_plan_id NULL,所有 AP 子查詢回 NULL |
一律 LEFT/子查詢;FE 顯示「尚未建立」「未指派」 |
| 4 | DEV 的填寫站 job due_at 多為 NULL,清單期限全空會被當 bug |
驗收 10 要先在流程範本設站期限再測 |
| 5 | 「我的稽核」一輪一列、本案一稽核次一列,兩頁列數對不上 | 刻意的;稽核次欄寫明第幾次 |
| 6 | route_capabilities 漏綁 → 所有人看不到選單項且無錯誤 |
migration DO 區塊驗恰一顆 ALL |
| 7 | 徽章每切頁打一次 count | 只在選單載入與離開本頁時打;count SQL 不 join AP |
| 8 | 對著 FR-142 線變動中的 auditor_dashboard_query.py 抄形狀 |
卡 1 等那條線 commit 後開工,抄 commit 後的版本 |
| 9 | 搜尋詞「外部」同時命中專案名含「外部」的列 | 接受;性質比對是 OR 條件之一,不排除專案名 |