Files
teatea-pension/modules/operations/server/context.ts

200 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}