From 5e9ef9cab2dcb3f67f0f56bd3b3cd3d7801d1e22 Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 21 Mar 2026 22:04:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20R17-4=20=E8=AF=95?= =?UTF-8?q?=E7=82=B9=E8=A7=82=E6=B5=8B=E6=94=AF=E6=92=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/portal.ts | 15 ++++ client/src/pages/PortalHomePage.tsx | 35 ++++++++++ deploy/trialrun/checklist.md | 2 +- deploy/trialrun/observation-template.md | 37 ++++++++++ deploy/trialrun/runbook.md | 1 + docs/admin-handbook.md | 6 ++ docs/feature-matrix.md | 1 + ...-03-21_12-40-00-r17-small-team-go-live.csv | 16 ++--- server/src/routes/portal.js | 68 ++++++++++++++++++- 9 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 deploy/trialrun/observation-template.md diff --git a/client/src/api/portal.ts b/client/src/api/portal.ts index e37259ca..41d12072 100644 --- a/client/src/api/portal.ts +++ b/client/src/api/portal.ts @@ -29,6 +29,21 @@ export type PortalHomePayload = { weeklySummary: { weekStart: string; submittedCount: number; missingCount: number; needsHelpCount: number; href: string } | null; todoSummary: { openCount: number; highPriorityCount: number; href: string } | null; peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null; + adoptionSummary: + | { + activeEmployeeCount: number; + login7dCount: number; + weeklySubmittedCount: number; + weeklyMissingCount: number; + presenceUpdatedCount: number; + presenceMissingCount: number; + announcementCount: number; + login7dRate: number; + weeklySubmissionRate: number; + presenceCoverageRate: number; + hints: string[]; + } + | null; }; export async function fetchPortalHome() { diff --git a/client/src/pages/PortalHomePage.tsx b/client/src/pages/PortalHomePage.tsx index 7c8f2b12..07b4fa61 100644 --- a/client/src/pages/PortalHomePage.tsx +++ b/client/src/pages/PortalHomePage.tsx @@ -131,6 +131,41 @@ export function PortalHomePage() { + {payload.adoptionSummary ? ( +
+
+

试点观测摘要

+ 查看工作区设置 +
+
+
+ 近 7 天登录:{payload.adoptionSummary.login7dCount}/{payload.adoptionSummary.activeEmployeeCount} +
覆盖率 {payload.adoptionSummary.login7dRate}%
+
+
+ 本周周报:已交 {payload.adoptionSummary.weeklySubmittedCount} +
提交率 {payload.adoptionSummary.weeklySubmissionRate}%
+
+
+ 今日状态:已更新 {payload.adoptionSummary.presenceUpdatedCount} +
覆盖率 {payload.adoptionSummary.presenceCoverageRate}%
+
+
+ 当前公告:{payload.adoptionSummary.announcementCount} +
用于试点期统一通知
+
+
+
+
本周观测提示
+
    + {payload.adoptionSummary.hints.map((hint) => ( +
  • {hint}
  • + ))} +
+
+
+ ) : null} +

工作台摘要

diff --git a/deploy/trialrun/checklist.md b/deploy/trialrun/checklist.md index b8122cb0..a37afe16 100644 --- a/deploy/trialrun/checklist.md +++ b/deploy/trialrun/checklist.md @@ -65,4 +65,4 @@ - [ ] 至少 1 条公告已发布并在 `Portal` 可见 - [ ] 至少 1 次入职/离职/资产交接已演练 - [ ] 全员已知晓每周周报和今日状态使用要求 -- [ ] 已建立连续 4 周观测记录方式 +- [ ] 已建立连续 4 周观测记录方式(建议直接使用 `deploy/trialrun/observation-template.md`) diff --git a/deploy/trialrun/observation-template.md b/deploy/trialrun/observation-template.md new file mode 100644 index 00000000..bd3462f5 --- /dev/null +++ b/deploy/trialrun/observation-template.md @@ -0,0 +1,37 @@ +# 小团队试点 4 周观测模板 + +> 建议由管理员或老板每周五填写一次,用于 `R17-6` 最终验收留痕。 + +## 基本信息 +- 工作区模式:`small_team_internal` +- 观测周期:第 __ 周(起止日期:____ 至 ____) +- 填写人:____ + +## 量化指标 +| 指标 | 本周值 | 目标 | 结论 | +| --- | --- | --- | --- | +| 近 7 天登录人数 / 在职人数 | __ / __ | 全员每周至少登录 1 次 | __ | +| 本周周报提交人数 / 在职人数 | __ / __ | 全员提交 | __ | +| 今日状态更新人数 / 在职人数 | __ / __ | 全员工作日可见 | __ | +| 在岗表是否已停止外部维护 | 是 / 否 | 是 | __ | +| 离职交接表是否已停止外部维护 | 是 / 否 | 是 | __ | + +## 本周关键动作 +- 公告 / Portal / 周报 / 待办 的使用情况: +- 入职 / 离职 / 资产交接是否发生: +- 仍依赖聊天或表格补流程的场景: + +## 本周阻塞 +- 阻塞 1: +- 阻塞 2: +- 阻塞 3: + +## 下周调整 +- 是否需要调整提醒频率: +- 是否需要继续隐藏或降级某模块: +- 是否需要补充培训或说明: + +## 结论 +- 本周是否达到试点预期:是 / 否 +- 若否,主要原因: +- 最终验收建议:继续观测 / 可进入验收 diff --git a/deploy/trialrun/runbook.md b/deploy/trialrun/runbook.md index fb35cfb3..baae9711 100644 --- a/deploy/trialrun/runbook.md +++ b/deploy/trialrun/runbook.md @@ -224,4 +224,5 @@ docker logs -f hyb-oa-trialrun-server - 团队状态更新人数 - 公告阅读反馈 - 入职/离职/资产交接演练情况 +- 建议统一使用 `deploy/trialrun/observation-template.md` 填写每周结论与阻塞。 - 若连续两周无人使用某入口,考虑继续降级模块可见性。 diff --git a/docs/admin-handbook.md b/docs/admin-handbook.md index f523bea4..76e29a06 100644 --- a/docs/admin-handbook.md +++ b/docs/admin-handbook.md @@ -74,3 +74,9 @@ - 至少 1 次真实离职或模拟离职跑通 - 全员连续 4 周提交周报 - 管理员不再额外维护“今日在岗表”或“离职交接表” + +## 11. 4 周观测操作法 +1. 每周一进入 `Portal` 查看“试点观测摘要”,记录近 7 天登录覆盖率、周报提交率、团队状态覆盖率。 +2. 每周五在 `deploy/trialrun/observation-template.md` 填写本周结论、关键阻塞、是否需要调整提醒或导航。 +3. 若某周有员工未登录、未提周报、未更新状态,优先通过公告或站内通知补提醒,不引入额外表格。 +4. 连续 4 周都达到预期后,再将 `R17-6` 关单并形成最终验收结论。 diff --git a/docs/feature-matrix.md b/docs/feature-matrix.md index de373265..9435993a 100644 --- a/docs/feature-matrix.md +++ b/docs/feature-matrix.md @@ -345,4 +345,5 @@ - **R15 团队状态**:新增 `EmployeePresence` 与团队在岗视图,支持员工手工更新今日状态,请假自动覆盖为 `leave`。 - **R16 周报与待办**:新增 `WeeklyReport` 与 `TodoItem`,支持周报懒创建、提交、团队汇总、待办创建/完成/取消,并接入 `Portal` 与搜索。 - **R17 真可用收口**:新增小团队管理员手册、员工快速上手文档、trialrun 运行手册与检查项补充,支持以 `small_team_internal` 为默认上线模式。 +- **R17-4 试点观测支撑**:`Portal` 新增管理员试点观测摘要(近 7 天登录、周报提交、团队状态覆盖率),并补充 4 周观测模板。 - **默认上线建议**:开启 `portal_home_v1`、`announcements_v1`、`presence_board_v1`、`weekly_report_v1`、`employee_lifecycle_v1`、`todo_board_v1`、`global_search_v1`;保留 `payment_batch_v1`、`cutover_console_v1` 为管理员备用能力。 diff --git a/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv b/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv index a9e40861..d04769a8 100644 --- a/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv +++ b/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv @@ -1,8 +1,8 @@ -ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes -R17-0,R17执行合同建档与上线边界冻结,创建并校验小团队上线收口合同 CSV 并冻结本阶段只做默认配置 放量 文档 手册 试点和验收,CSV 校验通过且明确目标是小团队真正可用而不是继续扩展新模块,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-21_12-40-00-r17-small-team-go-live.csv,manual,DONE,DONE,DONE,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|issues/2026-03-21_12-30-00-r16-weekly-report-todo.csv,[],none -R17-1,默认配置与功能开关收口,为 trialrun 和 seed 打开 small_team_internal profile 与核心小团队功能开关 并默认降级重模块,trialrun 默认开启 Portal 公告 周报 待办 团队状态 人员生命周期 并默认关闭 payment batch cutover 与重模块主导航,cd server && npm test && git diff --check,manual,DONE,DONE,DONE,server/src/scripts/seed-config.js|server/.env.example|deploy/trialrun/.env.example|server/src/services/featureFlags/index.js,"[""R17-0""]",none -R17-2,存量员工与资产数据整理支撑,提供小团队上线所需的人员档案 资产归属和部门关系整理支撑接口或导入校验,管理员可在上线前核对员工档案 部门关系和资产归属缺口,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/employees.js|server/src/routes/assets.js|client/src/pages/PeoplePage.tsx|client/src/pages/AssetRegistryPage.tsx,"[""R17-1""]",none -R17-3,管理员手册与员工使用手册,新增 admin-handbook 和 employee-quickstart 并补齐试运行 runbook/checklist 的小团队模式步骤,管理员和员工均有可执行手册 支撑首次配置 入职 离职 周报与团队状态的真实演练,git diff --check,manual,DONE,DONE,DONE,docs/admin-handbook.md|docs/employee-quickstart.md|deploy/trialrun/runbook.md|deploy/trialrun/checklist.md,"[""R17-1""]",none -R17-4,全员试点与观测留痕,补齐试点观测字段 页面或文档留痕 支撑 4 周使用观测和真实验收记录,系统可记录或沉淀全员使用情况 周报提交率 关键阻塞和试点观察结论,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/portal.js|client/src/pages/PortalHomePage.tsx|docs/admin-handbook.md|docs/feature-matrix.md,"[""R17-2"",""R17-3""]",none -R17-5,默认收口无用主导航模块,按小团队产品模式彻底收口全员默认导航 仅为管理员保留备用企业模块入口,全员默认主导航仅剩 Portal 团队 请假 报销 资产 周报 待办 公告 管理员附加入口符合约定,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/navigation/NavLinks.tsx|client/src/router/index.tsx|client/src/pages/PortalHomePage.tsx,"[""R17-1""]",none -R17-6,R17最终验收,完成默认配置 文档手册 试点观测和主导航收口后的最终验收,全员账户 档案 部门关系已补齐 入职离职资产交接至少各跑通一次 全员连续使用周报与团队状态且主回归 E2E 全绿,cd server && npm test && ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build && npm run test:e2e,manual,DOING,TODO,TODO,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|server/scripts/regression-all.sh|client/tests/e2e|docs/admin-handbook.md|docs/employee-quickstart.md,"[""R17-4"",""R17-5""]",代码/文档/回归基线已完成;仍待真实 4 周试点观测与全员连续使用证据后再关单 +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +R17-0,R17执行合同建档与上线边界冻结,创建并校验小团队上线收口合同 CSV 并冻结本阶段只做默认配置 放量 文档 手册 试点和验收,CSV 校验通过且明确目标是小团队真正可用而不是继续扩展新模块,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-21_12-40-00-r17-small-team-go-live.csv,manual,DONE,DONE,DONE,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|issues/2026-03-21_12-30-00-r16-weekly-report-todo.csv,[],none +R17-1,默认配置与功能开关收口,为 trialrun 和 seed 打开 small_team_internal profile 与核心小团队功能开关 并默认降级重模块,trialrun 默认开启 Portal 公告 周报 待办 团队状态 人员生命周期 并默认关闭 payment batch cutover 与重模块主导航,cd server && npm test && git diff --check,manual,DONE,DONE,DONE,server/src/scripts/seed-config.js|server/.env.example|deploy/trialrun/.env.example|server/src/services/featureFlags/index.js,"[""R17-0""]",none +R17-2,存量员工与资产数据整理支撑,提供小团队上线所需的人员档案 资产归属和部门关系整理支撑接口或导入校验,管理员可在上线前核对员工档案 部门关系和资产归属缺口,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/employees.js|server/src/routes/assets.js|client/src/pages/PeoplePage.tsx|client/src/pages/AssetRegistryPage.tsx,"[""R17-1""]",none +R17-3,管理员手册与员工使用手册,新增 admin-handbook 和 employee-quickstart 并补齐试运行 runbook/checklist 的小团队模式步骤,管理员和员工均有可执行手册 支撑首次配置 入职 离职 周报与团队状态的真实演练,git diff --check,manual,DONE,DONE,DONE,docs/admin-handbook.md|docs/employee-quickstart.md|deploy/trialrun/runbook.md|deploy/trialrun/checklist.md,"[""R17-1""]",none +R17-4,全员试点与观测留痕,补齐试点观测字段 页面或文档留痕 支撑 4 周使用观测和真实验收记录,系统可记录或沉淀全员使用情况 周报提交率 关键阻塞和试点观察结论,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/portal.js|client/src/pages/PortalHomePage.tsx|docs/admin-handbook.md|docs/feature-matrix.md|deploy/trialrun/observation-template.md,"[""R17-2"",""R17-3""]",Portal 已提供试点观测摘要并补齐 4 周观测模板 +R17-5,默认收口无用主导航模块,按小团队产品模式彻底收口全员默认导航 仅为管理员保留备用企业模块入口,全员默认主导航仅剩 Portal 团队 请假 报销 资产 周报 待办 公告 管理员附加入口符合约定,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/navigation/NavLinks.tsx|client/src/router/index.tsx|client/src/pages/PortalHomePage.tsx,"[""R17-1""]",none +R17-6,R17最终验收,完成默认配置 文档手册 试点观测和主导航收口后的最终验收,全员账户 档案 部门关系已补齐 入职离职资产交接至少各跑通一次 全员连续使用周报与团队状态且主回归 E2E 全绿,cd server && npm test && ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build && npm run test:e2e,manual,DOING,TODO,TODO,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|server/scripts/regression-all.sh|client/tests/e2e|docs/admin-handbook.md|docs/employee-quickstart.md,"[""R17-4"",""R17-5""]",代码/文档/回归基线已完成;仍待真实 4 周试点观测与全员连续使用证据后再关单 diff --git a/server/src/routes/portal.js b/server/src/routes/portal.js index cfed1134..eb2d7e6b 100644 --- a/server/src/routes/portal.js +++ b/server/src/routes/portal.js @@ -146,6 +146,26 @@ function buildQuickLinks({ request, featureEnabled }) { return items.slice(0, 8); } +function buildAdoptionHints({ activeEmployeeCount, login7dCount, weeklySummary, teamPresenceSummary, offboardingCount, assetReturnPendingCount }) { + const hints = []; + if (login7dCount < activeEmployeeCount) { + hints.push(`近 7 天仍有 ${Math.max(0, activeEmployeeCount - login7dCount)} 人未登录系统`); + } + if ((weeklySummary?.missingCount ?? 0) > 0) { + hints.push(`本周仍有 ${weeklySummary.missingCount} 人未提交周报`); + } + if ((teamPresenceSummary?.unspecified ?? 0) > 0) { + hints.push(`今日仍有 ${teamPresenceSummary.unspecified} 人未更新团队状态`); + } + if ((offboardingCount ?? 0) > 0 || (assetReturnPendingCount ?? 0) > 0) { + hints.push(`仍有 ${offboardingCount ?? 0} 条离职流程和 ${assetReturnPendingCount ?? 0} 项资产交接待收口`); + } + if (!hints.length) { + hints.push("近 7 天登录、周报、团队状态与离职交接均满足试点观测口径"); + } + return hints; +} + async function portalRoutes(fastify) { fastify.get("/portal/home", { preHandler: fastify.authenticate }, async (request, reply) => { await ensureFeatureEnabled(fastify, request); @@ -180,6 +200,8 @@ async function portalRoutes(fastify) { onboardingCount, offboardingCount, assetReturnPendingCount, + activeEmployeeCount, + login7dCount, ] = await Promise.all([ fastify.prisma.workflowTask.count({ where: { status: "pending", OR: assigneeFilters } }), fastify.prisma.reimbursementForm.count({ where: { OR: [{ owner_id: request.employee.id }, { submitter_id: request.employee.id }] } }), @@ -225,6 +247,19 @@ async function portalRoutes(fastify) { featureEnabled.employeeLifecycle && isManagerView ? fastify.prisma.assetCard.count({ where: { assignee: { is: { employment_status: "offboarding" } } } }) : Promise.resolve(0), + isManagerView ? fastify.prisma.employee.count({ where: { status: "active" } }) : Promise.resolve(0), + isManagerView + ? fastify.prisma.authSession.findMany({ + where: { + revoked_at: null, + expires_at: { gt: new Date() }, + last_seen_at: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, + employee: { status: "active" }, + }, + select: { employee_id: true }, + distinct: ["employee_id"], + }).then((rows) => rows.length) + : Promise.resolve(0), ]); const todayPresence = featureEnabled.presenceBoard @@ -236,15 +271,14 @@ async function portalRoutes(fastify) { const teamWeeklySummary = featureEnabled.weeklyReport && isManagerView ? await (async () => { - const [employeeCount, submittedCount, needsHelpCount] = await Promise.all([ - fastify.prisma.employee.count({ where: { status: "active" } }), + const [submittedCount, needsHelpCount] = await Promise.all([ fastify.prisma.weeklyReport.count({ where: { week_start: parseDateOnlyString(currentWeekStart), status: "submitted" } }), fastify.prisma.weeklyReport.count({ where: { week_start: parseDateOnlyString(currentWeekStart), needs_help: true } }), ]); return { weekStart: currentWeekStart, submittedCount, - missingCount: Math.max(0, employeeCount - submittedCount), + missingCount: Math.max(0, activeEmployeeCount - submittedCount), needsHelpCount, }; })() @@ -325,6 +359,33 @@ async function portalRoutes(fastify) { cards.push({ key: "risk_flags", title: "待复核风险", value: riskCount, href: "/ap-operations", severity: "high" }); } + const adoptionSummary = isManagerView + ? (() => { + const presenceUpdatedCount = teamPresenceSummary ? teamPresenceSummary.total - teamPresenceSummary.unspecified : 0; + const weeklySubmittedCount = teamWeeklySummary?.submittedCount ?? 0; + return { + activeEmployeeCount, + login7dCount, + weeklySubmittedCount, + weeklyMissingCount: teamWeeklySummary?.missingCount ?? 0, + presenceUpdatedCount, + presenceMissingCount: teamPresenceSummary?.unspecified ?? 0, + announcementCount: announcementRows.length, + login7dRate: activeEmployeeCount > 0 ? Number(((login7dCount / activeEmployeeCount) * 100).toFixed(1)) : 0, + weeklySubmissionRate: activeEmployeeCount > 0 ? Number(((weeklySubmittedCount / activeEmployeeCount) * 100).toFixed(1)) : 0, + presenceCoverageRate: activeEmployeeCount > 0 ? Number(((presenceUpdatedCount / activeEmployeeCount) * 100).toFixed(1)) : 0, + hints: buildAdoptionHints({ + activeEmployeeCount, + login7dCount, + weeklySummary: teamWeeklySummary, + teamPresenceSummary, + offboardingCount, + assetReturnPendingCount, + }), + }; + })() + : null; + return sendData(reply, request, { workspace, greeting: { name: request.employee.name ?? request.employee.id }, @@ -370,6 +431,7 @@ async function portalRoutes(fastify) { href: "/people", } : null, + adoptionSummary, }); }); }