200 lines
7.5 KiB
TypeScript
200 lines
7.5 KiB
TypeScript
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 };
|
||
}
|