FR-150 · 設計文件 · 2026-10-18(D1~D11 定案)
專案總覽一顆鈕開彈窗,勾未完成任務、留一句話、寄出
到期提醒目前只有排程自動寄兩次(剩 3 天、到期當天)。PM 遇到急件想不等排程直接催人,本案在專案總覽動作列加「寄送任務提醒」,開一個彈窗列出本輪未完成任務,PM 勾要催的、可附一段留言,按一次寄出。信寄給每張卡目前該處理的人,每張卡留一則系統留言記錄被催過,寄送紀錄進既有的通知紀錄頁並標出是誰寄的。
狀態:設計定案(D1~D11),已開卡待派|日期:2026-10-18|母卡 CM-2894、子卡 CM-2895 / CM-2896 / CM-2897 需求與決策紀錄:
requirement.md|雛形:prototype/index.html開發 branch:三個 repo 目前都在feature/FR-139(開工前 runner 自己git branch --show-current確認,不對就停下問)
🔴 這份是設計決策,不是現況。 內文的行號停在 2026-10-18 定稿當時,實作改動的結果不回頭改寫這裡。
PM 在專案總覽按「寄送任務提醒」,彈窗勾幾張未完成任務、可附留言,按一次寄出提醒信給每張卡目前該處理的人。
到期提醒(FR-140)只在剩 3 天與到期當天各自動寄一次,同一階段不重寄。PM 遇到急件時有三個缺口:
逐張開抽屜按太煩,一鍵全發會催到不急的人,所以做「一個彈窗、勾、寄」。
sequenceDiagram
actor PM as 專案 manager
participant FE as 專案總覽
participant BE as 後端
actor AS as 任務處理人
PM->>FE: 按「寄送任務提醒」
FE->>BE: GET 候選任務(本輪未完成)
BE-->>FE: 任務清單(含處理人、期限、上次手動提醒時間)
PM->>FE: 勾任務、填留言、按寄出
FE->>FE: 勾到 10 分鐘內寄過的 → 二次確認
FE->>BE: POST job_uids+message
BE->>BE: 驗 manager、驗每張卡屬於本專案本輪且未完成
loop 每張卡
BE->>AS: 寄一封提醒信(每位處理人一封)
BE->>BE: 寫通知紀錄(triggered_by=PM)
BE->>BE: 卡上留一則系統留言
end
BE-->>FE: 寄了幾封、失敗幾封、略過幾張
FE-->>PM: toast 結果
| 誰 | 看到/做什麼 |
|---|---|
| 專案 manager | 總覽動作列多一顆「寄送任務提醒」;彈窗勾任務、留言、寄出;toast 看結果 |
| 任務處理人 | 收到一封「專案管理者提醒」信,附 PM 留言與卡片連結;開卡看到一則系統留言 |
| 其他專案參與者 | 通知紀錄頁多出這類列,標「管理者提醒 · <PM 暱稱>」;任務卡上看得到系統留言 |
| 沒設定任何東西的客戶 | 什麼都不變,自動到期提醒照舊 |
| # | 決策 | 定案 | 理由 | 被排除的選項 |
|---|---|---|---|---|
| D1 | 入口與互動 | 專案總覽動作列一顆鈕,開彈窗勾選後一次寄 | PM 催人的時機是看整體進度時;勾選兼顧「他選」與「一次多張」 | 逐卡抽屜按鈕(決策者:一個一個點會崩潰);一鍵全發(催到不急的人、無法說原因) |
| D2 | 誰能按 | 專案 manager;本輪已結束時入口不顯示、端點擋 | 與改期限同一條守門(assert_project_manager+輪次未結束) |
— |
| D3 | 列哪些卡 | 本輪狀態 TODO/PROCESSING 的卡;沒輪次的舊專案退回全專案 | 與「開始執行任務」同一套輪次解析(round_no 最大),避免催到上一輪殘卡 | 跨輪全列(覆核輪會撈到母輪殘卡) |
| D4 | 沒期限的卡 | 列出、可勾 | 手動版的本質是 PM 判斷急,不綁期限 | 只列有期限的卡 |
| D5 | 沒人可寄的卡 | 列出但灰掉不能勾;後端收到也略過並回報張數 | 寄了也沒人收;讓 PM 看見「這張還沒派人」本身也有用 | 不列(PM 會以為漏了) |
| D6 | 寄給誰 | 每張卡的處理人(見 D10);發信的 PM 與其他 manager 都不寄 | PM 知道自己寄了;manager 互收催信是噪音;要知道有人催過看卡上系統留言 | 照自動版一起寄 manager |
| D7 | 一人多卡 | 同一收件人合併成一封清單信(決策者 2026-10-22 確認) | 同一人被催多張卡時只收一封,收件匣不被洗版;信內逐張列出卡片 | 一卡一封 |
| D8 | 留言 | 選填、上限 500 字、整批共用一段;進信件與系統留言 | PM 要能說為什麼急;逐卡留言會讓彈窗變表單 | 逐卡留言 |
| D9 | 重複寄 | 可以;勾到 10 分鐘內寄過的卡,寄出前二次確認;後端不擋 | 急件催兩次是正常需求,二次確認擋手滑就夠 | 後端硬擋冷卻期;每日上限 |
| D10 | 處理人怎麼算(FR-148 任務關卡後) | 卡有關卡且已過第 1 關 → 寄當下那關的人(job_review_step_assignees);零關或在第 1 關 → 寄 task_assignees |
「催」要催現在卡在誰手上;卡在主管審的時候催執行人沒用 | 一律寄原指派人;指派人+當下關卡都寄 |
| D11 | 卡上系統留言 | 每張被寄的卡留一則,作者「系統」,內容「管理者 <暱稱> 已寄出提醒」+留言 | 處理人開卡就看到被催過;補上 D6 不寄 manager 的資訊缺口 | 不留 |
D10 決策者原話:「就是通知當下在執行任務的人」。
| 項目 | 位置 | 本案怎麼接 |
|---|---|---|
| 自動到期提醒 | app/task_alert/service/job_due_reminder_service.py |
不改它;把「寄一封+寫紀錄」那段抽成共用 helper,自動版與手動版都呼叫 |
| 收件人查詢 | infra/readmodel/tasks/job_due_reminder_query.py::recipients |
不改;手動版另寫收件人查詢(D6 不含 manager、D10 依關卡) |
| 通知紀錄表 | compliance.task_alert_notifications;唯一索引 (object_type, COALESCE(scan_uid, object_uid), COALESCE(stage,''), recipient_email) |
加 triggered_by;手動列 object_type=job_due、stage=manual_<epoch> 天然繞過唯一鍵 |
| 通知紀錄 API | app/task_alert/service/task_alert_notification_app_service.py::_to_view、api/task_alert/serializers/task_alert_notification.py |
回傳加 triggered_by、triggered_by_name |
| 通知紀錄頁 | FE src/views/project/ProjectAlertNotificationsView.vue:203,標籤 t(\lang.alert_notifications.job_due_${data.stage}`)` |
陷阱:手動列 stage 是 manual_<epoch>,照拼會找不到 key。判 stage.startsWith('manual_') 走另一個 key |
| 守門 | app/flow_control/service/job_service.py::_require_manager(內部 assert_project_manager) |
手動版 service 用同一個 common.authz.project.assert_project_manager |
| 輪次解析 | infra/flow_control/repository/task_execution_query.py::_current_round_id(round_no 最大) |
候選清單用同一個邏輯;不要另寫一套,抽出或直接呼叫 |
| 系統留言 | app/flow_control/service/not_met_job_redo_service.py:80:job_comment_domain_service.create(job_uid, author_id=None, author_nickname=<系統>, content) |
照抄這個寫法 |
| 關卡 | compliance.job_executions.review_step_no(NULL=零關)、compliance.job_review_step_assignees(第 2 關以後的人) |
D10 收件人依此判斷 |
| 總覽動作列 | FE src/views/project/ProjectAuditorOverview.vue:1113-1160 |
該檔 2,134 行已超標,彈窗一律新元件,總覽頁只加一顆鈕+一行掛元件 |
| 待辦清單 view | public.vw_user_job_queue(一列一指派人) |
不能拿來當候選清單:沒指派人的卡不會出現,D5 要列灰掉的卡。另寫以任務為主體的查詢 |
migration:scripts/sql/2026-10-18-fr150-task-alert-triggered-by.sql
ALTER TABLE compliance.task_alert_notifications
ADD COLUMN IF NOT EXISTS triggered_by varchar(64); -- 2026-10-18
COMMENT ON COLUMN compliance.task_alert_notifications.triggered_by
IS '手動觸發者 login_name;排程自動寄的為 NULL'; -- 2026-10-18phase=active、envs=*,只套 DEV;回報「出貨基線待重產」。infra/task_alert/models/ 對應 model 加欄(帶 comment=);entity 加 triggered_by: Optional[str] = None。stage:manual_<epoch 秒>,同一批共用同一個值,之後查「這一批寄了哪些」對得起來。stage 欄已是 varchar(32),manual_ 加 10 位數 17 字元,夠用。兩支都在 api/task_alert/,route 不碰 session。
| 方法 | 路徑 | 用途 |
|---|---|---|
| GET | /project/<project_uid>/job-due-reminder/candidates |
彈窗清單 |
| POST | /project/<project_uid>/job-due-reminder |
寄出 |
GET 回傳(每張卡一筆):
| 欄位 | 說明 |
|---|---|
job_uid、job_name |
— |
control_no、control_title |
控制項代碼與名稱,沒有就 null |
status |
TODO/PROCESSING |
due_at、due_at_manual |
期限 |
recipients |
[{user_id, login_name, nickname}],依 D10 算出的處理人;空陣列=灰掉 |
review_step_label |
有關卡時「第 N 關 · <關名>」,零關為 null |
last_manual_reminded_at |
該卡最近一筆 stage LIKE 'manual_%' 的 created_at,沒有就 null |
排序在 BE 做:逾期 → 3 天內 → 其他有期限 → 沒期限;同級依期限近到遠,再依任務名。
POST body:{ "job_uids": [...], "message": "..." },job_uids 1~200 筆,message 可空、≤500 字。
POST 回傳:{ jobs, sent, failed, skipped_no_recipient }。
錯誤:
| 情況 | 例外 | error code |
|---|---|---|
| 不是 manager | ForbiddenError |
沿用 GRC_NOT_MANAGER |
| 本輪已結束 | PreconditionFailedError |
新增一個 GRC_412xxx(本輪已結束,不能寄提醒) |
job_uids 有不屬於本專案本輪、或已完成/取消的卡 |
BadRequestError,整批退回不部分寄 |
新增一個 GRC_400xxx |
task_alert 模組既有的錯誤碼就用 common/code/flow_control_error_code.py,新碼也加在那裡,實作時查當下各段最大序號往下接(FR-148 平行開發中,序號會變);同步 FE error-code.json。
新檔 app/task_alert/service/manual_job_due_reminder_service.py,兩個 public method 都 @transaction:
list_candidates(project_uid, user_id):守門 → 查詢 → 組回傳。send(project_uid, job_uids, message, user_ctx):守門 → 驗卡 → 逐卡寄信+寫紀錄+留言 → 回統計。共用 helper:把自動版 _notify_job 內「claim 紀錄 → 寄 → mark」那段抽成 app/task_alert/service/job_due_mail_sender.py,兩邊共用;自動版行為不變。
信件(force_locale("zh_Hant_TW"),與自動版一致):
job_due_manual_mail_subject:[%(system)s] 專案管理者提醒:%(job)sjob_due_mail_content,when 用 job_due_manual_mail_when:「專案管理者 %(pm)s 請您儘速處理」job_due_manual_mail_message:「html.escape 後把換行轉 <br>_due_text 回空字串會留下一行「完成期限:」,要處理)系統留言 key job_due_manual_comment:「管理者 %(pm)s 已寄出提醒」,有留言時接「:%(message)s」。作者用 not_met_redo_comment.author_nickname() 同一個系統名稱。留言寫入失敗只記 warning,不影響寄信結果。
寄送:同步。一封失敗不影響其他封;SMTP 未啟用時 send_mail_notification 回 False,照自動版記 failed。
新檔 infra/readmodel/tasks/manual_job_due_reminder_query.py,出口 list[dict]。
list_candidates(project_id):以 job_executions 為主體,用 workflow_execution_control_mapping.round_id = <本輪> 圈範圍(沒輪次退回 task_assignees.project_id),JOIN 控制項名稱、review_step_no;處理人與上次手動提醒另兩支批次查詢補上,不在主 SQL 裡 GROUP。recipients_for(job_ids):D10 規則,回 {job_id: [(user_id, email, nickname, login_name)]},排除 email 為空者。last_manual_reminded(job_uids):MAX(created_at) WHERE object_type='job_due' AND stage LIKE 'manual\_%'。validate_jobs(project_id, job_uids):回不合格的 uid 清單。總覽頁(ProjectAuditorOverview.vue)只加:
<Button v-if="isProjectManager && selectedApUid && !selectedAp?.is_finished"
outlined size="small" icon="pi pi-send"
:label="t('lang.job_due_reminder.btn_entry')"
@click="reminderDialogVisible = true" />
<JobDueReminderDialog v-model:visible="reminderDialogVisible" :project-uid="projectId" />
位置放在「通知紀錄」鈕前面,兩顆相鄰。
新元件 src/components/grc/job-due-reminder/JobDueReminderDialog.vue,資料與送出邏輯放 composable src/composables/useJobDueReminder.js:
min(960px, 95vw)。JobDueDateText)/上次提醒(「X 分鐘前」「X 天前」「—」)。recipients 為空的列:勾選框 disabled、整列降透明度、處理人欄寫「尚未指派」。maxlength=500,右下字數);一行摘要「將寄給 N 張任務、共 M 位處理人」(M 為去重人數);「取消」「寄出」。last_manual_reminded_at 在 10 分鐘內的卡時,先 confirm.require 列出張數再確認。通知紀錄頁(ProjectAlertNotificationsView.vue):job_due 列判 stage.startsWith('manual_') 時,標籤改 lang.alert_notifications.job_due_manual(「管理者提醒」),後接 triggered_by_name。
i18n:新檔 job-due-reminder.json(zh-tw/en 兩份),namespace job_due_reminder;FE 的 i18n 是手動 import,要在 src/config/locales/index.js 補兩行(比照 alert-notifications.json 第 60、160 行),漏了畫面會顯示 key 原字串。通知紀錄補一個 key。
設計守門:照 guidant-design-system skill;不准左側粗色邊框;字級字型走全站。
project-management/project-overview.md §1.1 動作列加一列。project-management/project-alert-notifications.md §1 來源加第三種、§11 加手動寄送一列、§6 欄位加 triggered_by。audit-execution/my-tasks.md §11.5 到期提醒段補一句「PM 也可在專案總覽手動寄」。| 卡 | 範圍 | 依賴 | 驗收 |
|---|---|---|---|
| ① BE 全部(CM-2895) | migration、ORM/entity、共用寄信 helper(自動版改呼叫它)、readmodel、service、兩支端點、error code、信件與留言 i18n、通知紀錄 API 加欄 | — | 用 curl 打 GET 看到本輪未完成卡(含未指派卡、空 recipients);POST 兩張卡,信箱各收到一封、紀錄表多兩列 stage=manual_*、triggered_by 有值、卡上各多一則系統留言;自動版排程照舊寄(把一張卡期限設 3 天後跑一次 tick) |
| ② FE 彈窗+通知紀錄標示(CM-2896) | 總覽頁入口、JobDueReminderDialog、composable、通知紀錄頁手動列標籤、i18n、error-code.json |
① | 對照雛形;10 分鐘內再寄跳二次確認;通知紀錄頁手動列顯示「管理者提醒 · PM 名」 |
| ③ spec(CM-2897) | §5.6 三頁 | ①② | 決策者收尾令下才做 |
回歸:自動到期提醒(剩 3 天、當天)、通知紀錄頁其他三種列的標籤、總覽頁其他按鈕。
| 你想知道 | 看這份 |
|---|---|
| PM 原話與每題怎麼裁 | requirement.md |
| 畫面長怎樣 | 雛形 |
| 為什麼這樣決定、要做哪些 | 本頁 |