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
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readProjectDetail = () => readFileSync(resolve(__dirname, "./ProjectDetail.vue"), "utf8");
describe("ProjectDetail setup draft publish workflow", () => {
it("marks the draft server-synced after a successful server draft save", () => {
const source = readProjectDetail();
const functionIndex = source.indexOf("const persistSetupDraft = async");
const successIndex = source.indexOf("applySetupResponseMeta(data);", functionIndex);
const syncedIndex = source.indexOf("markSetupServerSynced();", successIndex);
expect(functionIndex).toBeGreaterThan(-1);
expect(successIndex).toBeGreaterThan(-1);
expect(syncedIndex).toBeGreaterThan(successIndex);
});
it("reconciles stale dirty flags before opening the publish dialog", () => {
const source = readProjectDetail();
const functionIndex = source.indexOf("const publishConfigNow = async");
const reconcileIndex = source.indexOf("reconcileDraftSyncStateBeforePublish();", functionIndex);
const unsavedGuardIndex = source.indexOf("if (hasSetupDraftUnsavedChanges.value)", functionIndex);
expect(functionIndex).toBeGreaterThan(-1);
expect(reconcileIndex).toBeGreaterThan(functionIndex);
expect(unsavedGuardIndex).toBeGreaterThan(reconcileIndex);
});
it("uses an empty project snapshot as the first publish diff baseline", () => {
const source = readProjectDetail();
const computedIndex = source.indexOf("const projectPublishCompareBase = computed");
const emptyBaselineIndex = source.indexOf("return isFirstPublish.value ? emptyProjectInfoDraft() : null;", computedIndex);
expect(computedIndex).toBeGreaterThan(-1);
expect(emptyBaselineIndex).toBeGreaterThan(computedIndex);
});
it("clears the full setup draft including project info", () => {
const source = readProjectDetail();
const functionIndex = source.indexOf("const handleClearDraft = async");
const confirmTextIndex = source.indexOf("确认清空当前立项配置草稿的所有数据?该操作不会影响已发布版本。", functionIndex);
const applyServerDataIndex = source.indexOf("applySetupDraft(clone(data.data));", functionIndex);
const clearProjectDraftIndex = source.indexOf("clearLocalProjectDraft();", applyServerDataIndex);
expect(functionIndex).toBeGreaterThan(-1);
expect(confirmTextIndex).toBeGreaterThan(functionIndex);
expect(applyServerDataIndex).toBeGreaterThan(confirmTextIndex);
expect(clearProjectDraftIndex).toBeGreaterThan(applyServerDataIndex);
expect(source.indexOf("applySetupDraft(createDefaultSetupDraft(siteOptions.value));", functionIndex)).toBe(-1);
});
it("keeps early termination out of regular visit schedule options", () => {
const source = readProjectDetail();
expect(source).toContain('const specialVisitOptions = ["筛选访视", "基线访视", "终止治疗"];');
expect(source).toContain("提前终止不作为常规计划访视配置");
});
});