feat: 完成 R17-4 试点观测支撑

这个提交包含在:
2026-03-21 22:04:30 +08:00
父节点 736c803d19
当前提交 5e9ef9cab2
共修改 9 个文件,包含 169 行新增和 12 行删除
+15
查看文件
@@ -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() {
+35
查看文件
@@ -131,6 +131,41 @@ export function PortalHomePage() {
</div>
</div>
{payload.adoptionSummary ? (
<div className="card-std space-y-3 p-6 xl:col-span-2">
<div className="flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">试点观测摘要</h3>
<Link to="/system/workspace" className="text-sm text-primary hover:underline">查看工作区设置</Link>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4 text-sm">
<div className="rounded-lg border border-zinc-200 px-3 py-3">
近 7 天登录:<span className="font-medium text-zinc-900">{payload.adoptionSummary.login7dCount}/{payload.adoptionSummary.activeEmployeeCount}</span>
<div className="mt-1 text-xs text-zinc-500">覆盖率 {payload.adoptionSummary.login7dRate}%</div>
</div>
<div className="rounded-lg border border-zinc-200 px-3 py-3">
本周周报:<span className="font-medium text-zinc-900">已交 {payload.adoptionSummary.weeklySubmittedCount}</span>
<div className="mt-1 text-xs text-zinc-500">提交率 {payload.adoptionSummary.weeklySubmissionRate}%</div>
</div>
<div className="rounded-lg border border-zinc-200 px-3 py-3">
今日状态:<span className="font-medium text-zinc-900">已更新 {payload.adoptionSummary.presenceUpdatedCount}</span>
<div className="mt-1 text-xs text-zinc-500">覆盖率 {payload.adoptionSummary.presenceCoverageRate}%</div>
</div>
<div className="rounded-lg border border-zinc-200 px-3 py-3">
当前公告:<span className="font-medium text-zinc-900">{payload.adoptionSummary.announcementCount}</span>
<div className="mt-1 text-xs text-zinc-500">用于试点期统一通知</div>
</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm">
<div className="font-medium text-zinc-900">本周观测提示</div>
<ul className="mt-2 space-y-1 text-zinc-600 list-disc pl-5">
{payload.adoptionSummary.hints.map((hint) => (
<li key={hint}>{hint}</li>
))}
</ul>
</div>
</div>
) : null}
<div className="card-std space-y-3 p-6">
<h3 className="text-base font-semibold text-zinc-900">工作台摘要</h3>
<div className="space-y-3 text-sm">
+1 -1
查看文件
@@ -65,4 +65,4 @@
- [ ] 至少 1 条公告已发布并在 `Portal` 可见
- [ ] 至少 1 次入职/离职/资产交接已演练
- [ ] 全员已知晓每周周报和今日状态使用要求
- [ ] 已建立连续 4 周观测记录方式
- [ ] 已建立连续 4 周观测记录方式(建议直接使用 `deploy/trialrun/observation-template.md`)
+37
查看文件
@@ -0,0 +1,37 @@
# 小团队试点 4 周观测模板
> 建议由管理员或老板每周五填写一次,用于 `R17-6` 最终验收留痕。
## 基本信息
- 工作区模式:`small_team_internal`
- 观测周期:第 __ 周(起止日期:____ 至 ____)
- 填写人:____
## 量化指标
| 指标 | 本周值 | 目标 | 结论 |
| --- | --- | --- | --- |
| 近 7 天登录人数 / 在职人数 | __ / __ | 全员每周至少登录 1 次 | __ |
| 本周周报提交人数 / 在职人数 | __ / __ | 全员提交 | __ |
| 今日状态更新人数 / 在职人数 | __ / __ | 全员工作日可见 | __ |
| 在岗表是否已停止外部维护 | 是 / 否 | 是 | __ |
| 离职交接表是否已停止外部维护 | 是 / 否 | 是 | __ |
## 本周关键动作
- 公告 / Portal / 周报 / 待办 的使用情况:
- 入职 / 离职 / 资产交接是否发生:
- 仍依赖聊天或表格补流程的场景:
## 本周阻塞
- 阻塞 1:
- 阻塞 2:
- 阻塞 3:
## 下周调整
- 是否需要调整提醒频率:
- 是否需要继续隐藏或降级某模块:
- 是否需要补充培训或说明:
## 结论
- 本周是否达到试点预期:是 / 否
- 若否,主要原因:
- 最终验收建议:继续观测 / 可进入验收
+1
查看文件
@@ -224,4 +224,5 @@ docker logs -f hyb-oa-trialrun-server
- 团队状态更新人数
- 公告阅读反馈
- 入职/离职/资产交接演练情况
- 建议统一使用 `deploy/trialrun/observation-template.md` 填写每周结论与阻塞。
- 若连续两周无人使用某入口,考虑继续降级模块可见性。
+6
查看文件
@@ -74,3 +74,9 @@
- 至少 1 次真实离职或模拟离职跑通
- 全员连续 4 周提交周报
- 管理员不再额外维护“今日在岗表”或“离职交接表”
## 11. 4 周观测操作法
1. 每周一进入 `Portal` 查看“试点观测摘要”,记录近 7 天登录覆盖率、周报提交率、团队状态覆盖率。
2. 每周五在 `deploy/trialrun/observation-template.md` 填写本周结论、关键阻塞、是否需要调整提醒或导航。
3. 若某周有员工未登录、未提周报、未更新状态,优先通过公告或站内通知补提醒,不引入额外表格。
4. 连续 4 周都达到预期后,再将 `R17-6` 关单并形成最终验收结论。
+1
查看文件
@@ -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` 为管理员备用能力。
@@ -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 周试点观测与全员连续使用证据后再关单
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 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
3 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
4 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
5 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
6 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 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"] none Portal 已提供试点观测摘要并补齐 4 周观测模板
7 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
8 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 周试点观测与全员连续使用证据后再关单
+65 -3
查看文件
@@ -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,
});
});
}