FR-144 · 設計定案 v1 · 2026-10-17

稽核計畫直達入口:設計定案

左側選單「稽核計畫」、一個稽核次一列的表格、後端分頁、整列直達 FR-141 填寫頁

左側選單「待辦任務」底下多一項「稽核計畫」,旁邊帶「待填寫」筆數。進去是一張表格:一個稽核次一列,狀態分段鈕帶筆數、一個搜尋框、欄頭排序、後端分頁、整列可點直達該份稽核計畫。版式與 FR-142「我的稽核」、FR-143「改善計畫」入口同一套。不加表、不動套件:一支清單讀模型+端點、一筆選單登記、一支 FE 頁。本文件寫定案規則、為什麼這樣定、踩到會怎樣。

D1~D3 定案(2026-10-17) 兩張卡,端點先、頁面後 不動套件、不加表 陷阱:目前稽核次不帶 pass、分母數 include-objectives

🔴 這份是設計決策,不是現況。 內文停在 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。

§1

變更紀錄

日期 變更 對應
2026-10-17 v1:D1~D3 定案;資料模型、清單查詢、端點契約、FE 頁面、兩張卡、端到端驗收 討論稿定案

§2

分工概述(30 秒版)

整案一句話:左側選單多一項「稽核計畫」,點進去一張表格列跟我有關的每一份稽核計畫,點列直達 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 稽核計畫頁(不動)]
圖 1 — 兩張卡的依賴

§3

決策定案

決策 定案 理由 被排除方案
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 做法一致,工具列少三個下拉 狀態/性質/角色三個下拉

§4

現況接入點

元件 現況 本案動作
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 改善計畫入口共用

§5

詳細設計

① 清單查詢(卡 1)

檔: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 service 與端點(卡 1)

  • 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:di_containers/flow_control/flow_control_containers.py 加 Factory(照 auditor_dashboard_service 那段)。
  • Route: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 取)。
  • Schema: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)

③ 選單登記(卡 1)

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;寫完回報「出貨基線待重產」。

④ 清單頁(卡 2)

  • 路由:/project/audit-plans,name audit-plan-list,component @/views/project/AuditPlanListView.vue,meta breadcrumb label audit-plan-list。
  • api 常數:AUDIT_PLANS_MY_LIST: getUrl('/audit-plans/my/list')、AUDIT_PLANS_MY_COUNT: getUrl('/audit-plans/my/count')。
  • i18n: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 改善計畫入口沿用同機制。

§6

端到端驗收

  1. 選單:顧問帳號登入,左側「待辦任務」底下出現「稽核計畫 N」,N=待填寫筆數;viewer-only 帳號看不到此項。
  2. 清單內容:DEV 建一個專案,走到填寫站(內稽第 1 次),清單出現一列「第 1 輪 · 內稽第 1 次」待填寫 0%;在 FR-141 頁填三條 AO 的方法後回清單,百分比變為 3/總數;填計畫概要的稽核日期後,稽核日期欄兩行顯示。
  3. 多稽核次:同專案走到內稽第 2 次,清單多一列「內稽第 2 次」,第 1 次那列變「稽核中」或「已結案」、只有檢視。
  4. 外稽未指派:走到外稽填寫站且未指派稽核人員;PM 帳號看到該列「未指派」+「去指派」跳專案規劃頁;只是 auditor 的帳號看不到該列。
  5. 覆核者:reviewer 帳號看得到列、只有「檢視」、按下去 FR-141 頁唯讀。
  6. 分段鈕與搜尋:切「已結案」筆數與列數一致;搜「外稽」只剩外部稽核;搜人名只剩該人參與的;清空回全部。
  7. 排序與分頁:點「填寫期限」升、降、取消三態;每頁 20,第二頁有資料;排序切換後分頁回第 1 頁。
  8. 點列導頁:目前稽核次的列導到 FR-141 頁可編輯;已結束那次的列導過去是唯讀且麵包屑顯示該次。
  9. D2:規劃中的輪次不在清單。
  10. 期限:填寫站 job 有 due_at 的列,待填寫時顯示「逾期 N 天」或「剩 N 天」;鎖定後不標。

§7

拆分表

卡 範圍 動哪裡 套件 依賴 驗收
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

§8

坑與陷阱

# 坑 怎麼避
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 條件之一,不排除專案名