feat: refine subject visits and project workflows

Add early termination visit workflow with ordering, non-applicable visit handling, visit window display, and medication adherence support.

Extend monitoring visit issue template fields, site scoping, setup draft project info handling, login security UI, attachment behavior, and related tests/migrations.
This commit is contained in:
Cheng Zhou
2026-05-09 17:10:34 +08:00
parent 74feca4467
commit 917ab7ccf1
41 changed files with 3463 additions and 701 deletions
+22
View File
@@ -32,6 +32,28 @@ const statusLabelMap: Record<string, string> = {
};
const setupFieldLabelMap: Record<keyof SetupConfigDraft, Record<string, string>> = {
projectInfo: {
code: "项目编号",
name: "项目简称",
project_full_name: "项目全称",
sponsor: "申办方",
protocol_no: "方案号",
lead_unit: "组长单位",
principal_investigator: "主要研究者",
main_pm: "主PM",
research_analysis: "研究分期",
research_product: "研究产品",
control_product: "对照产品",
indication: "适应症",
research_population: "研究人群",
research_design: "研究设计",
status: "项目状态",
plan_start_date: "计划开始日期",
plan_end_date: "计划结束日期",
planned_site_count: "计划中心数",
planned_enrollment_count: "计划入组例数",
visit_schedule: "访视计划",
},
projectMilestones: {
id: "ID",
name: "里程碑",
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import type { ProjectPublishSnapshot } from "../types/setupConfig";
import { buildProjectDiffRows } from "./setupPublishWorkflow";
import { serializeDiffValue } from "./setupDiffRows";
const emptyProjectSnapshot = (): ProjectPublishSnapshot => ({
code: "",
name: "",
project_full_name: "",
sponsor: "",
protocol_no: "",
lead_unit: "",
principal_investigator: "",
main_pm: "",
research_analysis: "",
research_product: "",
control_product: "",
indication: "",
research_population: "",
research_design: "",
plan_start_date: "",
plan_end_date: "",
planned_site_count: null,
planned_enrollment_count: null,
status: "",
visit_schedule: [],
});
describe("setup publish workflow project diff rows", () => {
it("shows visit schedule changes under the summary module", () => {
const current = emptyProjectSnapshot();
current.visit_schedule = [
{
visit_code: "V1",
baseline_offset_days: 0,
window_before_days: 0,
window_after_days: 3,
},
];
const rows = buildProjectDiffRows(current, emptyProjectSnapshot(), serializeDiffValue);
expect(rows).toEqual([
{
moduleLabel: "方案摘要",
path: "访视计划",
changeType: "修改",
localValue: "已配置列表(1项)",
serverValue: "已配置列表(0项)",
},
]);
});
it("does not treat equivalent visit schedule arrays as changed", () => {
const current = emptyProjectSnapshot();
const base = emptyProjectSnapshot();
current.visit_schedule = [
{
visit_code: "V1",
baseline_offset_days: 0,
window_before_days: 0,
window_after_days: 3,
},
];
base.visit_schedule = [
{
visit_code: "V1",
baseline_offset_days: 0,
window_before_days: 0,
window_after_days: 3,
},
];
expect(buildProjectDiffRows(current, base, serializeDiffValue)).toEqual([]);
});
});
+30 -2
View File
@@ -122,6 +122,34 @@ const PROJECT_FIELD_LABEL_MAP: Record<keyof ProjectPublishSnapshot, string> = {
visit_schedule: "访视计划",
};
const PROJECT_FIELD_MODULE_LABEL_MAP: Record<keyof ProjectPublishSnapshot, string> = {
code: "项目信息",
name: "项目信息",
project_full_name: "项目信息",
sponsor: "项目信息",
protocol_no: "项目信息",
lead_unit: "项目信息",
principal_investigator: "项目信息",
main_pm: "项目信息",
research_analysis: "研究信息",
research_product: "研究信息",
control_product: "研究信息",
indication: "研究信息",
research_population: "研究信息",
research_design: "研究信息",
plan_start_date: "执行信息",
plan_end_date: "执行信息",
planned_site_count: "执行信息",
planned_enrollment_count: "执行信息",
status: "执行信息",
visit_schedule: "方案摘要",
};
const isProjectFieldValueEqual = (currentValue: unknown, baseValue: unknown): boolean => {
if (currentValue === baseValue) return true;
return JSON.stringify(currentValue ?? null) === JSON.stringify(baseValue ?? null);
};
export const buildProjectDiffRows = (
currentSnapshot: ProjectPublishSnapshot,
baseSnapshot: ProjectPublishSnapshot | null,
@@ -130,9 +158,9 @@ export const buildProjectDiffRows = (
if (!baseSnapshot) return [];
const rows: SetupDiffRow[] = [];
(Object.keys(PROJECT_FIELD_LABEL_MAP) as Array<keyof ProjectPublishSnapshot>).forEach((key) => {
if (currentSnapshot[key] === baseSnapshot[key]) return;
if (isProjectFieldValueEqual(currentSnapshot[key], baseSnapshot[key])) return;
rows.push({
moduleLabel: "项目信息",
moduleLabel: PROJECT_FIELD_MODULE_LABEL_MAP[key],
path: PROJECT_FIELD_LABEL_MAP[key],
changeType: "修改",
localValue: serializeValue(currentSnapshot[key]),