feat: add elder bed operational context
This commit is contained in:
199
modules/operations/server/context.ts
Normal file
199
modules/operations/server/context.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { and, desc, eq, inArray, or } from "drizzle-orm";
|
||||
|
||||
import { getDatabase } from "@/modules/core/server/db";
|
||||
import { careTasks, healthAnomalyReviews, systemIncidents, vitalRecords } from "@/modules/core/server/schema";
|
||||
import type { Admission, FacilityBed, Permission } from "@/modules/core/types";
|
||||
import type { Elder } from "@/modules/elders/types";
|
||||
import { CARE_LEVEL_LABELS, ELDER_STATUS_LABELS } from "@/modules/elders/types";
|
||||
import { CARE_TASK_STATUS_LABELS, CARE_TASK_TYPE_LABELS } from "@/modules/care/types";
|
||||
import { HEALTH_REVIEW_SEVERITY_LABELS, HEALTH_REVIEW_STATUS_LABELS } from "@/modules/health/types";
|
||||
import { INCIDENT_SEVERITY_LABELS, INCIDENT_STATUS_LABELS } from "@/modules/emergency/types";
|
||||
import type { BedOperationalContext, ElderBedContextData, ElderOperationalContext, OperationalContextLine } from "@/modules/operations/types";
|
||||
|
||||
type ContextInput = {
|
||||
admissions: Admission[];
|
||||
beds: FacilityBed[];
|
||||
elders: Elder[];
|
||||
organizationId: string;
|
||||
permissions: readonly Permission[];
|
||||
};
|
||||
|
||||
function hasPermission(permissions: readonly Permission[], permission: Permission): boolean {
|
||||
return permissions.includes(permission);
|
||||
}
|
||||
|
||||
function addLine(target: { lines: OperationalContextLine[] }, line: OperationalContextLine): void {
|
||||
if (target.lines.length >= 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.lines.push(line);
|
||||
}
|
||||
|
||||
function includesText(source: string, token: string): boolean {
|
||||
return token.length > 0 && source.includes(token);
|
||||
}
|
||||
|
||||
export async function listElderBedContextData(input: ContextInput): Promise<ElderBedContextData> {
|
||||
const { admissions, beds, elders: elderList, organizationId, permissions } = input;
|
||||
const elderContexts = Object.fromEntries(elderList.map((elder) => [elder.id, { lines: [] }])) as Record<string, ElderOperationalContext>;
|
||||
const bedContexts = Object.fromEntries(beds.map((bed) => [bed.id, { lines: [] }])) as Record<string, BedOperationalContext>;
|
||||
const elderById = new Map(elderList.map((elder) => [elder.id, elder]));
|
||||
const activeAdmissions = admissions.filter((admission) => admission.status === "active");
|
||||
|
||||
for (const admission of activeAdmissions) {
|
||||
const elder = elderById.get(admission.elderId);
|
||||
const bedContext = bedContexts[admission.bedId];
|
||||
if (!elder || !bedContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bedContext.occupant = {
|
||||
elderName: elder.name,
|
||||
careLevel: CARE_LEVEL_LABELS[elder.careLevel],
|
||||
status: ELDER_STATUS_LABELS[elder.status],
|
||||
};
|
||||
}
|
||||
|
||||
const database = getDatabase();
|
||||
const elderIds = elderList.map((elder) => elder.id);
|
||||
if (elderIds.length === 0) {
|
||||
return { bedContexts, elderContexts };
|
||||
}
|
||||
|
||||
const [careRows, reviewRows, vitalRows, incidentRows] = await Promise.all([
|
||||
hasPermission(permissions, "care:read")
|
||||
? database
|
||||
.select()
|
||||
.from(careTasks)
|
||||
.where(and(eq(careTasks.organizationId, organizationId), inArray(careTasks.elderId, elderIds)))
|
||||
.orderBy(desc(careTasks.scheduledAt))
|
||||
.limit(60)
|
||||
: [],
|
||||
hasPermission(permissions, "health:read")
|
||||
? database
|
||||
.select()
|
||||
.from(healthAnomalyReviews)
|
||||
.where(and(eq(healthAnomalyReviews.organizationId, organizationId), inArray(healthAnomalyReviews.elderId, elderIds)))
|
||||
.orderBy(desc(healthAnomalyReviews.createdAt))
|
||||
.limit(60)
|
||||
: [],
|
||||
hasPermission(permissions, "health:read")
|
||||
? database
|
||||
.select()
|
||||
.from(vitalRecords)
|
||||
.where(and(eq(vitalRecords.organizationId, organizationId), inArray(vitalRecords.elderId, elderIds)))
|
||||
.orderBy(desc(vitalRecords.recordedAt))
|
||||
.limit(60)
|
||||
: [],
|
||||
hasPermission(permissions, "incident:read")
|
||||
? database
|
||||
.select()
|
||||
.from(systemIncidents)
|
||||
.where(
|
||||
and(
|
||||
eq(systemIncidents.organizationId, organizationId),
|
||||
or(eq(systemIncidents.status, "open"), eq(systemIncidents.status, "acknowledged")),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(systemIncidents.createdAt))
|
||||
.limit(60)
|
||||
: [],
|
||||
]);
|
||||
|
||||
const bedIdByElderId = new Map(activeAdmissions.map((admission) => [admission.elderId, admission.bedId]));
|
||||
const seenCareElders = new Set<string>();
|
||||
for (const task of careRows) {
|
||||
if (!task.elderId || seenCareElders.has(task.elderId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const line: OperationalContextLine = {
|
||||
kind: "care",
|
||||
label: "护理",
|
||||
value: `${CARE_TASK_TYPE_LABELS[task.careType]} / ${CARE_TASK_STATUS_LABELS[task.status]}`,
|
||||
tone: task.status === "pending" || task.status === "in_progress" ? "warning" : "muted",
|
||||
};
|
||||
const elderContext = elderContexts[task.elderId];
|
||||
if (elderContext) {
|
||||
addLine(elderContext, line);
|
||||
}
|
||||
const bedId = bedIdByElderId.get(task.elderId);
|
||||
const bedContext = bedId ? bedContexts[bedId] : undefined;
|
||||
if (bedContext) {
|
||||
addLine(bedContext, line);
|
||||
}
|
||||
seenCareElders.add(task.elderId);
|
||||
}
|
||||
|
||||
const seenReviewElders = new Set<string>();
|
||||
for (const review of reviewRows) {
|
||||
if (seenReviewElders.has(review.elderId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const line: OperationalContextLine = {
|
||||
kind: "health",
|
||||
label: "健康",
|
||||
value: `${HEALTH_REVIEW_SEVERITY_LABELS[review.severity]} / ${HEALTH_REVIEW_STATUS_LABELS[review.status]}:${review.title}`,
|
||||
tone: review.severity === "critical" ? "danger" : "warning",
|
||||
};
|
||||
const elderContext = elderContexts[review.elderId];
|
||||
if (elderContext) {
|
||||
addLine(elderContext, line);
|
||||
}
|
||||
const bedId = bedIdByElderId.get(review.elderId);
|
||||
const bedContext = bedId ? bedContexts[bedId] : undefined;
|
||||
if (bedContext) {
|
||||
addLine(bedContext, line);
|
||||
}
|
||||
seenReviewElders.add(review.elderId);
|
||||
}
|
||||
|
||||
const seenVitalElders = new Set<string>();
|
||||
for (const vital of vitalRows) {
|
||||
if (seenReviewElders.has(vital.elderId) || seenVitalElders.has(vital.elderId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bp = vital.systolicBp && vital.diastolicBp ? `血压 ${vital.systolicBp}/${vital.diastolicBp}` : "";
|
||||
const spo2 = vital.spo2 === null ? "" : `血氧 ${vital.spo2}%`;
|
||||
const value = [bp, spo2].filter(Boolean).join(" · ") || "最近体征已记录";
|
||||
const line: OperationalContextLine = { kind: "health", label: "体征", value, tone: "muted" };
|
||||
const elderContext = elderContexts[vital.elderId];
|
||||
if (elderContext) {
|
||||
addLine(elderContext, line);
|
||||
}
|
||||
seenVitalElders.add(vital.elderId);
|
||||
}
|
||||
|
||||
for (const incident of incidentRows) {
|
||||
const haystack = [incident.title, incident.description, incident.source].join(" ");
|
||||
for (const elder of elderList) {
|
||||
const bedId = bedIdByElderId.get(elder.id);
|
||||
const bed = bedId ? beds.find((item) => item.id === bedId) : undefined;
|
||||
const matchesElder = includesText(haystack, elder.name);
|
||||
const matchesBed = bed ? includesText(haystack, bed.code) || includesText(haystack, bed.roomName) : false;
|
||||
if (!matchesElder && !matchesBed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const line: OperationalContextLine = {
|
||||
kind: "incident",
|
||||
label: "应急",
|
||||
value: `${INCIDENT_SEVERITY_LABELS[incident.severity]} / ${INCIDENT_STATUS_LABELS[incident.status]}:${incident.title}`,
|
||||
tone: incident.severity === "critical" ? "danger" : "warning",
|
||||
};
|
||||
const elderContext = elderContexts[elder.id];
|
||||
if (elderContext) {
|
||||
addLine(elderContext, line);
|
||||
}
|
||||
const bedContext = bedId ? bedContexts[bedId] : undefined;
|
||||
if (bedContext) {
|
||||
addLine(bedContext, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { bedContexts, elderContexts };
|
||||
}
|
||||
26
modules/operations/types.ts
Normal file
26
modules/operations/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type ContextLineKind = "care" | "health" | "incident" | "admission";
|
||||
|
||||
export type OperationalContextLine = {
|
||||
kind: ContextLineKind;
|
||||
label: string;
|
||||
value: string;
|
||||
tone?: "danger" | "muted" | "warning";
|
||||
};
|
||||
|
||||
export type ElderOperationalContext = {
|
||||
lines: OperationalContextLine[];
|
||||
};
|
||||
|
||||
export type BedOperationalContext = {
|
||||
occupant?: {
|
||||
careLevel: string;
|
||||
elderName: string;
|
||||
status: string;
|
||||
};
|
||||
lines: OperationalContextLine[];
|
||||
};
|
||||
|
||||
export type ElderBedContextData = {
|
||||
bedContexts: Record<string, BedOperationalContext>;
|
||||
elderContexts: Record<string, ElderOperationalContext>;
|
||||
};
|
||||
Reference in New Issue
Block a user