export function createInitialState({ activePatientId = "patient-zhang-san", demoNow = "2026-07-03T09:00:00+08:00", } = {}) { return { activeTab: "home", activePatientId, demoNow, appliedProjectionFixtureIds: [], activeAgentTaskProjection: null, patientWorkItems: [], journeyProjections: [], reportSourceProjections: {}, reportFeedback: [], reportFilters: { family: "ALL", status: "ALL" }, resultSnapshotProjections: {}, commandProjections: {}, messages: [], consents: {}, consentScopeBindings: {}, overlays: { commandId: null, consentId: null, consentScope: null, reportId: null, reportView: null, reportUnavailableReason: null, reportExpanded: false, reportFeedbackOpen: false, reportFeedbackSubmitted: false, reportServiceNotice: null, appointmentId: null, journeyView: null, scenarioPanelOpen: false, }, }; } const deepFreeze = (value) => { if (value && typeof value === "object" && !Object.isFrozen(value)) { Object.freeze(value); Object.values(value).forEach(deepFreeze); } return value; }; const isRecord = (value) => ( value !== null && typeof value === "object" && !Array.isArray(value) ); const reportIdPattern = /^[^\s\u0000-\u001F\u007F]{1,128}$/; const reportHashPattern = /^[^\s\u0000-\u001F\u007F]{1,256}$/; const reportSourceStatuses = new Set(["FINAL", "PRELIMINARY"]); const reportFamilies = new Set(["LAB", "EXAM"]); const reportTypes = new Set(["BLOOD_ROUTINE", "IMAGING"]); const reportVerificationStatuses = new Set(["VERIFIED", "NEED_REVIEW"]); export const bloodRoutineReportFixture = deepFreeze({ reportId: "report-blood-20260702", family: "LAB", reportType: "BLOOD_ROUTINE", title: "血常规", hospitalName: "空海医院", departmentName: "检验科", issuedAt: "2026-07-03 09:30", sourceStatus: "FINAL", interpretationStatus: "NOT_REQUESTED", verificationStatus: "VERIFIED", sourceVersion: 1, sourceHash: "sha256:blood-v1", readStatus: "UNREAD", metrics: [ { code: "WBC", name: "白细胞计数", value: 11.2, unit: "10^9/L", referenceRange: "3.5–9.5", sourceFlag: "HIGH", }, ], }); const registrationDemoText = "我想挂明天神经内科李明主任的号"; const registrationResultRef = "result-slot-001"; const registrationResultVersion = 1; const registrationItems = deepFreeze([ { candidateId: "candidate-slot-0930", visitDate: "2026-07-03", timePeriod: "上午", visitTime: "09:30", departmentName: "神经内科", doctorName: "李明", doctorTitle: "主任医师", remainingSlots: 3, fee: "30元", }, { candidateId: "candidate-slot-1030", visitDate: "2026-07-03", timePeriod: "上午", visitTime: "10:30", departmentName: "神经内科", doctorName: "李明", doctorTitle: "主任医师", remainingSlots: 1, fee: "30元", }, { candidateId: "candidate-slot-1430", visitDate: "2026-07-03", timePeriod: "下午", visitTime: "14:30", departmentName: "神经内科", doctorName: "李明", doctorTitle: "主任医师", remainingSlots: 5, fee: "30元", }, ]); const registrationResultText = [ `已为你找到 ${registrationItems[0].visitDate} ${registrationItems[0].departmentName}${registrationItems[0].doctorName}${registrationItems[0].doctorTitle}的 ${registrationItems.length} 个号源:`, ...registrationItems.map((item) => ( `${item.visitDate} ${item.timePeriod} ${item.visitTime},` + `${item.departmentName},${item.doctorName}${item.doctorTitle},` + `余号 ${item.remainingSlots},挂号费 ${item.fee};` )), "请选择一个号源。", ].join(""); const nextMessageId = (messages, role) => { const usedIds = new Set( messages .filter((message) => message && typeof message === "object") .map(({ id }) => id), ); let sequence = messages.length + 1; while (usedIds.has(`message-${role}-${sequence}`)) { sequence += 1; } return `message-${role}-${sequence}`; }; const simulateRegistrationQueryBackendFixture = (interaction) => { if (interaction?.text !== registrationDemoText) { return { type: "NO_RESULT", text: interaction?.text ? "暂时没有找到匹配结果。你可以换个说法,试试智能导诊,或转人工服务。" : "请输入想办理的就医服务。你也可以试试智能导诊或人工服务。", }; } return { type: "REGISTRATION_CANDIDATES", task: { taskId: "agent-task-registration-001", type: "REGISTRATION", stage: "CANDIDATE_SELECTION", constraints: { departmentName: "神经内科", doctorName: "李明", doctorTitle: "主任医师", visitDate: "2026-07-03", }, missingFields: [], activeResultRef: registrationResultRef, }, result: { resultRef: registrationResultRef, resultVersion: registrationResultVersion, expiresAt: "2026-07-03T10:00:00+08:00", text: registrationResultText, items: registrationItems.map((item) => ({ ...item })), }, }; }; const applyRegistrationQueryResponseFixture = ( state, interaction, fixture, ) => { const messages = Array.isArray(state.messages) ? state.messages : []; const userMessage = interaction.text ? [{ id: nextMessageId(messages, "user"), role: "user", text: interaction.text, }] : []; const assistantMessage = { id: nextMessageId([...messages, ...userMessage], "assistant"), role: "assistant", text: fixture.text ?? fixture.result?.text, ...(fixture.result?.resultRef ? { resultRef: fixture.result.resultRef } : {}), }; if (fixture.type !== "REGISTRATION_CANDIDATES") { return { ...state, activeTab: "assistant", messages: [...messages, ...userMessage, assistantMessage], }; } return { ...state, activeTab: "assistant", activeAgentTaskProjection: fixture.task, resultSnapshotProjections: { ...(state.resultSnapshotProjections ?? {}), [fixture.result.resultRef]: fixture.result, }, messages: [...messages, ...userMessage, assistantMessage], }; }; export function submitInteraction(state, text) { if (!state || typeof state !== "object" || Array.isArray(state)) { return state; } const normalizedText = typeof text === "string" ? text.trim() : ""; const interaction = { interactionType: "USER_MESSAGE", text: normalizedText, }; const responseFixture = simulateRegistrationQueryBackendFixture( interaction, ); return applyRegistrationQueryResponseFixture( state, interaction, responseFixture, ); } export function buildCandidateSelectionInteraction( state, candidateSelection, now, ) { const task = state?.activeAgentTaskProjection; const resultRef = task?.activeResultRef; const result = typeof resultRef === "string" ? state?.resultSnapshotProjections?.[resultRef] : null; const normalizedSelection = typeof candidateSelection === "string" ? candidateSelection.trim() : candidateSelection; const candidateId = normalizedSelection === "选第一个" ? result?.items?.[0]?.candidateId : normalizedSelection; const expiresAt = Date.parse(result?.expiresAt); const evaluatedAt = Date.parse(now ?? state?.demoNow); const isExpired = !Number.isFinite(expiresAt) || !Number.isFinite(evaluatedAt) || expiresAt <= evaluatedAt; const hasCandidate = Array.isArray(result?.items) && result.items.some( (item) => item?.candidateId === candidateId, ); if ( task?.type !== "REGISTRATION" || task.stage !== "CANDIDATE_SELECTION" || result?.resultRef !== resultRef || !Number.isInteger(result?.resultVersion) || result.resultVersion <= 0 || isExpired || !hasCandidate ) { return null; } return { interactionType: "SELECT_CANDIDATE", resultRef, resultVersion: result.resultVersion, candidateId, }; } export function applyCandidateSelectionResponseFixture(state, fixture) { if (!state || typeof state !== "object" || !fixture?.id) { return state; } const appliedIds = Array.isArray(state.appliedProjectionFixtureIds) ? state.appliedProjectionFixtureIds : []; if (appliedIds.includes(fixture.id)) { return state; } const task = state?.activeAgentTaskProjection; const result = task?.activeResultRef ? state?.resultSnapshotProjections?.[task.activeResultRef] : null; const isActiveSelection = task?.type === "REGISTRATION" && task.stage === "CANDIDATE_SELECTION" && result?.resultRef === task.activeResultRef; if (!isActiveSelection) { return state; } const canonicalErrors = new Set([ "RESULT_VERSION_CONFLICT", "RESULT_EXPIRED", "SLOT_UNAVAILABLE", ]); const validation = fixture.validation ?? {}; const derivedError = validation.versionMatched === false ? "RESULT_VERSION_CONFLICT" : validation.notExpired === false ? "RESULT_EXPIRED" : validation.realtimeAvailable === false || validation.l1LockSucceeded === false ? "SLOT_UNAVAILABLE" : null; const baseState = { ...state, appliedProjectionFixtureIds: [...appliedIds, fixture.id], }; const evaluatedAt = Date.parse(state.demoNow); const snapshotExpiresAt = Date.parse(result.expiresAt); const snapshotNotExpired = Number.isFinite(evaluatedAt) && Number.isFinite(snapshotExpiresAt) && snapshotExpiresAt > evaluatedAt; const lockExpiresAt = Date.parse(fixture.lock?.expiresAt); const lockIdValid = typeof fixture.lock?.lockId === "string" && fixture.lock.lockId.trim().length > 0; const lockExpiryValid = Number.isFinite(evaluatedAt) && Number.isFinite(lockExpiresAt) && lockExpiresAt > evaluatedAt; const candidateExists = Array.isArray(result?.items) && result.items.some( (item) => item?.candidateId === fixture.candidateId, ); const validationPassed = [ "tenantMatched", "patientMatched", "sessionMatched", "versionMatched", "notExpired", "realtimeAvailable", "l1LockSucceeded", ].every((key) => validation[key] === true); const successIsValid = fixture.outcome === "SUCCESS" && fixture.resultRef === result.resultRef && fixture.resultVersion === result.resultVersion && candidateExists && validationPassed && snapshotNotExpired && lockIdValid && lockExpiryValid; if (!successIsValid) { const errorCode = canonicalErrors.has(fixture.errorCode) ? fixture.errorCode : derivedError ?? (!snapshotNotExpired ? "RESULT_EXPIRED" : !lockIdValid && fixture.outcome === "SUCCESS" ? "SLOT_UNAVAILABLE" : !lockExpiryValid && fixture.outcome === "SUCCESS" ? "RESULT_EXPIRED" : "UNKNOWN_SELECTION_ERROR"); const unavailableAdvice = errorCode === "SLOT_UNAVAILABLE" ? " 当前号源已无号,可查看其他时段或医生。" : ""; const messages = Array.isArray(state.messages) ? state.messages : []; return { ...baseState, activeAgentTaskProjection: { ...task, stage: "REQUERY_REQUIRED", activeResultRef: null, }, messages: [ ...messages, { id: nextMessageId(messages, "assistant"), role: "assistant", text: `结果已更新,请重新查询。${unavailableAdvice}`, }, ], }; } const messages = Array.isArray(state.messages) ? state.messages : []; return { ...baseState, activeAgentTaskProjection: { ...task, stage: "AWAITING_REGISTRATION_CONFIRMATION", selectedCandidateId: fixture.candidateId, lock: { ...fixture.lock }, }, messages: [ ...messages, { id: nextMessageId(messages, "assistant"), role: "assistant", text: "锁号成功,请在锁定时间内完成二次确认后挂号。", }, ], }; } const commandStatuses = new Set([ "PREPARED", "AWAITING_CONFIRMATION", "EXECUTING", "SUCCEEDED", "FAILED", "EXPIRED", "UNKNOWN", "REJECTED", ]); const commandTransitions = { L1: { PREPARED: new Set(["EXECUTING"]), EXECUTING: new Set(["SUCCEEDED", "FAILED", "UNKNOWN"]), UNKNOWN: new Set(["SUCCEEDED", "FAILED"]), }, L2: { PREPARED: new Set(["AWAITING_CONFIRMATION"]), AWAITING_CONFIRMATION: new Set([ "EXECUTING", "REJECTED", "EXPIRED", ]), EXECUTING: new Set(["SUCCEEDED", "FAILED", "UNKNOWN"]), UNKNOWN: new Set(["SUCCEEDED", "FAILED"]), }, }; const initialCommandStatuses = { L1: new Set(["PREPARED", "UNKNOWN"]), L2: new Set(["PREPARED", "AWAITING_CONFIRMATION", "UNKNOWN"]), }; const commandFixtureError = (message) => { throw new Error(`Command projection fixture invalid: ${message}`); }; const legacyAppointmentResult = { type: "APPOINTMENT_CREATED", eventId: "appointment-created-001", appointmentId: "appointment-001", patientId: "patient-zhang-san", summary: { visitDate: "2026-07-03", visitTime: "09:30", departmentName: "神经内科", doctorName: "李明", doctorTitle: "主任医师", fee: "30元", }, }; const applyAppointmentCreatedProjection = (state, fixture) => { const result = fixture.result ?? ( fixture.resultFixtureId === "appointment-created-001" ? legacyAppointmentResult : null ); if (!result || result.type !== "APPOINTMENT_CREATED") { return state; } const eventId = result.eventId ?? fixture.resultFixtureId; if ( typeof eventId !== "string" || !eventId || typeof result.appointmentId !== "string" || !result.appointmentId ) { commandFixtureError("appointment result identifiers are required"); } if (state.appliedProjectionFixtureIds.includes(eventId)) { return state; } const summary = result.summary ?? {}; const doctor = `${summary.doctorName ?? ""}${summary.doctorTitle ?? ""}`; return { ...state, appliedProjectionFixtureIds: [ ...state.appliedProjectionFixtureIds, eventId, ], patientWorkItems: [ ...state.patientWorkItems, { id: `work-item-${result.appointmentId}`, appointmentId: result.appointmentId, patientId: result.patientId, type: "APPOINTMENT", status: "UPCOMING", stage: "WAITING_ARRIVAL", priority: 80, title: `${summary.departmentName ?? "门诊"}预约成功`, description: `${summary.visitDate ?? ""} ${summary.visitTime ?? ""} · ${doctor} · ${summary.fee ?? ""}`, fee: summary.fee, primaryAction: null, secondaryAction: null, expiresAt: null, }, ], journeyProjections: [ ...state.journeyProjections, { id: `journey-${result.appointmentId}`, stage: "WAITING_ARRIVAL", title: "待到院", appointmentId: result.appointmentId, patientId: result.patientId, }, ], }; }; export function applyCommandProjectionFixture(state, fixture) { if (!state || typeof state !== "object" || Array.isArray(state)) { commandFixtureError("state is required"); } if (!fixture || typeof fixture !== "object" || Array.isArray(fixture)) { commandFixtureError("fixture is required"); } const { id, commandId, status } = fixture; if (typeof id !== "string" || !id.trim()) { commandFixtureError("id is required"); } if (typeof commandId !== "string" || !commandId.trim()) { commandFixtureError("commandId is required"); } if (status === "COMPLETED") { commandFixtureError("COMPLETED is not a Command status"); } if (!commandStatuses.has(status)) { commandFixtureError(`unknown status ${String(status)}`); } const appliedIds = Array.isArray(state.appliedProjectionFixtureIds) ? state.appliedProjectionFixtureIds : []; if (appliedIds.includes(id)) { return state; } const current = state.commandProjections?.[commandId]; const riskLevel = fixture.riskLevel ?? current?.riskLevel ?? "L2"; if (riskLevel !== "L1" && riskLevel !== "L2") { commandFixtureError(`unknown riskLevel ${String(riskLevel)}`); } if (current && current.riskLevel !== riskLevel) { commandFixtureError("riskLevel cannot change"); } const version = fixture.version ?? current?.version; if (version !== undefined && (!Number.isInteger(version) || version <= 0)) { commandFixtureError("version must be a positive integer"); } if (!current && !initialCommandStatuses[riskLevel].has(status)) { commandFixtureError( `illegal initial ${riskLevel} status ${status}`, ); } if (current) { const isFinalBusinessSnapshot = ( riskLevel === "L2" && current.status === "AWAITING_CONFIRMATION" && status === "SUCCEEDED" && fixture.resultFixtureId === "appointment-created-001" && fixture.result === undefined && fixture.version === undefined ); // The exact legacy appointment fixture may close a missed EXECUTING gap. // Every current incremental or structured-result fixture stays strict. const transitionAllowed = commandTransitions[riskLevel][current.status] ?.has(status); if ( !isFinalBusinessSnapshot && ( !Number.isInteger(fixture.version) || !Number.isInteger(current.version) || fixture.version <= current.version ) ) { commandFixtureError("version must explicitly increase"); } if (!transitionAllowed && !isFinalBusinessSnapshot) { commandFixtureError( `illegal ${riskLevel} transition ${current.status} -> ${status}`, ); } } const projection = { ...current, commandId, status, riskLevel, ...(version === undefined ? {} : { version }), ...(fixture.details === undefined ? {} : { details: structuredClone(fixture.details) }), ...(fixture.resultFixtureId === undefined ? {} : { resultFixtureId: fixture.resultFixtureId }), ...(fixture.result === undefined ? {} : { result: structuredClone(fixture.result) }), }; let nextState = { ...state, appliedProjectionFixtureIds: [...appliedIds, id], commandProjections: { ...(state.commandProjections ?? {}), [commandId]: projection, }, overlays: { ...state.overlays, commandId, }, }; if ( status === "SUCCEEDED" && ( fixture.result?.type === "APPOINTMENT_CREATED" || fixture.resultFixtureId === "appointment-created-001" ) ) { nextState = applyAppointmentCreatedProjection(nextState, fixture); } return nextState; } export function buildCommandConfirmationRequest( state, commandId, { channel, idempotencyKey } = {}, ) { const command = state?.commandProjections?.[commandId]; if (!command) { throw new Error("Command 不存在,无法确认"); } if (command.status === "UNKNOWN") { throw new Error("结果确认中,请勿重复操作"); } if (command.status === "EXPIRED") { throw new Error("Command 已过期,请重新查询号源"); } if (command.status !== "AWAITING_CONFIRMATION") { throw new Error(`当前状态 ${command.status} 不允许重复确认`); } if (!Number.isInteger(command.version) || command.version <= 0) { throw new Error("Command version 无效"); } if (channel !== "CARD") { throw new Error("确认渠道不受支持"); } if (typeof idempotencyKey !== "string" || !idempotencyKey.trim()) { throw new Error("幂等键不能为空"); } return { path: `/api/v2/commands/${encodeURIComponent(commandId)}/confirm`, method: "POST", commandId, body: { commandVersion: command.version, confirmation: { channel, explicit: true, }, }, headers: { "X-Emoon-Idempotency-Key": idempotencyKey, }, }; } export function buildCommandRejectionRequest( state, commandId, { reason, idempotencyKey } = {}, ) { const command = state?.commandProjections?.[commandId]; if (!command) { throw new Error("Command 不存在,无法拒绝"); } if (command.status !== "AWAITING_CONFIRMATION") { throw new Error(`当前状态 ${command.status} 不允许拒绝`); } if (!Number.isInteger(command.version) || command.version <= 0) { throw new Error("Command version 无效"); } if (typeof idempotencyKey !== "string" || !idempotencyKey.trim()) { throw new Error("幂等键不能为空"); } return { path: `/api/v2/commands/${encodeURIComponent(commandId)}/reject`, method: "POST", commandId, body: { commandVersion: command.version, ...(typeof reason === "string" && reason.trim() ? { reason: reason.trim() } : {}), }, headers: { "X-Emoon-Idempotency-Key": idempotencyKey, }, }; } const statusRank = { ACTION_REQUIRED: 0, IN_PROGRESS: 1, UPCOMING: 2, COMPLETED: 3, }; export const reportInterpretationProjectionFixture = deepFreeze({ resultRef: "result-report-interpretation-001", resultVersion: 1, interpretation: { headline: "3 项指标轻度异常", summary: "这份报告有 3 项指标超出参考范围,变化幅度较小,需要结合近期症状和病史由医生综合判断。", safety: "本解释仅用于帮助理解报告指标,不用于判断疾病或替代医生意见。如有明显不适,请及时联系医务人员。", highlights: [ { name: "白细胞计数", value: "11.2", flag: "偏高" }, { name: "中性粒细胞比例", value: "78%", flag: "偏高" }, { name: "淋巴细胞比例", value: "18%", flag: "偏低" }, ], explanations: [ { indicator: "白细胞 11.2 ×10⁹/L", explanation: "轻度高于报告所示参考范围,单项变化不能独立说明具体原因。", commonFactors: "近期感染、睡眠不足、运动或情绪紧张等。", }, { indicator: "中性粒细胞比例 78%", explanation: "比例轻度升高,需结合白细胞总数和当前症状判断。", commonFactors: "细菌感染、急性炎症或生理应激等。", }, { indicator: "淋巴细胞比例 18%", explanation: "比例轻度偏低,可能与中性粒细胞比例升高同时出现。", commonFactors: "感染恢复期、压力或个体短期波动等。", }, ], canDo: [ "查看并保存医院原始报告。", "结合近期症状和既往情况,向开单医生咨询。", "如有明显不适,及时联系医务人员。", ], evidence: [ "医院原始报告中的指标结果、单位、参考范围和异常标记。", "医院已审核的终版报告。", ], }, }); const reportConsentId = (reportId) => `consent-ai-${reportId}`; const encodeReportId = (reportId) => String(reportId).replace( /[^A-Za-z0-9._-]/g, (character) => ( `~${character.charCodeAt(0).toString(16).padStart(4, "0")}` ), ); const reportResultSnapshotId = (reportId) => ( `result-report-interpretation-${encodeReportId(reportId)}` ); const reportFeedbackCategories = new Set([ "HELPFUL", "METRIC_INACCURATE", "HARD_TO_UNDERSTAND", "DIFFERS_FROM_DOCTOR", "OTHER", ]); const readReportIssuedFixture = (fixture) => { if ( fixture.payload !== undefined && !isRecord(fixture.payload) ) { return null; } const payload = fixture.payload ?? {}; const isExamination = payload.family === "EXAM"; const report = { ...bloodRoutineReportFixture, ...payload, interpretationStatus: "NOT_REQUESTED", metrics: Array.isArray(payload.metrics) ? payload.metrics : isExamination ? [] : bloodRoutineReportFixture.metrics, }; if ( typeof report.reportId !== "string" || !reportIdPattern.test(report.reportId) || !Number.isInteger(report.sourceVersion) || report.sourceVersion <= 0 || typeof report.sourceHash !== "string" || !reportHashPattern.test(report.sourceHash) || !reportSourceStatuses.has(report.sourceStatus) || !reportFamilies.has(report.family) || !reportTypes.has(report.reportType) || !reportVerificationStatuses.has(report.verificationStatus) || (report.family === "LAB" && report.reportType !== "BLOOD_ROUTINE") || (report.family === "EXAM" && report.reportType !== "IMAGING") ) { return null; } return deepFreeze(structuredClone(report)); }; export const hasValidReportConsent = (state, reportId) => { if ( typeof reportId !== "string" || reportId.length === 0 || !isRecord(state?.consents) || !isRecord(state?.consentScopeBindings) ) { return false; } const consentId = reportConsentId(reportId); const consent = Object.hasOwn(state.consents, consentId) ? state.consents[consentId] : null; const binding = Object.hasOwn(state.consentScopeBindings, consentId) ? state.consentScopeBindings[consentId] : null; const expiresAt = Date.parse(consent?.expiresAt); const evaluatedAt = Date.parse(state?.demoNow); return consent?.id === consentId && consent.status === "GRANTED" && Number.isFinite(expiresAt) && Number.isFinite(evaluatedAt) && expiresAt > evaluatedAt && binding?.patientId === state?.activePatientId && binding?.reportId === reportId; }; export const selectValidReportResult = (state, reportId) => { if (typeof reportId !== "string" || reportId.length === 0) { return null; } const resultRef = reportResultSnapshotId(reportId); const reportSources = state?.reportSourceProjections; const results = state?.resultSnapshotProjections; const report = ( isRecord(reportSources) && Object.hasOwn(reportSources, reportId) ) ? reportSources[reportId] : null; const result = ( isRecord(results) && Object.hasOwn(results, resultRef) ) ? results[resultRef] : null; const hasScopedTask = Array.isArray(state?.patientWorkItems) && state.patientWorkItems.some((item) => ( item?.type === "REPORT_REVIEW" && item.reportId === reportId )); return isRecord(report) && report.reportId === reportId && report.interpretationStatus === "READY" && hasScopedTask && hasValidReportConsent(state, reportId) && evaluateReportInterpretationGate(state, reportId).allowed && isRecord(result) && result.resultRef === resultRef && result.reportId === reportId && result.sourceVersion === report.sourceVersion && result.sourceHash === report.sourceHash && Number.isInteger(result.resultVersion) && result.resultVersion > 0 && result.valid === true && isRecord(result.interpretation) ? result : null; }; export function selectHomeTasks(state) { const patientWorkItems = Array.isArray(state?.patientWorkItems) ? state.patientWorkItems.filter( (item) => item !== null && typeof item === "object" && !Array.isArray(item), ) : []; const priorityOf = ({ priority }) => ( Number.isFinite(priority) ? priority : 0 ); const rankOf = ({ status }) => ( typeof status === "string" && Object.hasOwn(statusRank, status) ? statusRank[status] : 4 ); return patientWorkItems.sort((left, right) => { const statusDelta = rankOf(left) - rankOf(right); return statusDelta || priorityOf(right) - priorityOf(left); }); } export function selectReports(state, filters = {}) { const family = filters?.family ?? "ALL"; const status = filters?.status ?? "ALL"; const projections = state?.reportSourceProjections; if (!projections || typeof projections !== "object" || Array.isArray(projections)) { return []; } return Object.values(projections) .filter((report) => report && typeof report === "object" && !Array.isArray(report)) .filter((report) => family === "ALL" || report.family === family) .filter((report) => status === "ALL" || report.readStatus === status) .sort((left, right) => { const issuedAtDelta = String(right.issuedAt ?? "") .localeCompare(String(left.issuedAt ?? "")); return issuedAtDelta || String(left.reportId ?? "") .localeCompare(String(right.reportId ?? "")); }); } const upsertProjection = (items, nextItem) => { const source = Array.isArray(items) ? items : []; const index = source.findIndex((item) => item?.id === nextItem.id); if (index < 0) { return [...source, nextItem]; } return source.map((item, itemIndex) => ( itemIndex === index ? { ...item, ...nextItem } : item )); }; const journeyTitleByStage = { WAITING_ARRIVAL: "待到院", CHECKIN_AVAILABLE: "可以签到", QUEUE_WAITING: "候诊中", VISIT_IN_PROGRESS: "就诊中", POST_VISIT: "诊后服务", }; const updateJourneyProjection = ( journeys, appointmentId, stage, details = {}, ) => upsertProjection(journeys, { id: `journey-${appointmentId}`, appointmentId, stage, title: journeyTitleByStage[stage], ...details, }); const appendJourneyMessage = (messages, fixture, text) => { if (!text) { return Array.isArray(messages) ? messages : []; } return [ ...(Array.isArray(messages) ? messages : []), { id: `message-${fixture.id}`, type: "JOURNEY", unread: true, title: text, createdAt: fixture.occurredAt, }, ]; }; const readReportInvalidationFixture = (fixture) => { const payload = isRecord(fixture?.payload) ? fixture.payload : null; const reportId = payload?.reportId; const sourceVersion = payload?.sourceVersion; const sourceHash = payload?.sourceHash; if ( typeof reportId !== "string" || reportId.trim().length === 0 || !/^[A-Za-z0-9._-]+$/.test(reportId) || !Number.isInteger(sourceVersion) || sourceVersion <= 0 || typeof sourceHash !== "string" || sourceHash.trim().length === 0 || !/^[A-Za-z0-9._:-]+$/.test(sourceHash) ) { return null; } return { reportId, sourceVersion, sourceHash: sourceHash.trim(), }; }; const applyReportInvalidationProjection = (state, nextState, fixture) => { const { reportId, sourceVersion, sourceHash } = readReportInvalidationFixture(fixture); const report = typeof reportId === "string" && isRecord(state.reportSourceProjections) && Object.hasOwn(state.reportSourceProjections, reportId) ? state.reportSourceProjections[reportId] : null; const latestKnownVersion = Math.max( Number.isInteger(report?.sourceVersion) ? report.sourceVersion : 0, Number.isInteger(report?.invalidationVersion) ? report.invalidationVersion : 0, ); if ( !isRecord(report) || report.reportId !== reportId || sourceVersion <= latestKnownVersion ) { return nextState; } const withdrawn = fixture.type === "REPORT_WITHDRAWN"; const resultSnapshotProjections = Object.fromEntries( Object.entries(state.resultSnapshotProjections ?? {}).map( ([resultRef, result]) => ( isRecord(result) && result.reportId === reportId ? [resultRef, { ...result, valid: false, invalidatedBy: fixture.id, invalidatedReason: withdrawn ? "REPORT_WITHDRAWN" : "REPORT_CORRECTED", invalidatedSourceVersion: sourceVersion, }] : [resultRef, result] ), ), ); const reportSourceProjections = { ...state.reportSourceProjections, [reportId]: { ...report, interpretationStatus: "INVALIDATED", invalidationVersion: sourceVersion, invalidationHash: sourceHash, invalidatedAt: fixture.occurredAt, invalidationReason: withdrawn ? "REPORT_WITHDRAWN" : "REPORT_CORRECTED", }, }; const patientWorkItems = Array.isArray(state.patientWorkItems) ? state.patientWorkItems.map((item) => ( item?.type === "REPORT_REVIEW" && item.reportId === reportId ? { ...item, status: "ACTION_REQUIRED", stage: "REPORT_INVALIDATED", title: withdrawn ? "报告已撤回" : "报告已更正", description: withdrawn ? "医院已撤回当前报告,请以医院后续通知为准" : "医院已更新报告,原辅助解释已失效", primaryAction: "VIEW_REPORT", secondaryAction: null, } : item )) : state.patientWorkItems; const isActiveReport = state.overlays?.reportId === reportId; return { ...nextState, reportSourceProjections, patientWorkItems, resultSnapshotProjections, overlays: isActiveReport ? { ...state.overlays, consentId: null, consentScope: null, reportView: "REPORT_UNAVAILABLE", reportUnavailableReason: "REPORT_INVALIDATED", reportFeedbackOpen: false, reportFeedbackSubmitted: false, reportServiceNotice: null, } : state.overlays, }; }; export function applyServerProjectionFixture(state, fixture) { if ( !isRecord(state) || !Array.isArray(state.appliedProjectionFixtureIds) || !isRecord(fixture) || typeof fixture.id !== "string" || fixture.id.trim().length === 0 || typeof fixture.type !== "string" || fixture.type.trim().length === 0 ) { return state; } if (state.appliedProjectionFixtureIds.includes(fixture.id)) { return state; } const isReportInvalidation = fixture.type === "REPORT_CORRECTED" || fixture.type === "REPORT_WITHDRAWN"; if (isReportInvalidation && !readReportInvalidationFixture(fixture)) { return state; } const issuedReport = fixture.type === "REPORT_ISSUED" ? readReportIssuedFixture(fixture) : null; if (fixture.type === "REPORT_ISSUED" && !issuedReport) { return state; } const nextState = { ...state, appliedProjectionFixtureIds: [ ...state.appliedProjectionFixtureIds, fixture.id, ], }; if (fixture.type === "REPORT_ISSUED") { const report = issuedReport; const isExamination = report.family === "EXAM"; const reportId = report.reportId; const currentReport = state.reportSourceProjections?.[reportId]; const currentVersion = currentReport?.sourceVersion; const invalidationVersion = currentReport?.invalidationVersion; const conflictsWithInvalidation = Number.isInteger(invalidationVersion) && ( report.sourceVersion < invalidationVersion || ( report.sourceVersion === invalidationVersion && report.sourceHash !== currentReport.invalidationHash ) ); if ( currentReport && ( !Number.isInteger(report.sourceVersion) || !Number.isInteger(currentVersion) || report.sourceVersion <= currentVersion || conflictsWithInvalidation ) ) { return nextState; } const resultRef = reportResultSnapshotId(reportId); const currentResult = isRecord(state.resultSnapshotProjections) && Object.hasOwn(state.resultSnapshotProjections, resultRef) ? state.resultSnapshotProjections[resultRef] : null; const resultSnapshotProjections = currentReport && isRecord(currentResult) ? { ...state.resultSnapshotProjections, [resultRef]: { ...currentResult, valid: false, invalidatedReason: "SOURCE_UPDATED", invalidatedAt: fixture.occurredAt ?? state.demoNow, }, } : state.resultSnapshotProjections; const consentId = reportConsentId(reportId); const currentConsent = isRecord(state.consents) && Object.hasOwn(state.consents, consentId) ? state.consents[consentId] : null; const consents = currentReport && isRecord(currentConsent) ? { ...state.consents, [consentId]: { ...currentConsent, status: "REVOKED", revokedAt: fixture.occurredAt ?? state.demoNow, }, } : state.consents; const consentScopeBindings = isRecord(state.consentScopeBindings) ? Object.fromEntries( Object.entries(state.consentScopeBindings).filter( ([bindingId]) => !currentReport || bindingId !== consentId, ), ) : state.consentScopeBindings; return { ...nextState, reportSourceProjections: { ...(state.reportSourceProjections ?? {}), [reportId]: report, }, resultSnapshotProjections, consents, consentScopeBindings, overlays: currentReport && state.overlays?.reportId === reportId ? { ...state.overlays, consentId: null, consentScope: null, reportView: "REPORT_DETAIL", reportUnavailableReason: null, reportFeedbackOpen: false, reportFeedbackSubmitted: false, reportServiceNotice: null, } : state.overlays, patientWorkItems: upsertProjection( state.patientWorkItems, { id: `work-item-${reportId}`, reportId, type: "REPORT_REVIEW", status: "ACTION_REQUIRED", stage: "REPORT_AVAILABLE", priority: 90, title: `${typeof report.title === "string" && report.title.trim() ? report.title.trim() : isExamination ? "检查" : "检验"}报告已出`, description: isExamination ? "医院检查报告已审核,可查看报告详情" : "18项指标中有3项超出参考范围", primaryAction: isExamination ? "VIEW_REPORT" : "INTERPRET_REPORT", secondaryAction: isExamination ? null : "VIEW_REPORT", expiresAt: null, }, ), messages: upsertProjection( state.messages, { id: `message-${reportId}`, type: "REPORT", reportId, unread: true, title: `${typeof report.title === "string" && report.title.trim() ? report.title.trim() : isExamination ? "检查" : "检验"}报告已出`, createdAt: fixture.occurredAt, }, ), }; } if (isReportInvalidation) { return applyReportInvalidationProjection(state, nextState, fixture); } const payload = fixture.payload ?? {}; const appointmentId = payload.appointmentId ?? "appointment-001"; let patientWorkItems = state.patientWorkItems; let journeyProjections = state.journeyProjections; let messages = state.messages; switch (fixture.type) { case "APPOINTMENT_CREATED": { patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-${appointmentId}`, appointmentId, type: "APPOINTMENT", status: "UPCOMING", stage: "WAITING_ARRIVAL", priority: 80, title: `${payload.departmentName ?? "门诊"}预约成功`, description: `${payload.visitTime ?? "09:30"} · ${payload.doctorName ?? "接诊医生"}`, primaryAction: null, secondaryAction: null, expiresAt: null, }); journeyProjections = updateJourneyProjection( journeyProjections, appointmentId, "WAITING_ARRIVAL", { visitTime: payload.visitTime ?? "09:30", departmentName: payload.departmentName ?? "神经内科", doctorName: payload.doctorName ?? "李明", }, ); messages = appendJourneyMessage(messages, fixture, "预约成功,请按时到院"); break; } case "CHECKIN_AVAILABLE": { patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-checkin-${appointmentId}`, appointmentId, type: "CHECK_IN", status: "ACTION_REQUIRED", stage: "CONFIRMATION_REQUIRED", priority: 95, title: "神经内科可以签到", description: `已到达${payload.location ?? "门诊诊区"},请确认后签到`, location: payload.location, primaryAction: "CONFIRM_CHECKIN", secondaryAction: "VIEW_ROUTE", expiresAt: null, }); journeyProjections = updateJourneyProjection( journeyProjections, appointmentId, "CHECKIN_AVAILABLE", { location: payload.location, arrivedAt: fixture.occurredAt }, ); messages = appendJourneyMessage(messages, fixture, "已到达诊区,可以签到"); break; } case "CHECKIN_COMPLETED": { patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-checkin-${appointmentId}`, appointmentId, type: "CHECK_IN", status: "COMPLETED", stage: "COMPLETED", priority: 95, title: "签到已完成", description: "已进入候诊队列", primaryAction: null, secondaryAction: null, expiresAt: null, }); patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-queue-${appointmentId}`, appointmentId, type: "WAITING_QUEUE", status: "IN_PROGRESS", stage: "QUEUE_WAITING", priority: 90, title: "神经内科候诊中", description: `排队号 ${payload.queueNumber ?? "待更新"}`, queueNumber: payload.queueNumber, room: payload.room, primaryAction: "VIEW_QUEUE", secondaryAction: "VIEW_ROUTE", expiresAt: null, }); journeyProjections = updateJourneyProjection( journeyProjections, appointmentId, "QUEUE_WAITING", { checkedInAt: fixture.occurredAt, queueNumber: payload.queueNumber, room: payload.room, }, ); messages = appendJourneyMessage(messages, fixture, "签到成功,已进入候诊队列"); break; } case "QUEUE_UPDATED": { const currentQueue = patientWorkItems.find( (item) => item?.id === `work-item-queue-${appointmentId}`, ); patientWorkItems = upsertProjection(patientWorkItems, { ...(currentQueue ?? {}), id: `work-item-queue-${appointmentId}`, appointmentId, type: "WAITING_QUEUE", status: "IN_PROGRESS", stage: "QUEUE_WAITING", priority: 90, title: "神经内科候诊中", description: `前方 ${payload.peopleAhead ?? 0} 人,预计 ${payload.estimatedMinutes ?? 0} 分钟`, queueNumber: payload.queueNumber ?? currentQueue?.queueNumber, peopleAhead: payload.peopleAhead, estimatedMinutes: payload.estimatedMinutes, room: payload.room ?? currentQueue?.room, delayed: Boolean(payload.delayed), updatedAt: fixture.occurredAt, primaryAction: "VIEW_QUEUE", secondaryAction: "VIEW_ROUTE", expiresAt: null, }); journeyProjections = updateJourneyProjection( journeyProjections, appointmentId, "QUEUE_WAITING", { queueNumber: payload.queueNumber, peopleAhead: payload.peopleAhead, estimatedMinutes: payload.estimatedMinutes, room: payload.room, delayed: Boolean(payload.delayed), updatedAt: fixture.occurredAt, }, ); messages = appendJourneyMessage(messages, fixture, "候诊队列已更新"); break; } case "VISIT_STARTED": { patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-queue-${appointmentId}`, appointmentId, type: "WAITING_QUEUE", status: "COMPLETED", stage: "COMPLETED", priority: 90, title: "候诊已结束", description: "医生已开始接诊", primaryAction: null, secondaryAction: null, expiresAt: null, }); patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-visit-${appointmentId}`, appointmentId, type: "VISIT", status: "IN_PROGRESS", stage: "VISIT_IN_PROGRESS", priority: 100, title: "医生接诊中", description: payload.room ?? "请在诊室内完成就诊", primaryAction: null, secondaryAction: null, expiresAt: null, }); journeyProjections = updateJourneyProjection( journeyProjections, appointmentId, "VISIT_IN_PROGRESS", { room: payload.room, startedAt: fixture.occurredAt }, ); messages = appendJourneyMessage(messages, fixture, "医生已开始接诊"); break; } case "VISIT_COMPLETED": { patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-visit-${appointmentId}`, appointmentId, type: "VISIT", status: "COMPLETED", stage: "COMPLETED", priority: 100, title: "本次就诊已完成", description: "诊后记录与服务已更新", primaryAction: null, secondaryAction: null, expiresAt: null, }); patientWorkItems = upsertProjection(patientWorkItems, { id: `work-item-post-visit-${appointmentId}`, appointmentId, type: "POST_VISIT", status: "ACTION_REQUIRED", stage: "POST_VISIT", priority: 70, title: "查看诊后服务", description: "查看门诊记录、处方和复诊建议", primaryAction: null, secondaryAction: null, expiresAt: null, }); journeyProjections = updateJourneyProjection( journeyProjections, appointmentId, "POST_VISIT", { completedAt: fixture.occurredAt }, ); messages = appendJourneyMessage(messages, fixture, "就诊完成,诊后服务已更新"); break; } default: return nextState; } return { ...nextState, patientWorkItems, journeyProjections, messages, activeAgentTaskProjection: state.activeAgentTaskProjection, commandProjections: state.commandProjections, }; } export function requestReportInterpretation(state, reportId) { if (!Array.isArray(state?.patientWorkItems)) { return state; } const itemIndex = state.patientWorkItems.findIndex( (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId, ) ?? -1; if (itemIndex < 0) { return state; } const hasConsent = hasValidReportConsent(state, reportId); const gate = evaluateReportInterpretationGate(state, reportId); const consentId = reportConsentId(reportId); if (!gate.allowed) { const unavailableReason = [ "OCR_REVIEW_REQUIRED", "CRITICAL_NOTICE", "AI_UNAVAILABLE", "UNSUPPORTED", ].includes(gate.reason) ? gate.reason : "UNSUPPORTED"; const currentInterpretationStatus = state .reportSourceProjections?.[reportId]?.interpretationStatus; return { ...state, patientWorkItems: state.patientWorkItems.map((item, index) => ( index === itemIndex ? { ...item, stage: "REPORT_UNAVAILABLE" } : item )), reportSourceProjections: { ...state.reportSourceProjections, [reportId]: { ...state.reportSourceProjections[reportId], interpretationStatus: currentInterpretationStatus === "INVALIDATED" ? "INVALIDATED" : "NOT_REQUESTED", }, }, overlays: { ...state.overlays, consentId: null, consentScope: null, reportId, reportView: "REPORT_UNAVAILABLE", reportUnavailableReason: unavailableReason, }, }; } const existingResult = selectValidReportResult(state, reportId); if ( hasConsent && gate.allowed && state.reportSourceProjections?.[reportId]?.interpretationStatus === "READY" && existingResult ) { return { ...state, patientWorkItems: state.patientWorkItems.map((item, index) => ( index === itemIndex ? { ...item, stage: "INTERPRETED" } : item )), overlays: { ...state.overlays, consentId: null, consentScope: null, reportId, reportView: "INTERPRETED", }, }; } const patientWorkItems = state.patientWorkItems.map((item, index) => ( index === itemIndex ? { ...item, stage: hasConsent ? "INTERPRETING" : "CONSENT_REQUIRED" } : item )); return { ...state, patientWorkItems, reportSourceProjections: hasConsent ? { ...state.reportSourceProjections, [reportId]: { ...state.reportSourceProjections[reportId], interpretationStatus: "GENERATING", }, } : state.reportSourceProjections, overlays: { ...state.overlays, consentId: hasConsent ? null : consentId, consentScope: hasConsent ? null : { patientId: state.activePatientId, reportId, }, reportId, reportView: hasConsent ? "INTERPRETING" : state?.overlays?.reportView ?? null, }, }; } export function evaluateReportInterpretationGate(state, reportId) { const report = state?.reportSourceProjections?.[reportId]; if (!report || typeof report !== "object" || Array.isArray(report)) { return { allowed: false, reason: "REPORT_NOT_FOUND" }; } if (report.family !== "LAB" || report.reportType !== "BLOOD_ROUTINE") { return { allowed: false, reason: "UNSUPPORTED" }; } if (report.sourceStatus !== "FINAL") { return { allowed: false, reason: "SOURCE_NOT_FINAL" }; } if (report.verificationStatus !== "VERIFIED") { return { allowed: false, reason: typeof report.verificationReason === "string" ? report.verificationReason : "VERIFICATION_REQUIRED", }; } if (report.criticalNotice === true) { return { allowed: false, reason: "CRITICAL_NOTICE" }; } if (report.interpretationStatus === "INVALIDATED") { return { allowed: false, reason: "REPORT_INVALIDATED" }; } if ( report.interpretationUnavailableReason === "AI_UNAVAILABLE" || report.interpretationUnavailableReason === "UNSUPPORTED" ) { return { allowed: false, reason: report.interpretationUnavailableReason, }; } return { allowed: true, reason: null }; } export function grantConsent(state, consentId) { const reportId = state?.overlays?.reportId; const expectedConsentId = typeof reportId === "string" ? reportConsentId(reportId) : null; const scope = state?.overlays?.consentScope; if ( consentId !== expectedConsentId || state?.overlays?.consentId !== consentId || scope?.patientId !== state?.activePatientId || scope?.reportId !== reportId || !Array.isArray(state?.patientWorkItems) ) { return state; } const itemIndex = state?.patientWorkItems?.findIndex( (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "CONSENT_REQUIRED", ) ?? -1; if (itemIndex < 0 || !evaluateReportInterpretationGate(state, reportId).allowed) { return state; } const consent = { id: consentId, purpose: "用于本次报告的 AI 辅助解释", scope: "当前血常规报告", expiresAt: "2026-07-09T23:59:59+08:00", revocable: true, status: "GRANTED", }; const binding = { patientId: state.activePatientId, reportId, }; const pendingConsentState = { ...state, consents: { ...state.consents, [consentId]: consent, }, consentScopeBindings: { ...state.consentScopeBindings, [consentId]: binding, }, }; if (!hasValidReportConsent(pendingConsentState, reportId)) { return { ...state, patientWorkItems: state.patientWorkItems.map((item, index) => ( index === itemIndex ? { ...item, stage: "REPORT_AVAILABLE" } : item )), reportSourceProjections: { ...state.reportSourceProjections, [reportId]: { ...state.reportSourceProjections[reportId], interpretationStatus: "NOT_REQUESTED", }, }, overlays: { ...state.overlays, consentId: null, consentScope: null, reportView: "REPORT_DETAIL", }, }; } return { ...state, consents: pendingConsentState.consents, consentScopeBindings: pendingConsentState.consentScopeBindings, reportSourceProjections: { ...state.reportSourceProjections, [reportId]: { ...state.reportSourceProjections[reportId], interpretationStatus: "GENERATING", }, }, patientWorkItems: state.patientWorkItems.map((item, index) => { if (index === itemIndex) { return { ...item, stage: "INTERPRETING" }; } return item; }), resultSnapshotProjections: { ...(state.resultSnapshotProjections ?? {}), }, overlays: { ...state.overlays, consentId: null, consentScope: null, reportView: "INTERPRETING", }, }; } export function openReportDetails(state, reportId) { const exists = state?.patientWorkItems?.some( (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId, ); const report = state?.reportSourceProjections?.[reportId]; const hasConsistentSource = ( report && typeof report === "object" && !Array.isArray(report) && (report.reportId === undefined || report.reportId === reportId) ); if (!exists || !hasConsistentSource) { return state; } const isExamination = report.family === "EXAM"; const gate = isExamination ? { allowed: true, reason: null } : evaluateReportInterpretationGate(state, reportId); const unavailableReason = gate.allowed ? null : ( [ "OCR_REVIEW_REQUIRED", "CRITICAL_NOTICE", "AI_UNAVAILABLE", "UNSUPPORTED", ].includes(gate.reason) ? gate.reason : "UNSUPPORTED" ); return { ...state, reportSourceProjections: { ...(state.reportSourceProjections ?? {}), [reportId]: { ...report, readStatus: "READ", }, }, overlays: { ...state.overlays, reportId, reportView: unavailableReason ? "REPORT_UNAVAILABLE" : "REPORT_DETAIL", reportUnavailableReason: unavailableReason, reportExpanded: false, reportFeedbackOpen: false, reportFeedbackSubmitted: false, reportServiceNotice: null, }, }; } export function closeReportView(state) { if (!state?.overlays?.reportId && !state?.overlays?.reportView) { return state; } return { ...state, overlays: { ...state.overlays, consentId: null, consentScope: null, reportId: null, reportView: null, reportUnavailableReason: null, reportExpanded: false, reportFeedbackOpen: false, reportFeedbackSubmitted: false, reportServiceNotice: null, }, }; } export function rejectConsent(state, consentId) { const reportId = state?.overlays?.reportId; if ( consentId !== reportConsentId(reportId) || state?.overlays?.consentId !== consentId || state?.overlays?.consentScope?.patientId !== state?.activePatientId || state?.overlays?.consentScope?.reportId !== reportId || !Array.isArray(state?.patientWorkItems) ) { return state; } const hasPendingItem = state.patientWorkItems?.some( (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "CONSENT_REQUIRED", ); if (typeof reportId !== "string" || !hasPendingItem) { return state; } return { ...state, patientWorkItems: state.patientWorkItems.map((item) => ( item?.type === "REPORT_REVIEW" && item.reportId === reportId ? { ...item, stage: "REPORT_AVAILABLE" } : item )), overlays: { ...state.overlays, consentId: null, consentScope: null, reportView: "REPORT_DETAIL", }, }; } export function applyReportInterpretationProjectionFixture(state, reportId) { const itemIndex = state?.patientWorkItems?.findIndex( (item) => item?.type === "REPORT_REVIEW" && item.reportId === reportId && item.stage === "INTERPRETING", ) ?? -1; const consentGranted = hasValidReportConsent(state, reportId); const gate = evaluateReportInterpretationGate(state, reportId); const source = state?.reportSourceProjections?.[reportId]; if ( itemIndex < 0 || !consentGranted || !gate.allowed || !isRecord(source) ) { return state; } const result = { ...reportInterpretationProjectionFixture, resultRef: reportResultSnapshotId(reportId), reportId, sourceVersion: source.sourceVersion, sourceHash: source.sourceHash, valid: true, interpretation: { ...reportInterpretationProjectionFixture.interpretation, highlights: reportInterpretationProjectionFixture.interpretation .highlights.map((highlight) => ({ ...highlight })), explanations: reportInterpretationProjectionFixture.interpretation .explanations.map((explanation) => ({ ...explanation })), canDo: [ ...reportInterpretationProjectionFixture.interpretation.canDo, ], evidence: [ ...reportInterpretationProjectionFixture.interpretation.evidence, ], }, }; return { ...state, patientWorkItems: state.patientWorkItems.map((item, index) => ( index === itemIndex ? { ...item, stage: "INTERPRETED" } : item )), reportSourceProjections: { ...state.reportSourceProjections, [reportId]: { ...state.reportSourceProjections[reportId], interpretationStatus: "READY", }, }, resultSnapshotProjections: { ...state.resultSnapshotProjections, [result.resultRef]: result, }, overlays: { ...state.overlays, consentId: null, reportId, reportView: "INTERPRETED", }, }; } export function submitReportFeedback(state, payload = {}) { const reportId = payload?.reportId; const category = payload?.category; const reportSources = state?.reportSourceProjections; const report = ( typeof reportId === "string" && reportId.length > 0 && reportSources && typeof reportSources === "object" && !Array.isArray(reportSources) && Object.hasOwn(reportSources, reportId) ) ? reportSources[reportId] : null; const hasReportTask = Array.isArray(state?.patientWorkItems) && state.patientWorkItems.some((item) => ( item?.type === "REPORT_REVIEW" && item.reportId === reportId )); const result = selectValidReportResult(state, reportId); if ( !report || typeof report !== "object" || Array.isArray(report) || report.reportId !== reportId || !hasReportTask || !reportFeedbackCategories.has(category) || !Array.isArray(state?.reportFeedback) || !hasValidReportConsent(state, reportId) || !evaluateReportInterpretationGate(state, reportId).allowed || report.interpretationStatus !== "READY" || !result ) { return state; } const feedback = { id: [ "feedback", encodeReportId(reportId), report.sourceVersion, category, ].join("-"), reportId, sourceVersion: report.sourceVersion, sourceHash: report.sourceHash, resultRef: result.resultRef, resultVersion: result.resultVersion, category, }; if (state.reportFeedback.some((item) => item?.id === feedback.id)) { return state; } return { ...state, reportFeedback: [...state.reportFeedback, feedback], }; } export function revokeConsent(state, consentId) { if ( !isRecord(state) || typeof consentId !== "string" || !isRecord(state.consents) || !isRecord(state.consentScopeBindings) || !isRecord(state.reportSourceProjections) || !isRecord(state.resultSnapshotProjections) || !isRecord(state.overlays) || !Array.isArray(state.patientWorkItems) || !Object.hasOwn(state.consents, consentId) || !Object.hasOwn(state.consentScopeBindings, consentId) ) { return state; } const consent = state.consents[consentId]; const binding = state.consentScopeBindings[consentId]; if ( !isRecord(consent) || !isRecord(binding) || typeof binding.reportId !== "string" || !Object.hasOwn(state.reportSourceProjections, binding.reportId) || !isRecord(state.reportSourceProjections[binding.reportId]) || consentId !== reportConsentId(binding.reportId) || consent?.id !== consentId || binding?.patientId !== state?.activePatientId ) { return state; } const reportId = binding.reportId; const consentScopeBindings = Object.fromEntries( Object.entries(state.consentScopeBindings).filter( ([bindingConsentId]) => bindingConsentId !== consentId, ), ); const resultSnapshotProjections = Object.fromEntries( Object.entries(state.resultSnapshotProjections ?? {}).filter( ([, result]) => result?.reportId !== reportId, ), ); return { ...state, consents: { ...state.consents, [consentId]: { ...consent, status: "REVOKED", }, }, consentScopeBindings, reportSourceProjections: { ...state.reportSourceProjections, [reportId]: { ...state.reportSourceProjections[reportId], interpretationStatus: "NOT_REQUESTED", }, }, patientWorkItems: state.patientWorkItems.map((item) => ( item?.type === "REPORT_REVIEW" && item.reportId === reportId ? { ...item, stage: "REPORT_AVAILABLE" } : item )), resultSnapshotProjections, overlays: { ...state.overlays, consentId: null, consentScope: null, reportId, reportView: "REPORT_DETAIL", reportFeedbackOpen: false, reportFeedbackSubmitted: false, reportServiceNotice: null, }, }; } const scenarioNames = new Set([ "REPORT_ARRIVED", "REPORT_DETAIL", "REPORT_CONSENT", "REPORT_GENERATING", "REPORT_READY", "REPORT_NEED_REVIEW", "REPORT_CRITICAL", "REPORT_INVALIDATED", "EMPTY_HOME", "REGISTRATION_QUERY", "COMMAND_PREPARED", "APPOINTMENT_CREATED", "CHECKIN_AVAILABLE", "QUEUE_UPDATED", ]); const loadRegistrationScenario = (base) => submitInteraction( base, registrationDemoText, ); const loadLockedRegistrationScenario = (base) => { const queried = loadRegistrationScenario(base); return applyCandidateSelectionResponseFixture(queried, { id: "scenario-selection-success-001", outcome: "SUCCESS", resultRef: "result-slot-001", resultVersion: 1, candidateId: "candidate-slot-0930", validation: { tenantMatched: true, patientMatched: true, sessionMatched: true, versionMatched: true, notExpired: true, realtimeAvailable: true, l1LockSucceeded: true, }, lock: { lockId: "lock-slot-001", expiresAt: "2026-07-03T09:05:00+08:00", }, }); }; const loadCommandPreparedScenario = (base) => { const locked = loadLockedRegistrationScenario(base); return applyCommandProjectionFixture(locked, { id: "scenario-command-awaiting-001", commandId: "command-register-001", version: 1, riskLevel: "L2", status: "AWAITING_CONFIRMATION", details: { operationName: "确认挂号", patientName: "张先生", departmentName: "神经内科", doctorName: "李明", doctorTitle: "主任医师", visitAt: "2026-07-03 09:30", fee: "30元", expiresAt: "2026-07-03T09:05:00+08:00", countdown: "剩余 5 分钟", candidateId: "candidate-slot-0930", }, }); }; export function loadScenario(state, scenarioName) { const normalized = scenarioNames.has(scenarioName) ? scenarioName : "EMPTY_HOME"; const base = { ...createInitialState({ activePatientId: state?.activePatientId ?? "patient-zhang-san", }), demoScenario: normalized, }; const reportFixture = { id: "scenario-report-arrived-001", type: "REPORT_ISSUED", occurredAt: "2026-07-03T09:30:00+08:00", payload: { reportId: "report-blood-20260702" }, }; switch (normalized) { case "REPORT_ARRIVED": return { ...applyServerProjectionFixture(base, reportFixture), demoScenario: normalized }; case "REPORT_DETAIL": { const report = applyServerProjectionFixture(base, reportFixture); return { ...openReportDetails(report, "report-blood-20260702"), demoScenario: normalized, }; } case "REPORT_CONSENT": { const report = openReportDetails( applyServerProjectionFixture(base, reportFixture), "report-blood-20260702", ); return { ...requestReportInterpretation(report, "report-blood-20260702"), demoScenario: normalized, }; } case "REPORT_GENERATING": { const pending = loadScenario(base, "REPORT_CONSENT"); return { ...grantConsent( pending, "consent-ai-report-blood-20260702", ), demoScenario: normalized, }; } case "REPORT_NEED_REVIEW": { const report = applyServerProjectionFixture(base, { ...reportFixture, payload: { ...reportFixture.payload, verificationStatus: "NEED_REVIEW", verificationReason: "OCR_REVIEW_REQUIRED", }, }); return { ...openReportDetails(report, "report-blood-20260702"), demoScenario: normalized, }; } case "REPORT_CRITICAL": { const report = applyServerProjectionFixture(base, { ...reportFixture, payload: { ...reportFixture.payload, criticalNotice: true, }, }); return { ...openReportDetails(report, "report-blood-20260702"), demoScenario: normalized, }; } case "REPORT_INVALIDATED": { const ready = loadScenario(base, "REPORT_READY"); return { ...applyServerProjectionFixture(ready, { id: "scenario-report-corrected-002", type: "REPORT_CORRECTED", occurredAt: "2026-07-03T11:00:00+08:00", payload: { reportId: "report-blood-20260702", sourceVersion: 2, sourceHash: "sha256:blood-v2", }, }), demoScenario: normalized, }; } case "REPORT_READY": { const report = applyServerProjectionFixture(base, reportFixture); const pending = requestReportInterpretation( report, "report-blood-20260702", ); const interpreting = grantConsent( pending, "consent-ai-report-blood-20260702", ); return { ...applyReportInterpretationProjectionFixture( interpreting, "report-blood-20260702", ), demoScenario: normalized, }; } case "REGISTRATION_QUERY": return { ...loadRegistrationScenario(base), demoScenario: normalized }; case "COMMAND_PREPARED": return { ...loadCommandPreparedScenario(base), demoScenario: normalized }; case "APPOINTMENT_CREATED": { const awaiting = loadCommandPreparedScenario(base); return { ...applyCommandProjectionFixture(awaiting, { id: "scenario-command-succeeded-001", commandId: "command-register-001", status: "SUCCEEDED", resultFixtureId: "appointment-created-001", }), activeTab: "home", demoScenario: normalized, }; } case "CHECKIN_AVAILABLE": { const appointment = loadScenario(base, "APPOINTMENT_CREATED"); return { ...applyServerProjectionFixture(appointment, { id: "scenario-checkin-available-001", type: "CHECKIN_AVAILABLE", occurredAt: "2026-07-03T09:10:00+08:00", payload: { appointmentId: "appointment-001", location: "门诊三楼神经内科诊区", }, }), demoScenario: normalized, }; } case "QUEUE_UPDATED": { const arrived = loadScenario(base, "CHECKIN_AVAILABLE"); const checkedIn = applyServerProjectionFixture(arrived, { id: "scenario-checkin-completed-001", type: "CHECKIN_COMPLETED", occurredAt: "2026-07-03T09:12:00+08:00", payload: { appointmentId: "appointment-001", queueNumber: "A023", room: "3F-08诊室", }, }); return { ...applyServerProjectionFixture(checkedIn, { id: "scenario-queue-updated-001", type: "QUEUE_UPDATED", occurredAt: "2026-07-03T09:18:00+08:00", payload: { appointmentId: "appointment-001", queueNumber: "A023", peopleAhead: 4, estimatedMinutes: 18, room: "3F-08诊室", delayed: true, }, }), demoScenario: normalized, }; } default: return base; } }