| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315 |
- 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;
- }
- }
|