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

379 lines
13 KiB
TypeScript

import { and, desc, eq, sql } from "drizzle-orm";
import { getDatabase } from "@/modules/core/server/db";
import {
accounts,
chronicConditions,
elders,
healthAnomalyReviews,
healthProfiles,
vitalRecords,
} from "@/modules/core/server/schema";
import type {
ChronicCondition,
ChronicConditionInput,
HealthAdminData,
HealthAnomalyReview,
HealthProfile,
HealthProfileInput,
HealthReviewUpdateInput,
VitalRecord,
VitalRecordInput,
} from "@/modules/health/types";
export type MutationFailure = {
success: false;
reason: string;
status: number;
};
type ElderRow = typeof elders.$inferSelect;
type AccountNameRow = {
id: string;
name: string;
};
export function isMutationFailure(value: unknown): value is MutationFailure {
return (
typeof value === "object" &&
value !== null &&
"success" in value &&
(value as { success?: unknown }).success === false
);
}
function iso(value: Date): string {
return value.toISOString();
}
function optionalIso(value: Date | null): string | undefined {
return value ? iso(value) : undefined;
}
function buildNameMap(rows: AccountNameRow[]): Map<string, string> {
return new Map(rows.map((row) => [row.id, row.name]));
}
function toProfile(row: typeof healthProfiles.$inferSelect, elderName: string): HealthProfile {
return {
id: row.id,
organizationId: row.organizationId,
elderId: row.elderId,
elderName,
allergyNotes: row.allergyNotes,
medicalHistory: row.medicalHistory,
medicationNotes: row.medicationNotes,
careRestrictions: row.careRestrictions,
emergencyNotes: row.emergencyNotes,
createdAt: iso(row.createdAt),
updatedAt: iso(row.updatedAt),
};
}
function toVital(row: typeof vitalRecords.$inferSelect, elderName: string, accountNameById: Map<string, string>): VitalRecord {
return {
id: row.id,
organizationId: row.organizationId,
elderId: row.elderId,
elderName,
recordedAt: iso(row.recordedAt),
source: row.source,
systolicBp: row.systolicBp ?? undefined,
diastolicBp: row.diastolicBp ?? undefined,
heartRate: row.heartRate ?? undefined,
temperatureTenths: row.temperatureTenths ?? undefined,
spo2: row.spo2 ?? undefined,
bloodGlucoseTenths: row.bloodGlucoseTenths ?? undefined,
weightTenths: row.weightTenths ?? undefined,
notes: row.notes,
createdByAccountId: row.createdByAccountId ?? undefined,
createdByName: row.createdByAccountId ? accountNameById.get(row.createdByAccountId) : undefined,
createdAt: iso(row.createdAt),
updatedAt: iso(row.updatedAt),
};
}
function toCondition(row: typeof chronicConditions.$inferSelect, elderName: string): ChronicCondition {
return {
id: row.id,
organizationId: row.organizationId,
elderId: row.elderId,
elderName,
name: row.name,
status: row.status,
diagnosedAt: optionalIso(row.diagnosedAt),
treatmentNotes: row.treatmentNotes,
followUpNotes: row.followUpNotes,
createdAt: iso(row.createdAt),
updatedAt: iso(row.updatedAt),
};
}
function toReview(
row: typeof healthAnomalyReviews.$inferSelect,
elderName: string,
accountNameById: Map<string, string>,
): HealthAnomalyReview {
return {
id: row.id,
organizationId: row.organizationId,
elderId: row.elderId,
elderName,
vitalRecordId: row.vitalRecordId ?? undefined,
severity: row.severity,
status: row.status,
title: row.title,
description: row.description,
reviewedByAccountId: row.reviewedByAccountId ?? undefined,
reviewedByName: row.reviewedByAccountId ? accountNameById.get(row.reviewedByAccountId) : undefined,
reviewedAt: optionalIso(row.reviewedAt),
resolutionNotes: row.resolutionNotes,
createdAt: iso(row.createdAt),
updatedAt: iso(row.updatedAt),
};
}
function getElderName(row: ElderRow | undefined): string {
return row?.name ?? "";
}
async function findElder(organizationId: string, elderId: string): Promise<ElderRow | undefined> {
const database = getDatabase();
const rows = await database
.select()
.from(elders)
.where(and(eq(elders.id, elderId), eq(elders.organizationId, organizationId)))
.limit(1);
return rows[0];
}
function detectVitalAnomaly(input: VitalRecordInput): { title: string; description: string; severity: "warning" | "critical" } | null {
if ((input.systolicBp !== undefined && input.systolicBp >= 180) || (input.diastolicBp !== undefined && input.diastolicBp >= 110)) {
return { title: "血压严重异常", description: "血压达到紧急复核阈值", severity: "critical" };
}
if ((input.systolicBp !== undefined && input.systolicBp >= 150) || (input.diastolicBp !== undefined && input.diastolicBp >= 95)) {
return { title: "血压异常", description: "血压偏高,需要护理人员复核", severity: "warning" };
}
if (input.temperatureTenths !== undefined && input.temperatureTenths >= 380) {
return { title: "体温异常", description: "体温偏高,需要复核", severity: "warning" };
}
if (input.spo2 !== undefined && input.spo2 < 92) {
return { title: "血氧异常", description: "血氧偏低,需要复核", severity: "critical" };
}
return null;
}
export async function listHealthAdminData(organizationId: string): Promise<HealthAdminData> {
const database = getDatabase();
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const [elderRows, profileRows, vitalRows, conditionRows, reviewRows, accountRows] = await Promise.all([
database.select().from(elders).where(eq(elders.organizationId, organizationId)).orderBy(desc(elders.createdAt)),
database.select().from(healthProfiles).where(eq(healthProfiles.organizationId, organizationId)).orderBy(desc(healthProfiles.updatedAt)),
database.select().from(vitalRecords).where(eq(vitalRecords.organizationId, organizationId)).orderBy(desc(vitalRecords.recordedAt)).limit(100),
database
.select()
.from(chronicConditions)
.where(eq(chronicConditions.organizationId, organizationId))
.orderBy(desc(chronicConditions.updatedAt)),
database
.select()
.from(healthAnomalyReviews)
.where(eq(healthAnomalyReviews.organizationId, organizationId))
.orderBy(desc(healthAnomalyReviews.createdAt)),
database.select({ id: accounts.id, name: accounts.name }).from(accounts),
]);
const elderById = new Map(elderRows.map((elder) => [elder.id, elder]));
const accountNameById = buildNameMap(accountRows);
return {
elders: elderRows.map((elder) => ({
id: elder.id,
name: elder.name,
careLevel: elder.careLevel,
status: elder.status,
})),
profiles: profileRows.map((profile) => toProfile(profile, getElderName(elderById.get(profile.elderId)))),
vitals: vitalRows.map((vital) => toVital(vital, getElderName(elderById.get(vital.elderId)), accountNameById)),
chronicConditions: conditionRows.map((condition) => toCondition(condition, getElderName(elderById.get(condition.elderId)))),
reviews: reviewRows.map((review) => toReview(review, getElderName(elderById.get(review.elderId)), accountNameById)),
metrics: {
eldersWithProfiles: profileRows.length,
vitalsToday: vitalRows.filter((vital) => vital.recordedAt >= todayStart).length,
activeChronicConditions: conditionRows.filter((condition) => condition.status === "active").length,
pendingReviews: reviewRows.filter((review) => review.status === "pending").length,
},
};
}
export async function upsertHealthProfile(input: HealthProfileInput & { elderId: string; organizationId: string }): Promise<HealthProfile | MutationFailure> {
const elder = await findElder(input.organizationId, input.elderId);
if (!elder) {
return { success: false, reason: "老人档案不存在", status: 404 };
}
const database = getDatabase();
const rows = await database
.insert(healthProfiles)
.values({
organizationId: input.organizationId,
elderId: input.elderId,
allergyNotes: input.allergyNotes,
medicalHistory: input.medicalHistory,
medicationNotes: input.medicationNotes,
careRestrictions: input.careRestrictions,
emergencyNotes: input.emergencyNotes,
})
.onConflictDoUpdate({
target: [healthProfiles.organizationId, healthProfiles.elderId],
set: {
allergyNotes: input.allergyNotes,
medicalHistory: input.medicalHistory,
medicationNotes: input.medicationNotes,
careRestrictions: input.careRestrictions,
emergencyNotes: input.emergencyNotes,
updatedAt: sql`NOW()`,
},
})
.returning();
const profile = rows[0];
if (!profile) {
return { success: false, reason: "健康档案保存失败", status: 500 };
}
return toProfile(profile, elder.name);
}
export async function createVitalRecord(input: VitalRecordInput & { accountId: string; organizationId: string }): Promise<
| {
vital: VitalRecord;
review?: HealthAnomalyReview;
}
| MutationFailure
> {
const elder = await findElder(input.organizationId, input.elderId);
if (!elder) {
return { success: false, reason: "老人档案不存在", status: 404 };
}
const database = getDatabase();
const created = await database.transaction(async (transaction) => {
const vitalRows = await transaction
.insert(vitalRecords)
.values({
organizationId: input.organizationId,
elderId: input.elderId,
recordedAt: input.recordedAt,
source: input.source,
systolicBp: input.systolicBp,
diastolicBp: input.diastolicBp,
heartRate: input.heartRate,
temperatureTenths: input.temperatureTenths,
spo2: input.spo2,
bloodGlucoseTenths: input.bloodGlucoseTenths,
weightTenths: input.weightTenths,
notes: input.notes,
createdByAccountId: input.accountId,
})
.returning();
const vital = vitalRows[0];
if (!vital) {
return null;
}
const anomaly = detectVitalAnomaly(input);
if (!anomaly) {
return { vital };
}
const reviewRows = await transaction
.insert(healthAnomalyReviews)
.values({
organizationId: input.organizationId,
elderId: input.elderId,
vitalRecordId: vital.id,
severity: anomaly.severity,
status: "pending",
title: anomaly.title,
description: anomaly.description,
})
.returning();
return { vital, review: reviewRows[0] };
});
if (!created) {
return { success: false, reason: "生命体征记录创建失败", status: 500 };
}
const accountRows = await database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(eq(accounts.id, input.accountId));
const accountNameById = buildNameMap(accountRows);
return {
vital: toVital(created.vital, elder.name, accountNameById),
review: created.review ? toReview(created.review, elder.name, accountNameById) : undefined,
};
}
export async function createChronicCondition(
input: ChronicConditionInput & { organizationId: string },
): Promise<ChronicCondition | MutationFailure> {
const elder = await findElder(input.organizationId, input.elderId);
if (!elder) {
return { success: false, reason: "老人档案不存在", status: 404 };
}
const database = getDatabase();
const rows = await database
.insert(chronicConditions)
.values({
organizationId: input.organizationId,
elderId: input.elderId,
name: input.name,
status: input.status,
diagnosedAt: input.diagnosedAt,
treatmentNotes: input.treatmentNotes,
followUpNotes: input.followUpNotes,
})
.returning();
const condition = rows[0];
if (!condition) {
return { success: false, reason: "慢病记录创建失败", status: 500 };
}
return toCondition(condition, elder.name);
}
export async function updateHealthReview(input: HealthReviewUpdateInput & { accountId: string; id: string; organizationId: string }): Promise<
HealthAnomalyReview | MutationFailure
> {
const database = getDatabase();
const rows = await database
.update(healthAnomalyReviews)
.set({
status: input.status,
resolutionNotes: input.resolutionNotes,
reviewedByAccountId: input.accountId,
reviewedAt: new Date(),
updatedAt: new Date(),
})
.where(and(eq(healthAnomalyReviews.id, input.id), eq(healthAnomalyReviews.organizationId, input.organizationId)))
.returning();
const review = rows[0];
if (!review) {
return { success: false, reason: "异常复核记录不存在", status: 404 };
}
const [elderRows, accountRows] = await Promise.all([
database.select().from(elders).where(eq(elders.id, review.elderId)).limit(1),
database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(eq(accounts.id, input.accountId)),
]);
return toReview(review, getElderName(elderRows[0]), buildNameMap(accountRows));
}