2255 lines
79 KiB
TypeScript
2255 lines
79 KiB
TypeScript
import { and, eq, isNull, or } from "drizzle-orm";
|
||
|
||
import type { ElderAiAnalysisOutput } from "@/modules/ai/types";
|
||
import { getDatabase } from "@/modules/core/server/db";
|
||
import {
|
||
admissions,
|
||
aiKnowledgeChunks,
|
||
aiKnowledgeEntries,
|
||
alertRules,
|
||
alertTriggers,
|
||
beds,
|
||
buildings,
|
||
campuses,
|
||
careTasks,
|
||
chronicConditions,
|
||
deviceAssets,
|
||
elderAiAnalyses,
|
||
elders,
|
||
familyContacts,
|
||
familyFeedback,
|
||
familyVisitAppointments,
|
||
floors,
|
||
healthAnomalyReviews,
|
||
healthProfiles,
|
||
maintenanceTickets,
|
||
notices,
|
||
rooms,
|
||
systemIncidents,
|
||
vitalRecords,
|
||
} from "@/modules/core/server/schema";
|
||
|
||
type SeedRoom = {
|
||
buildingName: string;
|
||
campusName: string;
|
||
code: string;
|
||
floorLevel: number;
|
||
floorName: string;
|
||
name: string;
|
||
capacity: number;
|
||
};
|
||
|
||
type SeedBed = {
|
||
code: string;
|
||
notes: string;
|
||
roomCode: string;
|
||
status: "available" | "occupied" | "maintenance" | "disabled";
|
||
};
|
||
|
||
type SeedElder = {
|
||
age: number;
|
||
careLevel: "self-care" | "semi-assisted" | "assisted" | "intensive";
|
||
gender: "male" | "female" | "other";
|
||
medicalNotes: string;
|
||
name: string;
|
||
phone: string;
|
||
primaryContact: string;
|
||
status: "active" | "pending" | "discharged";
|
||
};
|
||
|
||
type SeedAdmission = {
|
||
bedCode: string;
|
||
elderName: string;
|
||
notes: string;
|
||
status: "active" | "transferred" | "discharged";
|
||
};
|
||
|
||
type SeedHealthProfile = {
|
||
allergyNotes: string;
|
||
careRestrictions: string;
|
||
elderName: string;
|
||
emergencyNotes: string;
|
||
medicalHistory: string;
|
||
medicationNotes: string;
|
||
};
|
||
|
||
type SeedVitalRecord = {
|
||
bloodGlucoseTenths?: number;
|
||
diastolicBp?: number;
|
||
elderName: string;
|
||
heartRate?: number;
|
||
notes: string;
|
||
recordedHoursAgo: number;
|
||
source: "manual" | "device" | "import";
|
||
spo2?: number;
|
||
systolicBp?: number;
|
||
temperatureTenths?: number;
|
||
weightTenths?: number;
|
||
};
|
||
|
||
type SeedChronicCondition = {
|
||
elderName: string;
|
||
followUpNotes: string;
|
||
name: string;
|
||
status: "active" | "controlled" | "resolved";
|
||
treatmentNotes: string;
|
||
};
|
||
|
||
type SeedHealthReview = {
|
||
description: string;
|
||
elderName: string;
|
||
resolutionNotes: string;
|
||
severity: "info" | "warning" | "critical";
|
||
status: "pending" | "reviewed" | "resolved";
|
||
title: string;
|
||
vitalIndex: number;
|
||
};
|
||
|
||
type SeedCareTask = {
|
||
assigneeLabel: string;
|
||
careType: "daily_care" | "meal" | "medication" | "rehab" | "inspection" | "cleaning" | "other";
|
||
completedHoursAgo?: number;
|
||
elderName: string;
|
||
executionNotes: string;
|
||
priority: "low" | "normal" | "high" | "urgent";
|
||
scheduledHoursOffset: number;
|
||
status: "pending" | "in_progress" | "completed" | "cancelled";
|
||
title: string;
|
||
};
|
||
|
||
type SeedSystemIncident = {
|
||
acknowledgedHoursAgo?: number;
|
||
createdHoursAgo: number;
|
||
description: string;
|
||
resolvedHoursAgo?: number;
|
||
severity: "info" | "warning" | "critical";
|
||
source: string;
|
||
status: "open" | "acknowledged" | "resolved" | "closed";
|
||
title: string;
|
||
};
|
||
|
||
type SeedDeviceAsset = {
|
||
category: string;
|
||
code: string;
|
||
lastInspectedHoursAgo?: number;
|
||
location: string;
|
||
name: string;
|
||
notes: string;
|
||
status: "active" | "maintenance" | "disabled" | "retired";
|
||
};
|
||
|
||
type SeedMaintenanceTicket = {
|
||
assigneeLabel: string;
|
||
description: string;
|
||
deviceCode: string;
|
||
priority: "low" | "normal" | "high" | "urgent";
|
||
resolutionNotes: string;
|
||
status: "open" | "assigned" | "resolved" | "closed" | "cancelled";
|
||
title: string;
|
||
};
|
||
|
||
type SeedNotice = {
|
||
audience: string;
|
||
content: string;
|
||
publishedHoursAgo?: number;
|
||
status: "draft" | "published" | "retracted";
|
||
title: string;
|
||
};
|
||
|
||
type SeedAlertRule = {
|
||
conditionSummary: string;
|
||
name: string;
|
||
ruleType: "health" | "care" | "device" | "incident" | "admission" | "other";
|
||
severity: "info" | "warning" | "critical";
|
||
status: "enabled" | "disabled";
|
||
suggestion: string;
|
||
};
|
||
|
||
type SeedAlertTrigger = {
|
||
createdHoursAgo: number;
|
||
description: string;
|
||
elderName?: string;
|
||
handlingNotes: string;
|
||
ruleName: string;
|
||
source: string;
|
||
status: "open" | "acknowledged" | "resolved" | "closed";
|
||
title: string;
|
||
};
|
||
|
||
type SeedFamilyContact = {
|
||
elderName: string;
|
||
name: string;
|
||
notes: string;
|
||
phone: string;
|
||
relationship: string;
|
||
status: "active" | "suspended" | "archived";
|
||
};
|
||
|
||
type SeedFamilyVisit = {
|
||
contactName: string;
|
||
elderName: string;
|
||
notes: string;
|
||
scheduledHoursOffset: number;
|
||
status: "requested" | "approved" | "completed" | "cancelled";
|
||
};
|
||
|
||
type SeedFamilyFeedback = {
|
||
contactName: string;
|
||
content: string;
|
||
elderName: string;
|
||
feedbackType: "service" | "care" | "meal" | "environment" | "other";
|
||
responseNotes: string;
|
||
status: "open" | "in_progress" | "resolved" | "closed";
|
||
};
|
||
|
||
type SeedAiKnowledgeEntry = {
|
||
scope: "platform" | "organization";
|
||
title: string;
|
||
category: string;
|
||
tags: string;
|
||
body: string;
|
||
status: "enabled" | "disabled";
|
||
};
|
||
|
||
type SeedPreparedAiAnalysis = {
|
||
elderName: string;
|
||
createdHoursAgo: number;
|
||
dataScopes: string[];
|
||
result: ElderAiAnalysisOutput;
|
||
};
|
||
|
||
const DEFAULT_AI_MODEL_SUMMARY = {
|
||
provider: "openai-compatible",
|
||
chatModel: "care-analysis-v1",
|
||
knowledgeRetrieval: "keyword",
|
||
} satisfies ElderAiAnalysisOutput["modelSummary"];
|
||
|
||
const DEFAULT_ROOMS: SeedRoom[] = [
|
||
{ campusName: "主院区", buildingName: "护理楼", floorName: "一层", floorLevel: 1, code: "A101", name: "阳光房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "护理楼", floorName: "一层", floorLevel: 1, code: "A102", name: "康养房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "护理楼", floorName: "二层", floorLevel: 2, code: "A201", name: "记忆照护房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "护理楼", floorName: "二层", floorLevel: 2, code: "A202", name: "重点照护房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "护理楼", floorName: "三层", floorLevel: 3, code: "A301", name: "失能照护房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "护理楼", floorName: "三层", floorLevel: 3, code: "A302", name: "安宁照护房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "康复楼", floorName: "一层", floorLevel: 1, code: "R101", name: "康复观察房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "康复楼", floorName: "一层", floorLevel: 1, code: "R102", name: "短住评估房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "康复楼", floorName: "二层", floorLevel: 2, code: "R201", name: "术后康复房", capacity: 2 },
|
||
{ campusName: "主院区", buildingName: "康复楼", floorName: "二层", floorLevel: 2, code: "R202", name: "认知训练房", capacity: 2 },
|
||
{ campusName: "北苑院区", buildingName: "颐养楼", floorName: "一层", floorLevel: 1, code: "B101", name: "自理房", capacity: 2 },
|
||
{ campusName: "北苑院区", buildingName: "颐养楼", floorName: "二层", floorLevel: 2, code: "B201", name: "亲情套房", capacity: 2 },
|
||
{ campusName: "北苑院区", buildingName: "颐养楼", floorName: "二层", floorLevel: 2, code: "B202", name: "陪护套房", capacity: 2 },
|
||
{ campusName: "南苑院区", buildingName: "医养楼", floorName: "一层", floorLevel: 1, code: "S101", name: "医养观察房", capacity: 2 },
|
||
{ campusName: "南苑院区", buildingName: "医养楼", floorName: "一层", floorLevel: 1, code: "S102", name: "短期托养房", capacity: 2 },
|
||
];
|
||
|
||
const DEFAULT_BEDS: SeedBed[] = [
|
||
{ code: "A101-1", roomCode: "A101", status: "occupied", notes: "靠窗,已完成入住评估" },
|
||
{ code: "A101-2", roomCode: "A101", status: "available", notes: "可安排半护理老人入住" },
|
||
{ code: "A102-1", roomCode: "A102", status: "occupied", notes: "夜间巡护重点关注" },
|
||
{ code: "A102-2", roomCode: "A102", status: "maintenance", notes: "床头呼叫器检修中" },
|
||
{ code: "A201-1", roomCode: "A201", status: "occupied", notes: "记忆照护专区" },
|
||
{ code: "A201-2", roomCode: "A201", status: "available", notes: "靠近护理站" },
|
||
{ code: "A202-1", roomCode: "A202", status: "occupied", notes: "压疮风险重点巡查" },
|
||
{ code: "A202-2", roomCode: "A202", status: "disabled", notes: "待更换护栏" },
|
||
{ code: "A301-1", roomCode: "A301", status: "occupied", notes: "失能照护,配置防跌倒垫" },
|
||
{ code: "A301-2", roomCode: "A301", status: "available", notes: "鼻饲护理设备已消毒待用" },
|
||
{ code: "A302-1", roomCode: "A302", status: "available", notes: "家属陪伴预约优先" },
|
||
{ code: "A302-2", roomCode: "A302", status: "maintenance", notes: "氧气接口保养" },
|
||
{ code: "R101-1", roomCode: "R101", status: "occupied", notes: "康复训练后休息床位" },
|
||
{ code: "R101-2", roomCode: "R101", status: "available", notes: "临时观察床位" },
|
||
{ code: "R102-1", roomCode: "R102", status: "available", notes: "短住评估床位" },
|
||
{ code: "R102-2", roomCode: "R102", status: "available", notes: "短住评估床位" },
|
||
{ code: "R201-1", roomCode: "R201", status: "occupied", notes: "术后康复第 2 周" },
|
||
{ code: "R201-2", roomCode: "R201", status: "available", notes: "康复床位候补" },
|
||
{ code: "R202-1", roomCode: "R202", status: "occupied", notes: "认知训练老人长期床位" },
|
||
{ code: "R202-2", roomCode: "R202", status: "available", notes: "靠近活动室" },
|
||
{ code: "B101-1", roomCode: "B101", status: "occupied", notes: "自理老人长期床位" },
|
||
{ code: "B101-2", roomCode: "B101", status: "available", notes: "低照护需求床位" },
|
||
{ code: "B201-1", roomCode: "B201", status: "available", notes: "亲属探访便利" },
|
||
{ code: "B201-2", roomCode: "B201", status: "available", notes: "亲情套房备用床" },
|
||
{ code: "B202-1", roomCode: "B202", status: "occupied", notes: "陪护套房,家属每周探访" },
|
||
{ code: "B202-2", roomCode: "B202", status: "available", notes: "可接收短住老人" },
|
||
{ code: "S101-1", roomCode: "S101", status: "occupied", notes: "医养观察,夜间吸氧" },
|
||
{ code: "S101-2", roomCode: "S101", status: "disabled", notes: "待更换床垫" },
|
||
{ code: "S102-1", roomCode: "S102", status: "available", notes: "短期托养床位" },
|
||
{ code: "S102-2", roomCode: "S102", status: "available", notes: "短期托养床位" },
|
||
];
|
||
|
||
const DEFAULT_ELDERS: SeedElder[] = [
|
||
{
|
||
name: "王桂兰",
|
||
gender: "female",
|
||
age: 82,
|
||
careLevel: "intensive",
|
||
status: "active",
|
||
primaryContact: "张敏",
|
||
phone: "13800000001",
|
||
medicalNotes: "高血压,需晨晚血压记录。",
|
||
},
|
||
{
|
||
name: "李建国",
|
||
gender: "male",
|
||
age: 76,
|
||
careLevel: "semi-assisted",
|
||
status: "active",
|
||
primaryContact: "李明",
|
||
phone: "13800000002",
|
||
medicalNotes: "餐后散步陪同,关注睡眠情况。",
|
||
},
|
||
{
|
||
name: "周玉珍",
|
||
gender: "female",
|
||
age: 88,
|
||
careLevel: "assisted",
|
||
status: "active",
|
||
primaryContact: "周强",
|
||
phone: "13800000003",
|
||
medicalNotes: "糖尿病饮食,餐前血糖提醒。",
|
||
},
|
||
{
|
||
name: "赵国华",
|
||
gender: "male",
|
||
age: 91,
|
||
careLevel: "intensive",
|
||
status: "active",
|
||
primaryContact: "赵琳",
|
||
phone: "13800000004",
|
||
medicalNotes: "夜间翻身,两小时巡房。",
|
||
},
|
||
{
|
||
name: "孙秀梅",
|
||
gender: "female",
|
||
age: 73,
|
||
careLevel: "semi-assisted",
|
||
status: "active",
|
||
primaryContact: "孙磊",
|
||
phone: "13800000005",
|
||
medicalNotes: "膝关节康复训练,每日下午一次。",
|
||
},
|
||
{
|
||
name: "刘长青",
|
||
gender: "male",
|
||
age: 68,
|
||
careLevel: "self-care",
|
||
status: "active",
|
||
primaryContact: "刘洋",
|
||
phone: "13800000006",
|
||
medicalNotes: "自理,需定期复查用药。",
|
||
},
|
||
{
|
||
name: "陈秀英",
|
||
gender: "female",
|
||
age: 69,
|
||
careLevel: "self-care",
|
||
status: "pending",
|
||
primaryContact: "陈佳",
|
||
phone: "13800000007",
|
||
medicalNotes: "待完成入住评估。",
|
||
},
|
||
{
|
||
name: "何德明",
|
||
gender: "male",
|
||
age: 84,
|
||
careLevel: "assisted",
|
||
status: "pending",
|
||
primaryContact: "何静",
|
||
phone: "13800000008",
|
||
medicalNotes: "家属已提交体检资料,等待床位确认。",
|
||
},
|
||
{
|
||
name: "吴佩兰",
|
||
gender: "female",
|
||
age: 79,
|
||
careLevel: "semi-assisted",
|
||
status: "discharged",
|
||
primaryContact: "吴昊",
|
||
phone: "13800000009",
|
||
medicalNotes: "已退住,保留历史档案。",
|
||
},
|
||
{
|
||
name: "郑文远",
|
||
gender: "male",
|
||
age: 72,
|
||
careLevel: "self-care",
|
||
status: "discharged",
|
||
primaryContact: "郑洁",
|
||
phone: "13800000010",
|
||
medicalNotes: "短住康复结束后退住。",
|
||
},
|
||
{
|
||
name: "马淑芬",
|
||
gender: "female",
|
||
age: 86,
|
||
careLevel: "intensive",
|
||
status: "active",
|
||
primaryContact: "马晨",
|
||
phone: "13800000011",
|
||
medicalNotes: "吞咽困难,晚餐需软食并观察呛咳。",
|
||
},
|
||
{
|
||
name: "胡志强",
|
||
gender: "male",
|
||
age: 80,
|
||
careLevel: "assisted",
|
||
status: "active",
|
||
primaryContact: "胡悦",
|
||
phone: "13800000012",
|
||
medicalNotes: "术后康复,需记录每日步行距离。",
|
||
},
|
||
{
|
||
name: "高兰英",
|
||
gender: "female",
|
||
age: 77,
|
||
careLevel: "semi-assisted",
|
||
status: "active",
|
||
primaryContact: "高磊",
|
||
phone: "13800000013",
|
||
medicalNotes: "轻度认知障碍,活动后需陪同返回房间。",
|
||
},
|
||
{
|
||
name: "钱松柏",
|
||
gender: "male",
|
||
age: 89,
|
||
careLevel: "intensive",
|
||
status: "active",
|
||
primaryContact: "钱宁",
|
||
phone: "13800000014",
|
||
medicalNotes: "慢阻肺,夜间吸氧,重点关注血氧。",
|
||
},
|
||
{
|
||
name: "林玉琴",
|
||
gender: "female",
|
||
age: 71,
|
||
careLevel: "self-care",
|
||
status: "active",
|
||
primaryContact: "林涛",
|
||
phone: "13800000015",
|
||
medicalNotes: "短住托养,需协助完成初次健康档案。",
|
||
},
|
||
{
|
||
name: "曹文海",
|
||
gender: "male",
|
||
age: 83,
|
||
careLevel: "assisted",
|
||
status: "pending",
|
||
primaryContact: "曹颖",
|
||
phone: "13800000016",
|
||
medicalNotes: "已预约下周入住,等待家属确认床位。",
|
||
},
|
||
{
|
||
name: "罗海燕",
|
||
gender: "female",
|
||
age: 75,
|
||
careLevel: "semi-assisted",
|
||
status: "pending",
|
||
primaryContact: "罗杰",
|
||
phone: "13800000017",
|
||
medicalNotes: "需补充近期体检报告。",
|
||
},
|
||
{
|
||
name: "邓启明",
|
||
gender: "male",
|
||
age: 70,
|
||
careLevel: "self-care",
|
||
status: "discharged",
|
||
primaryContact: "邓可",
|
||
phone: "13800000018",
|
||
medicalNotes: "试住结束后转家庭照护。",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_ADMISSIONS: SeedAdmission[] = [
|
||
{ elderName: "王桂兰", bedCode: "A101-1", status: "active", notes: "首次入住评估已归档" },
|
||
{ elderName: "李建国", bedCode: "A102-1", status: "active", notes: "照护计划已同步至护理组" },
|
||
{ elderName: "周玉珍", bedCode: "A201-1", status: "active", notes: "记忆照护专区入住" },
|
||
{ elderName: "赵国华", bedCode: "A202-1", status: "active", notes: "重点照护入住" },
|
||
{ elderName: "孙秀梅", bedCode: "R101-1", status: "active", notes: "康复观察入住" },
|
||
{ elderName: "刘长青", bedCode: "B101-1", status: "active", notes: "自理区入住" },
|
||
{ elderName: "马淑芬", bedCode: "A301-1", status: "active", notes: "失能照护入住" },
|
||
{ elderName: "胡志强", bedCode: "R201-1", status: "active", notes: "术后康复入住" },
|
||
{ elderName: "高兰英", bedCode: "R202-1", status: "active", notes: "认知训练入住" },
|
||
{ elderName: "钱松柏", bedCode: "S101-1", status: "active", notes: "医养观察入住" },
|
||
{ elderName: "林玉琴", bedCode: "B202-1", status: "active", notes: "短住托养入住" },
|
||
{ elderName: "吴佩兰", bedCode: "B201-1", status: "discharged", notes: "历史退住记录" },
|
||
{ elderName: "邓启明", bedCode: "A301-2", status: "discharged", notes: "试住退住记录" },
|
||
];
|
||
|
||
const DEFAULT_HEALTH_PROFILES: SeedHealthProfile[] = [
|
||
{
|
||
elderName: "王桂兰",
|
||
allergyNotes: "青霉素过敏",
|
||
medicalHistory: "高血压十余年,偶发胸闷。",
|
||
medicationNotes: "晨服降压药,晚间复核血压。",
|
||
careRestrictions: "低盐饮食,避免剧烈活动。",
|
||
emergencyNotes: "胸闷或血压持续升高时立即联系医生。",
|
||
},
|
||
{
|
||
elderName: "周玉珍",
|
||
allergyNotes: "无明确药物过敏史",
|
||
medicalHistory: "2 型糖尿病,轻度认知障碍。",
|
||
medicationNotes: "餐前血糖监测,按医嘱用药。",
|
||
careRestrictions: "控糖饮食,夜间加强巡护。",
|
||
emergencyNotes: "低血糖症状时立即补糖并记录。",
|
||
},
|
||
{
|
||
elderName: "赵国华",
|
||
allergyNotes: "海鲜过敏",
|
||
medicalHistory: "脑梗后遗症,长期卧床风险。",
|
||
medicationNotes: "抗凝药按时服用。",
|
||
careRestrictions: "两小时翻身,防跌倒,防压疮。",
|
||
emergencyNotes: "意识异常或血氧下降时立即启动应急流程。",
|
||
},
|
||
{
|
||
elderName: "刘长青",
|
||
allergyNotes: "",
|
||
medicalHistory: "冠心病随访。",
|
||
medicationNotes: "自主管理用药,护理员每日提醒。",
|
||
careRestrictions: "避免爬楼和长时间站立。",
|
||
emergencyNotes: "胸痛需立即报告。",
|
||
},
|
||
{
|
||
elderName: "马淑芬",
|
||
allergyNotes: "头孢类药物过敏",
|
||
medicalHistory: "脑梗后吞咽功能下降,近期体重下降。",
|
||
medicationNotes: "晚间营养补充剂,服药后观察吞咽。",
|
||
careRestrictions: "软食,进食时保持坐位,餐后半小时内不平卧。",
|
||
emergencyNotes: "呛咳、紫绀或呼吸急促时立即通知医生。",
|
||
},
|
||
{
|
||
elderName: "胡志强",
|
||
allergyNotes: "",
|
||
medicalHistory: "髋关节术后康复中。",
|
||
medicationNotes: "按医嘱服用止痛药和抗凝药。",
|
||
careRestrictions: "转移位需双人协助,禁止独自上下楼。",
|
||
emergencyNotes: "伤口渗血或疼痛加重时联系康复医生。",
|
||
},
|
||
{
|
||
elderName: "高兰英",
|
||
allergyNotes: "无明确药物过敏史",
|
||
medicalHistory: "轻度认知障碍,夜间偶发迷路。",
|
||
medicationNotes: "睡前药物由护理员核对。",
|
||
careRestrictions: "外出活动需佩戴定位牌。",
|
||
emergencyNotes: "离开活动区超过 10 分钟未返回时启动寻人流程。",
|
||
},
|
||
{
|
||
elderName: "钱松柏",
|
||
allergyNotes: "",
|
||
medicalHistory: "慢阻肺,长期吸入治疗。",
|
||
medicationNotes: "吸入剂每日两次,夜间低流量吸氧。",
|
||
careRestrictions: "避免受凉和烟味刺激。",
|
||
emergencyNotes: "血氧低于 90 或呼吸困难时立即启动应急流程。",
|
||
},
|
||
{
|
||
elderName: "林玉琴",
|
||
allergyNotes: "无",
|
||
medicalHistory: "短住托养,既往骨质疏松。",
|
||
medicationNotes: "钙剂和维生素 D 每日提醒。",
|
||
careRestrictions: "浴室需防滑协助。",
|
||
emergencyNotes: "跌倒后即刻评估并通知家属。",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_VITAL_RECORDS: SeedVitalRecord[] = [
|
||
{
|
||
elderName: "王桂兰",
|
||
recordedHoursAgo: 2,
|
||
source: "manual",
|
||
systolicBp: 158,
|
||
diastolicBp: 96,
|
||
heartRate: 88,
|
||
temperatureTenths: 367,
|
||
spo2: 96,
|
||
bloodGlucoseTenths: 62,
|
||
weightTenths: 612,
|
||
notes: "血压偏高,已安排复测。",
|
||
},
|
||
{
|
||
elderName: "李建国",
|
||
recordedHoursAgo: 5,
|
||
source: "device",
|
||
systolicBp: 128,
|
||
diastolicBp: 78,
|
||
heartRate: 76,
|
||
temperatureTenths: 365,
|
||
spo2: 98,
|
||
notes: "体征平稳。",
|
||
},
|
||
{
|
||
elderName: "周玉珍",
|
||
recordedHoursAgo: 7,
|
||
source: "manual",
|
||
systolicBp: 134,
|
||
diastolicBp: 82,
|
||
heartRate: 80,
|
||
temperatureTenths: 366,
|
||
spo2: 97,
|
||
bloodGlucoseTenths: 118,
|
||
notes: "餐后血糖偏高,已记录饮食。",
|
||
},
|
||
{
|
||
elderName: "赵国华",
|
||
recordedHoursAgo: 12,
|
||
source: "manual",
|
||
systolicBp: 146,
|
||
diastolicBp: 88,
|
||
heartRate: 92,
|
||
temperatureTenths: 381,
|
||
spo2: 91,
|
||
notes: "体温和血氧异常,需复核。",
|
||
},
|
||
{
|
||
elderName: "马淑芬",
|
||
recordedHoursAgo: 3,
|
||
source: "manual",
|
||
systolicBp: 142,
|
||
diastolicBp: 86,
|
||
heartRate: 96,
|
||
temperatureTenths: 369,
|
||
spo2: 94,
|
||
bloodGlucoseTenths: 73,
|
||
weightTenths: 486,
|
||
notes: "晚餐前复测,吞咽情况稳定。",
|
||
},
|
||
{
|
||
elderName: "胡志强",
|
||
recordedHoursAgo: 9,
|
||
source: "device",
|
||
systolicBp: 124,
|
||
diastolicBp: 76,
|
||
heartRate: 82,
|
||
temperatureTenths: 364,
|
||
spo2: 98,
|
||
weightTenths: 705,
|
||
notes: "康复训练后体征平稳。",
|
||
},
|
||
{
|
||
elderName: "高兰英",
|
||
recordedHoursAgo: 15,
|
||
source: "manual",
|
||
systolicBp: 136,
|
||
diastolicBp: 84,
|
||
heartRate: 78,
|
||
temperatureTenths: 366,
|
||
spo2: 97,
|
||
bloodGlucoseTenths: 65,
|
||
notes: "夜间巡护记录,情绪平稳。",
|
||
},
|
||
{
|
||
elderName: "钱松柏",
|
||
recordedHoursAgo: 1,
|
||
source: "device",
|
||
systolicBp: 150,
|
||
diastolicBp: 88,
|
||
heartRate: 104,
|
||
temperatureTenths: 372,
|
||
spo2: 89,
|
||
notes: "夜间血氧低,已安排吸氧复核。",
|
||
},
|
||
{
|
||
elderName: "林玉琴",
|
||
recordedHoursAgo: 22,
|
||
source: "import",
|
||
systolicBp: 118,
|
||
diastolicBp: 72,
|
||
heartRate: 70,
|
||
temperatureTenths: 363,
|
||
spo2: 99,
|
||
bloodGlucoseTenths: 58,
|
||
weightTenths: 552,
|
||
notes: "入院体检导入记录。",
|
||
},
|
||
{
|
||
elderName: "王桂兰",
|
||
recordedHoursAgo: 26,
|
||
source: "device",
|
||
systolicBp: 145,
|
||
diastolicBp: 90,
|
||
heartRate: 84,
|
||
temperatureTenths: 366,
|
||
spo2: 97,
|
||
notes: "前一日夜间复测,血压较高。",
|
||
},
|
||
{
|
||
elderName: "周玉珍",
|
||
recordedHoursAgo: 30,
|
||
source: "manual",
|
||
systolicBp: 130,
|
||
diastolicBp: 80,
|
||
heartRate: 74,
|
||
temperatureTenths: 365,
|
||
spo2: 98,
|
||
bloodGlucoseTenths: 92,
|
||
notes: "晨间血糖记录。",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_CHRONIC_CONDITIONS: SeedChronicCondition[] = [
|
||
{
|
||
elderName: "王桂兰",
|
||
name: "高血压",
|
||
status: "active",
|
||
treatmentNotes: "规律服药,晨晚血压监测。",
|
||
followUpNotes: "每周汇总血压趋势。",
|
||
},
|
||
{
|
||
elderName: "周玉珍",
|
||
name: "糖尿病",
|
||
status: "active",
|
||
treatmentNotes: "控糖饮食,餐前餐后血糖记录。",
|
||
followUpNotes: "异常血糖进入复核队列。",
|
||
},
|
||
{
|
||
elderName: "刘长青",
|
||
name: "冠心病",
|
||
status: "controlled",
|
||
treatmentNotes: "按医嘱服药,避免劳累。",
|
||
followUpNotes: "月度复诊提醒。",
|
||
},
|
||
{
|
||
elderName: "马淑芬",
|
||
name: "吞咽功能障碍",
|
||
status: "active",
|
||
treatmentNotes: "软食和吞咽训练,餐中观察呛咳。",
|
||
followUpNotes: "每三天评估进食量和体重。",
|
||
},
|
||
{
|
||
elderName: "胡志强",
|
||
name: "髋关节术后康复",
|
||
status: "active",
|
||
treatmentNotes: "每日步态训练,逐步增加步行距离。",
|
||
followUpNotes: "康复师每周复评。",
|
||
},
|
||
{
|
||
elderName: "高兰英",
|
||
name: "轻度认知障碍",
|
||
status: "controlled",
|
||
treatmentNotes: "认知训练和日常定向提醒。",
|
||
followUpNotes: "观察夜间离床次数。",
|
||
},
|
||
{
|
||
elderName: "钱松柏",
|
||
name: "慢阻肺",
|
||
status: "active",
|
||
treatmentNotes: "吸入治疗和低流量吸氧。",
|
||
followUpNotes: "每日汇总夜间血氧趋势。",
|
||
},
|
||
{
|
||
elderName: "林玉琴",
|
||
name: "骨质疏松",
|
||
status: "controlled",
|
||
treatmentNotes: "补钙、维生素 D 和防跌倒宣教。",
|
||
followUpNotes: "短住结束前出具照护建议。",
|
||
},
|
||
{
|
||
elderName: "吴佩兰",
|
||
name: "膝关节退行性病变",
|
||
status: "resolved",
|
||
treatmentNotes: "历史康复记录归档。",
|
||
followUpNotes: "退住后无需跟进。",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_HEALTH_REVIEWS: SeedHealthReview[] = [
|
||
{
|
||
elderName: "王桂兰",
|
||
vitalIndex: 0,
|
||
severity: "warning",
|
||
status: "pending",
|
||
title: "血压异常",
|
||
description: "收缩压和舒张压偏高,需要复测并确认用药。",
|
||
resolutionNotes: "",
|
||
},
|
||
{
|
||
elderName: "赵国华",
|
||
vitalIndex: 3,
|
||
severity: "critical",
|
||
status: "pending",
|
||
title: "血氧和体温异常",
|
||
description: "血氧低于阈值且体温偏高,需要立即复核。",
|
||
resolutionNotes: "",
|
||
},
|
||
{
|
||
elderName: "周玉珍",
|
||
vitalIndex: 2,
|
||
severity: "info",
|
||
status: "reviewed",
|
||
title: "餐后血糖偏高",
|
||
description: "餐后血糖高于日常水平。",
|
||
resolutionNotes: "已调整晚餐主食并安排次日复测。",
|
||
},
|
||
{
|
||
elderName: "钱松柏",
|
||
vitalIndex: 7,
|
||
severity: "critical",
|
||
status: "pending",
|
||
title: "夜间血氧低",
|
||
description: "设备记录血氧低于 90,需要确认吸氧和呼吸状态。",
|
||
resolutionNotes: "",
|
||
},
|
||
{
|
||
elderName: "马淑芬",
|
||
vitalIndex: 4,
|
||
severity: "warning",
|
||
status: "reviewed",
|
||
title: "体重下降观察",
|
||
description: "近期体重偏低,需要结合进食量继续观察。",
|
||
resolutionNotes: "已通知营养师调整晚间加餐。",
|
||
},
|
||
{
|
||
elderName: "王桂兰",
|
||
vitalIndex: 9,
|
||
severity: "info",
|
||
status: "resolved",
|
||
title: "血压复核完成",
|
||
description: "前一日夜间血压偏高,今日已复测。",
|
||
resolutionNotes: "晨间复测仍偏高,转入本日待处理复核。",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_CARE_TASKS: SeedCareTask[] = [
|
||
{
|
||
elderName: "王桂兰",
|
||
title: "晨间协助洗漱与翻身",
|
||
careType: "daily_care",
|
||
priority: "normal",
|
||
status: "pending",
|
||
scheduledHoursOffset: 1,
|
||
assigneeLabel: "一号护理组",
|
||
executionNotes: "",
|
||
},
|
||
{
|
||
elderName: "孙秀梅",
|
||
title: "膝关节康复训练陪同",
|
||
careType: "rehab",
|
||
priority: "high",
|
||
status: "in_progress",
|
||
scheduledHoursOffset: -1,
|
||
assigneeLabel: "康复护理组",
|
||
executionNotes: "已完成热身,等待康复师复核动作。",
|
||
},
|
||
{
|
||
elderName: "周玉珍",
|
||
title: "餐前血糖记录与助餐",
|
||
careType: "meal",
|
||
priority: "normal",
|
||
status: "completed",
|
||
scheduledHoursOffset: -4,
|
||
completedHoursAgo: 3,
|
||
assigneeLabel: "二号护理组",
|
||
executionNotes: "餐前血糖已记录,午餐摄入约八成。",
|
||
},
|
||
{
|
||
elderName: "赵国华",
|
||
title: "重点照护夜间巡检补记",
|
||
careType: "inspection",
|
||
priority: "urgent",
|
||
status: "pending",
|
||
scheduledHoursOffset: -2,
|
||
assigneeLabel: "夜班护理组",
|
||
executionNotes: "",
|
||
},
|
||
{
|
||
elderName: "刘长青",
|
||
title: "用药提醒与胸痛询问",
|
||
careType: "medication",
|
||
priority: "high",
|
||
status: "pending",
|
||
scheduledHoursOffset: 3,
|
||
assigneeLabel: "三号护理组",
|
||
executionNotes: "",
|
||
},
|
||
{
|
||
elderName: "马淑芬",
|
||
title: "晚餐软食进食观察",
|
||
careType: "meal",
|
||
priority: "urgent",
|
||
status: "pending",
|
||
scheduledHoursOffset: 2,
|
||
assigneeLabel: "重点护理组",
|
||
executionNotes: "",
|
||
},
|
||
{
|
||
elderName: "胡志强",
|
||
title: "步行训练记录",
|
||
careType: "rehab",
|
||
priority: "normal",
|
||
status: "completed",
|
||
scheduledHoursOffset: -6,
|
||
completedHoursAgo: 5,
|
||
assigneeLabel: "康复护理组",
|
||
executionNotes: "扶行 80 米,未诉明显疼痛。",
|
||
},
|
||
{
|
||
elderName: "高兰英",
|
||
title: "活动室返回陪同",
|
||
careType: "daily_care",
|
||
priority: "normal",
|
||
status: "in_progress",
|
||
scheduledHoursOffset: 0,
|
||
assigneeLabel: "二号护理组",
|
||
executionNotes: "已到活动室,等待小组活动结束。",
|
||
},
|
||
{
|
||
elderName: "钱松柏",
|
||
title: "夜间吸氧设备检查",
|
||
careType: "inspection",
|
||
priority: "urgent",
|
||
status: "pending",
|
||
scheduledHoursOffset: -1,
|
||
assigneeLabel: "夜班护理组",
|
||
executionNotes: "",
|
||
},
|
||
{
|
||
elderName: "林玉琴",
|
||
title: "短住入院宣教",
|
||
careType: "other",
|
||
priority: "low",
|
||
status: "completed",
|
||
scheduledHoursOffset: -24,
|
||
completedHoursAgo: 23,
|
||
assigneeLabel: "接待护理组",
|
||
executionNotes: "已完成房间设施、防跌倒和呼叫器使用说明。",
|
||
},
|
||
{
|
||
elderName: "赵国华",
|
||
title: "床单位清洁消毒",
|
||
careType: "cleaning",
|
||
priority: "normal",
|
||
status: "cancelled",
|
||
scheduledHoursOffset: -8,
|
||
assigneeLabel: "保洁协同组",
|
||
executionNotes: "因临时复查延期,已改派夜班完成。",
|
||
},
|
||
];
|
||
|
||
const DEFAULT_SYSTEM_INCIDENTS: SeedSystemIncident[] = [
|
||
{
|
||
severity: "warning",
|
||
status: "open",
|
||
title: "床头呼叫器待检修",
|
||
description: "A102-2 床位呼叫器处于检修状态,完成后可恢复使用。",
|
||
source: "设施巡检",
|
||
createdHoursAgo: 2,
|
||
},
|
||
{
|
||
severity: "info",
|
||
status: "acknowledged",
|
||
title: "本周护理评估待完成",
|
||
description: "陈秀英、何德明等待入住评估,可在老人档案和床位工作区继续处理。",
|
||
source: "护理排班",
|
||
createdHoursAgo: 6,
|
||
acknowledgedHoursAgo: 5,
|
||
},
|
||
{
|
||
severity: "critical",
|
||
status: "resolved",
|
||
title: "夜间巡房异常已处理",
|
||
description: "重点照护房夜间巡房提醒已恢复,事件记录已归档便于后续追踪。",
|
||
source: "夜间巡房系统",
|
||
createdHoursAgo: 14,
|
||
acknowledgedHoursAgo: 13,
|
||
resolvedHoursAgo: 11,
|
||
},
|
||
{
|
||
severity: "critical",
|
||
status: "open",
|
||
title: "S101 医养观察房血氧告警",
|
||
description: "钱松柏夜间血氧持续低于阈值,需要护理组和医生复核。",
|
||
source: "生命体征设备",
|
||
createdHoursAgo: 1,
|
||
},
|
||
{
|
||
severity: "warning",
|
||
status: "acknowledged",
|
||
title: "A302 氧气接口保养延期",
|
||
description: "A302-2 氧气接口保养未按计划完成,床位暂不安排入住。",
|
||
source: "设备巡检",
|
||
createdHoursAgo: 20,
|
||
acknowledgedHoursAgo: 18,
|
||
},
|
||
{
|
||
severity: "info",
|
||
status: "closed",
|
||
title: "短住托养资料归档完成",
|
||
description: "林玉琴短住托养资料已录入并完成家属确认。",
|
||
source: "接待护理组",
|
||
createdHoursAgo: 28,
|
||
acknowledgedHoursAgo: 27,
|
||
resolvedHoursAgo: 24,
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_DEVICE_ASSETS: SeedDeviceAsset[] = [
|
||
{
|
||
name: "A102 床头呼叫器",
|
||
code: "DEV-CALL-A102-2",
|
||
category: "呼叫系统",
|
||
location: "护理楼 A102-2",
|
||
status: "maintenance",
|
||
lastInspectedHoursAgo: 6,
|
||
notes: "按键回弹异常,已临时更换备用呼叫器。",
|
||
},
|
||
{
|
||
name: "S101 血氧监测仪",
|
||
code: "DEV-SPO2-S101",
|
||
category: "生命体征设备",
|
||
location: "医养楼 S101",
|
||
status: "active",
|
||
lastInspectedHoursAgo: 3,
|
||
notes: "夜间连续监测,数据接入健康复核。",
|
||
},
|
||
{
|
||
name: "A302 氧气接口",
|
||
code: "DEV-OXY-A302-2",
|
||
category: "供氧设施",
|
||
location: "护理楼 A302-2",
|
||
status: "maintenance",
|
||
lastInspectedHoursAgo: 20,
|
||
notes: "保养延期,床位暂不安排吸氧需求老人。",
|
||
},
|
||
{
|
||
name: "活动室康复步行带",
|
||
code: "DEV-REHAB-R101",
|
||
category: "康复设备",
|
||
location: "康复楼一层活动室",
|
||
status: "active",
|
||
lastInspectedHoursAgo: 30,
|
||
notes: "适用于步态训练和扶行评估。",
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_MAINTENANCE_TICKETS: SeedMaintenanceTicket[] = [
|
||
{
|
||
deviceCode: "DEV-CALL-A102-2",
|
||
title: "床头呼叫器按键失灵",
|
||
description: "A102-2 床位呼叫器偶发无响应,需要更换按键模块。",
|
||
priority: "high",
|
||
status: "assigned",
|
||
assigneeLabel: "设备运维组",
|
||
resolutionNotes: "已备件,预计今日 17:00 前完成。",
|
||
},
|
||
{
|
||
deviceCode: "DEV-OXY-A302-2",
|
||
title: "氧气接口保养延期",
|
||
description: "A302-2 氧气接口未完成季度保养。",
|
||
priority: "urgent",
|
||
status: "open",
|
||
assigneeLabel: "后勤维修",
|
||
resolutionNotes: "",
|
||
},
|
||
{
|
||
deviceCode: "DEV-REHAB-R101",
|
||
title: "康复步行带日常校准",
|
||
description: "例行速度校准和安全带检查。",
|
||
priority: "normal",
|
||
status: "resolved",
|
||
assigneeLabel: "康复设备供应商",
|
||
resolutionNotes: "已完成速度校准,安全带状态正常。",
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_NOTICES: SeedNotice[] = [
|
||
{
|
||
title: "本周家属探访安排",
|
||
content: "周六上午开放护理楼一层和康复楼探访,请各护理组提前确认老人状态和陪同人员。",
|
||
audience: "护理组、前台接待",
|
||
status: "published",
|
||
publishedHoursAgo: 4,
|
||
},
|
||
{
|
||
title: "夜间重点巡房提醒",
|
||
content: "钱松柏、赵国华、马淑芬列入今晚重点巡房名单,请夜班护理组按时记录。",
|
||
audience: "夜班护理组",
|
||
status: "published",
|
||
publishedHoursAgo: 10,
|
||
},
|
||
{
|
||
title: "设备巡检培训材料",
|
||
content: "呼叫器、供氧接口和血氧监测仪巡检 SOP 已更新,待主管确认后发布。",
|
||
audience: "设备运维组",
|
||
status: "draft",
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_ALERT_RULES: SeedAlertRule[] = [
|
||
{
|
||
name: "血氧低值连续告警",
|
||
ruleType: "health",
|
||
severity: "critical",
|
||
status: "enabled",
|
||
conditionSummary: "设备上报血氧连续低于 92% 超过 10 分钟。",
|
||
suggestion: "通知医生复核,护理组检查吸氧设备和体位。",
|
||
},
|
||
{
|
||
name: "高优先级护理任务逾期",
|
||
ruleType: "care",
|
||
severity: "warning",
|
||
status: "enabled",
|
||
conditionSummary: "高或紧急护理任务超过计划时间仍未完成。",
|
||
suggestion: "联系责任护理组并记录延误原因。",
|
||
},
|
||
{
|
||
name: "设备维修工单未派单",
|
||
ruleType: "device",
|
||
severity: "warning",
|
||
status: "enabled",
|
||
conditionSummary: "高优先级维修工单创建后 2 小时仍处于待处理。",
|
||
suggestion: "通知设备运维组完成派单。",
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_ALERT_TRIGGERS: SeedAlertTrigger[] = [
|
||
{
|
||
ruleName: "血氧低值连续告警",
|
||
elderName: "钱松柏",
|
||
title: "S101 血氧低值需复核",
|
||
description: "夜间血氧连续低于阈值,已同步到安全应急事件。",
|
||
status: "open",
|
||
source: "生命体征设备",
|
||
handlingNotes: "",
|
||
createdHoursAgo: 1,
|
||
},
|
||
{
|
||
ruleName: "高优先级护理任务逾期",
|
||
elderName: "赵国华",
|
||
title: "重点照护夜间巡检逾期",
|
||
description: "夜间巡检补记任务已超过计划时间。",
|
||
status: "acknowledged",
|
||
source: "护理任务",
|
||
handlingNotes: "夜班主管已确认,等待补录。",
|
||
createdHoursAgo: 2,
|
||
},
|
||
{
|
||
ruleName: "设备维修工单未派单",
|
||
title: "氧气接口保养待派单",
|
||
description: "A302 氧气接口保养延期,维修工单仍待处理。",
|
||
status: "open",
|
||
source: "设备运维",
|
||
handlingNotes: "",
|
||
createdHoursAgo: 5,
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_FAMILY_CONTACTS: SeedFamilyContact[] = [
|
||
{ elderName: "王桂兰", name: "张敏", relationship: "女儿", phone: "13800000001", status: "active", notes: "主要联系人,每周六探访。" },
|
||
{ elderName: "钱松柏", name: "钱宁", relationship: "儿子", phone: "13800000011", status: "active", notes: "关注夜间吸氧情况。" },
|
||
{ elderName: "林玉琴", name: "林晓", relationship: "女儿", phone: "13800000014", status: "active", notes: "短住托养资料已确认。" },
|
||
{ elderName: "赵国华", name: "赵蕾", relationship: "孙女", phone: "13800000016", status: "active", notes: "希望接收护理反馈。" },
|
||
];
|
||
|
||
export const DEFAULT_FAMILY_VISITS: SeedFamilyVisit[] = [
|
||
{ elderName: "王桂兰", contactName: "张敏", scheduledHoursOffset: 30, status: "approved", notes: "安排护理楼一层会客区。" },
|
||
{ elderName: "钱松柏", contactName: "钱宁", scheduledHoursOffset: 8, status: "requested", notes: "需医生确认夜间血氧情况后再回复。" },
|
||
{ elderName: "林玉琴", contactName: "林晓", scheduledHoursOffset: -20, status: "completed", notes: "已完成探访和短住事项确认。" },
|
||
];
|
||
|
||
export const DEFAULT_FAMILY_FEEDBACK: SeedFamilyFeedback[] = [
|
||
{
|
||
elderName: "赵国华",
|
||
contactName: "赵蕾",
|
||
feedbackType: "care",
|
||
content: "希望夜间巡房后能同步重点观察结果。",
|
||
status: "in_progress",
|
||
responseNotes: "护理主管已跟进,今晚开始补充巡房备注。",
|
||
},
|
||
{
|
||
elderName: "林玉琴",
|
||
contactName: "林晓",
|
||
feedbackType: "environment",
|
||
content: "短住房间清洁和呼叫器说明比较清楚。",
|
||
status: "resolved",
|
||
responseNotes: "已记录为接待服务反馈。",
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_AI_KNOWLEDGE_ENTRIES: SeedAiKnowledgeEntry[] = [
|
||
{
|
||
scope: "platform",
|
||
title: "夜间低血氧处理流程",
|
||
category: "医疗护理SOP",
|
||
tags: "血氧,吸氧,夜间,慢阻肺,应急复核",
|
||
body: "夜间连续血氧低于 92% 时,护理组需先确认探头位置、手部温度和体位,再记录复测值。血氧低于 90% 或伴随呼吸困难、口唇紫绀、意识改变时,立即通知值班医生并启动应急处置。慢阻肺老人应同步检查吸氧设备、管路连接和医嘱流量,复核后在生命体征、护理任务和事件记录中闭环。",
|
||
status: "enabled",
|
||
},
|
||
{
|
||
scope: "platform",
|
||
title: "跌倒高风险住民照护要点",
|
||
category: "安全照护SOP",
|
||
tags: "防跌倒,夜间巡房,转移位,环境安全",
|
||
body: "跌倒高风险老人应保持床旁通道清晰,夜间保留低位照明,呼叫器放在伸手可及位置。转移位、如厕和康复训练需按照照护等级安排单人或双人协助;近期血压波动、认知障碍、术后康复、骨质疏松老人应提高巡视频次。发生跌倒或险情后,先评估意识、疼痛和活动能力,再通知家属并记录完整经过。",
|
||
status: "enabled",
|
||
},
|
||
{
|
||
scope: "organization",
|
||
title: "重点照护房夜间巡房规范",
|
||
category: "机构制度",
|
||
tags: "夜班,巡房,重点照护,交接班",
|
||
body: "重点照护房夜班至少每两小时巡房一次;存在血氧异常、吞咽风险、卧床压疮风险或夜间迷路风险的老人需补充专项观察。巡房记录应包含老人状态、体位、设备连接、呼叫器可用性和异常处理结果。未按计划完成的巡房任务需要夜班主管在交接班前补录原因和后续安排。",
|
||
status: "enabled",
|
||
},
|
||
{
|
||
scope: "organization",
|
||
title: "吞咽困难软食与呛咳观察",
|
||
category: "营养照护",
|
||
tags: "吞咽,软食,呛咳,营养,体重",
|
||
body: "吞咽困难老人进食时保持坐位,餐中小口慢食,避免催促。软食、增稠液和营养补充剂按医嘱执行,餐后半小时内不平卧。出现连续呛咳、声音湿哑、口唇发紫、进食量下降或体重持续下降时,应通知医生、营养师和家属,并将观察结果同步到护理任务。",
|
||
status: "enabled",
|
||
},
|
||
{
|
||
scope: "organization",
|
||
title: "异常情况家属同步规则",
|
||
category: "服务沟通",
|
||
tags: "家属沟通,异常同步,护理反馈,探访",
|
||
body: "涉及跌倒、血氧异常、发热、连续拒食、护理任务明显延误或设备故障影响照护时,责任护理组应在处置完成后同步家属。沟通内容包含已发现的问题、已采取的措施、下一步观察计划和预计反馈时间;对仍在处理中事项,应在交接班中标注负责人,避免多次解释不一致。",
|
||
status: "enabled",
|
||
},
|
||
];
|
||
|
||
export const DEFAULT_PREPARED_AI_ANALYSES: SeedPreparedAiAnalysis[] = [
|
||
{
|
||
elderName: "钱松柏",
|
||
createdHoursAgo: 1,
|
||
dataScopes: ["elder", "health", "care", "incident", "facility", "knowledge"],
|
||
result: {
|
||
overallRiskLevel: "critical",
|
||
summary: "钱松柏夜间血氧低于 90%,且既往有慢阻肺和夜间吸氧记录,应优先完成呼吸状态复核、吸氧设备检查和医生评估。",
|
||
keyFindings: [
|
||
{
|
||
category: "呼吸风险",
|
||
severity: "critical",
|
||
evidence: "最新设备记录显示夜间血氧 89%,慢阻肺病史和长期吸入治疗增加夜间低氧风险。",
|
||
citationIds: ["resident-qsb-vitals", "kb-night-spo2"],
|
||
},
|
||
{
|
||
category: "设备闭环",
|
||
severity: "warning",
|
||
evidence: "系统事件已记录 S101 医养观察房血氧告警,需要确认吸氧设备、管路和医嘱流量。",
|
||
citationIds: ["resident-qsb-incident", "kb-night-spo2"],
|
||
},
|
||
],
|
||
recommendations: [
|
||
{
|
||
title: "立即复测血氧并通知值班医生",
|
||
priority: "urgent",
|
||
rationale: "血氧低于 90% 属于需要即时复核的高风险信号,且老人存在慢阻肺基础病。",
|
||
suggestedNextStep: "夜班护理组先复测血氧、检查吸氧设备和体位,再将复核结果同步给值班医生。",
|
||
citationIds: ["resident-qsb-vitals", "kb-night-spo2"],
|
||
},
|
||
{
|
||
title: "把低氧事件纳入交接班追踪",
|
||
priority: "high",
|
||
rationale: "告警、设备检查和医疗复核需要跨班次闭环,避免只完成单点处置。",
|
||
suggestedNextStep: "在护理任务中补充复测时间、吸氧流量确认结果和下一次观察时间。",
|
||
citationIds: ["resident-qsb-incident"],
|
||
},
|
||
],
|
||
dataGaps: ["缺少最近一次医生复核结论和吸氧流量确认记录。"],
|
||
citations: [
|
||
{
|
||
id: "resident-qsb-vitals",
|
||
sourceType: "resident_context",
|
||
sourceId: "elder:钱松柏:vitals",
|
||
title: "钱松柏生命体征与慢病记录",
|
||
excerpt: "慢阻肺,夜间吸氧;最新血氧记录 89%,心率 104,需关注呼吸状态。",
|
||
},
|
||
{
|
||
id: "resident-qsb-incident",
|
||
sourceType: "resident_context",
|
||
sourceId: "elder:钱松柏:incident",
|
||
title: "S101 医养观察房血氧告警",
|
||
excerpt: "钱松柏夜间血氧持续低于阈值,需要护理组和医生复核。",
|
||
},
|
||
{
|
||
id: "kb-night-spo2",
|
||
sourceType: "knowledge",
|
||
sourceId: "夜间低血氧处理流程",
|
||
title: "夜间低血氧处理流程",
|
||
excerpt: "血氧低于 90% 或伴随呼吸困难、口唇紫绀、意识改变时,立即通知值班医生并启动应急处置。",
|
||
},
|
||
],
|
||
confidence: 0.86,
|
||
modelSummary: DEFAULT_AI_MODEL_SUMMARY,
|
||
},
|
||
},
|
||
{
|
||
elderName: "马淑芬",
|
||
createdHoursAgo: 3,
|
||
dataScopes: ["elder", "health", "care", "knowledge"],
|
||
result: {
|
||
overallRiskLevel: "high",
|
||
summary: "马淑芬存在吞咽困难、近期体重下降和晚餐软食观察任务,当前重点是防呛咳、保证摄入量并完成营养师复评。",
|
||
keyFindings: [
|
||
{
|
||
category: "吞咽与营养",
|
||
severity: "warning",
|
||
evidence: "健康档案记录脑梗后吞咽功能下降、近期体重下降;护理计划要求晚餐软食并观察呛咳。",
|
||
citationIds: ["resident-msf-health", "kb-swallowing"],
|
||
},
|
||
{
|
||
category: "护理优先级",
|
||
severity: "warning",
|
||
evidence: "晚餐软食进食观察任务为紧急优先级,需在进食时段完成并记录摄入情况。",
|
||
citationIds: ["resident-msf-care"],
|
||
},
|
||
],
|
||
recommendations: [
|
||
{
|
||
title: "进食时段安排专人观察",
|
||
priority: "high",
|
||
rationale: "吞咽风险与体重下降同时出现,餐中观察比事后补记更能降低呛咳风险。",
|
||
suggestedNextStep: "重点护理组在晚餐时记录坐位、进食量、呛咳情况和餐后半小时体位。",
|
||
citationIds: ["resident-msf-care", "kb-swallowing"],
|
||
},
|
||
{
|
||
title: "同步营养师复评体重趋势",
|
||
priority: "normal",
|
||
rationale: "近期体重偏低已进入复核,需结合摄入量决定是否调整加餐。",
|
||
suggestedNextStep: "汇总三天进食量、体重和呛咳观察后交由营养师复评。",
|
||
citationIds: ["resident-msf-health"],
|
||
},
|
||
],
|
||
dataGaps: ["缺少最近三天完整进食量记录。"],
|
||
citations: [
|
||
{
|
||
id: "resident-msf-health",
|
||
sourceType: "resident_context",
|
||
sourceId: "elder:马淑芬:health",
|
||
title: "马淑芬健康档案",
|
||
excerpt: "脑梗后吞咽功能下降,近期体重下降;软食,进食时保持坐位,餐后半小时内不平卧。",
|
||
},
|
||
{
|
||
id: "resident-msf-care",
|
||
sourceType: "resident_context",
|
||
sourceId: "elder:马淑芬:care",
|
||
title: "晚餐软食进食观察任务",
|
||
excerpt: "晚餐软食进食观察为紧急护理任务,需记录进食和呛咳情况。",
|
||
},
|
||
{
|
||
id: "kb-swallowing",
|
||
sourceType: "knowledge",
|
||
sourceId: "吞咽困难软食与呛咳观察",
|
||
title: "吞咽困难软食与呛咳观察",
|
||
excerpt: "吞咽困难老人进食时保持坐位,餐中小口慢食;出现连续呛咳、声音湿哑或进食量下降时应同步医生、营养师和家属。",
|
||
},
|
||
],
|
||
confidence: 0.8,
|
||
modelSummary: DEFAULT_AI_MODEL_SUMMARY,
|
||
},
|
||
},
|
||
{
|
||
elderName: "王桂兰",
|
||
createdHoursAgo: 5,
|
||
dataScopes: ["elder", "health", "care", "family", "knowledge"],
|
||
result: {
|
||
overallRiskLevel: "medium",
|
||
summary: "王桂兰近期血压偏高但记录链路完整,建议继续晨晚复测、观察胸闷症状,并在家属探访前同步照护重点。",
|
||
keyFindings: [
|
||
{
|
||
category: "血压趋势",
|
||
severity: "warning",
|
||
evidence: "近期生命体征显示血压偏高,健康档案要求晨晚血压记录并在胸闷时立即联系医生。",
|
||
citationIds: ["resident-wgl-vitals"],
|
||
},
|
||
{
|
||
category: "家属沟通",
|
||
severity: "info",
|
||
evidence: "主要联系人每周六探访,可在探访前同步血压复测和低盐饮食执行情况。",
|
||
citationIds: ["resident-wgl-family", "kb-family-sync"],
|
||
},
|
||
],
|
||
recommendations: [
|
||
{
|
||
title: "保持晨晚血压复测",
|
||
priority: "normal",
|
||
rationale: "当前为持续偏高而非单次异常,连续趋势比单点数值更适合指导照护安排。",
|
||
suggestedNextStep: "护理组在晨晚复测后记录数值、用药时间和是否胸闷。",
|
||
citationIds: ["resident-wgl-vitals"],
|
||
},
|
||
{
|
||
title: "探访前同步照护重点",
|
||
priority: "low",
|
||
rationale: "家属是主要联系人,提前同步可减少探访时的信息差。",
|
||
suggestedNextStep: "前台或责任护理员在探访前准备血压趋势、饮食执行和护理安排摘要。",
|
||
citationIds: ["resident-wgl-family", "kb-family-sync"],
|
||
},
|
||
],
|
||
dataGaps: [],
|
||
citations: [
|
||
{
|
||
id: "resident-wgl-vitals",
|
||
sourceType: "resident_context",
|
||
sourceId: "elder:王桂兰:vitals",
|
||
title: "王桂兰生命体征与健康档案",
|
||
excerpt: "高血压十余年;最新血压偏高,需晨晚血压记录,胸闷或血压持续升高时立即联系医生。",
|
||
},
|
||
{
|
||
id: "resident-wgl-family",
|
||
sourceType: "resident_context",
|
||
sourceId: "elder:王桂兰:family",
|
||
title: "王桂兰家属联系人",
|
||
excerpt: "主要联系人张敏,每周六探访。",
|
||
},
|
||
{
|
||
id: "kb-family-sync",
|
||
sourceType: "knowledge",
|
||
sourceId: "异常情况家属同步规则",
|
||
title: "异常情况家属同步规则",
|
||
excerpt: "沟通内容包含已发现的问题、已采取的措施、下一步观察计划和预计反馈时间。",
|
||
},
|
||
],
|
||
confidence: 0.74,
|
||
modelSummary: DEFAULT_AI_MODEL_SUMMARY,
|
||
},
|
||
},
|
||
];
|
||
|
||
function buildSeedKnowledgeContent(entry: SeedAiKnowledgeEntry): string {
|
||
return [entry.title, entry.category, entry.tags, entry.body].filter(Boolean).join("\n");
|
||
}
|
||
|
||
function getSeedKnowledgeKey(entry: Pick<SeedAiKnowledgeEntry, "scope" | "title">): string {
|
||
return `${entry.scope}:${entry.title}`;
|
||
}
|
||
|
||
async function seedDefaultAiWorkspaceData(organizationId: string): Promise<void> {
|
||
const database = getDatabase();
|
||
const [existingKnowledgeRows, elderRows, existingAnalysisRows] = await Promise.all([
|
||
database
|
||
.select({ scope: aiKnowledgeEntries.scope, title: aiKnowledgeEntries.title })
|
||
.from(aiKnowledgeEntries)
|
||
.where(
|
||
or(
|
||
and(eq(aiKnowledgeEntries.scope, "platform"), isNull(aiKnowledgeEntries.organizationId)),
|
||
and(eq(aiKnowledgeEntries.scope, "organization"), eq(aiKnowledgeEntries.organizationId, organizationId)),
|
||
),
|
||
),
|
||
database.select({ id: elders.id, name: elders.name }).from(elders).where(eq(elders.organizationId, organizationId)),
|
||
database.select({ elderId: elderAiAnalyses.elderId }).from(elderAiAnalyses).where(eq(elderAiAnalyses.organizationId, organizationId)),
|
||
]);
|
||
|
||
const existingKnowledgeKeys = new Set(existingKnowledgeRows.map(getSeedKnowledgeKey));
|
||
const missingKnowledgeEntries = DEFAULT_AI_KNOWLEDGE_ENTRIES.filter((entry) => !existingKnowledgeKeys.has(getSeedKnowledgeKey(entry)));
|
||
const elderIdByName = new Map(elderRows.map((elder) => [elder.name, elder.id]));
|
||
const analyzedElderIds = new Set(existingAnalysisRows.map((analysis) => analysis.elderId));
|
||
const missingAnalyses = DEFAULT_PREPARED_AI_ANALYSES.filter((analysis) => {
|
||
const elderId = elderIdByName.get(analysis.elderName);
|
||
return elderId !== undefined && !analyzedElderIds.has(elderId);
|
||
});
|
||
|
||
if (missingKnowledgeEntries.length === 0 && missingAnalyses.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const now = new Date();
|
||
await database.transaction(async (transaction) => {
|
||
if (missingKnowledgeEntries.length > 0) {
|
||
const knowledgeRows = await transaction
|
||
.insert(aiKnowledgeEntries)
|
||
.values(
|
||
missingKnowledgeEntries.map((entry) => ({
|
||
scope: entry.scope,
|
||
organizationId: entry.scope === "organization" ? organizationId : null,
|
||
title: entry.title,
|
||
category: entry.category,
|
||
tags: entry.tags,
|
||
body: entry.body,
|
||
status: entry.status,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
})),
|
||
)
|
||
.returning();
|
||
if (knowledgeRows.length !== missingKnowledgeEntries.length) {
|
||
throw new Error("AI 知识库初始化失败");
|
||
}
|
||
|
||
const sourceByKey = new Map(missingKnowledgeEntries.map((entry) => [getSeedKnowledgeKey(entry), entry]));
|
||
await transaction.insert(aiKnowledgeChunks).values(
|
||
knowledgeRows.map((row) => {
|
||
const source = sourceByKey.get(getSeedKnowledgeKey(row));
|
||
if (!source) {
|
||
throw new Error("AI 知识库分块初始化失败");
|
||
}
|
||
|
||
return {
|
||
entryId: row.id,
|
||
organizationId: row.organizationId,
|
||
scope: row.scope,
|
||
chunkIndex: 0,
|
||
content: buildSeedKnowledgeContent(source),
|
||
embedding: [],
|
||
sourceTitle: row.title,
|
||
sourceCategory: row.category,
|
||
createdAt: now,
|
||
};
|
||
}),
|
||
);
|
||
}
|
||
|
||
if (missingAnalyses.length > 0) {
|
||
await transaction.insert(elderAiAnalyses).values(
|
||
missingAnalyses.map((analysis) => {
|
||
const elderId = elderIdByName.get(analysis.elderName);
|
||
if (!elderId) {
|
||
throw new Error("AI 分析记录初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
status: "completed" as const,
|
||
dataScopes: analysis.dataScopes,
|
||
resultJson: analysis.result,
|
||
citationsJson: analysis.result.citations,
|
||
modelSummaryJson: analysis.result.modelSummary,
|
||
createdAt: new Date(now.getTime() - analysis.createdHoursAgo * 60 * 60 * 1000),
|
||
};
|
||
}),
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
function getFamilyContactKey(elderId: string, contactName: string): string {
|
||
return `${elderId}:${contactName}`;
|
||
}
|
||
|
||
function getFamilyVisitKey(elderId: string, contactId: string, notes: string): string {
|
||
return `${elderId}:${contactId}:${notes}`;
|
||
}
|
||
|
||
function getFamilyFeedbackKey(elderId: string, contactId: string, content: string): string {
|
||
return `${elderId}:${contactId}:${content}`;
|
||
}
|
||
|
||
export async function seedDefaultCollaborationWorkspaceData(organizationId: string): Promise<void> {
|
||
const database = getDatabase();
|
||
const [
|
||
elderRows,
|
||
existingDeviceRows,
|
||
existingTicketRows,
|
||
existingNoticeRows,
|
||
existingAlertRuleRows,
|
||
existingAlertTriggerRows,
|
||
existingFamilyContactRows,
|
||
existingFamilyVisitRows,
|
||
existingFamilyFeedbackRows,
|
||
] = await Promise.all([
|
||
database.select({ id: elders.id, name: elders.name }).from(elders).where(eq(elders.organizationId, organizationId)),
|
||
database.select().from(deviceAssets).where(eq(deviceAssets.organizationId, organizationId)),
|
||
database.select().from(maintenanceTickets).where(eq(maintenanceTickets.organizationId, organizationId)),
|
||
database.select().from(notices).where(eq(notices.organizationId, organizationId)),
|
||
database.select().from(alertRules).where(eq(alertRules.organizationId, organizationId)),
|
||
database.select().from(alertTriggers).where(eq(alertTriggers.organizationId, organizationId)),
|
||
database.select().from(familyContacts).where(eq(familyContacts.organizationId, organizationId)),
|
||
database.select().from(familyVisitAppointments).where(eq(familyVisitAppointments.organizationId, organizationId)),
|
||
database.select().from(familyFeedback).where(eq(familyFeedback.organizationId, organizationId)),
|
||
]);
|
||
|
||
const elderIdByName = new Map(elderRows.map((elder) => [elder.name, elder.id]));
|
||
const existingDeviceCodes = new Set(existingDeviceRows.map((device) => device.code));
|
||
const existingTicketTitles = new Set(existingTicketRows.map((ticket) => ticket.title));
|
||
const existingNoticeTitles = new Set(existingNoticeRows.map((notice) => notice.title));
|
||
const existingAlertRuleNames = new Set(existingAlertRuleRows.map((rule) => rule.name));
|
||
const existingAlertTriggerTitles = new Set(existingAlertTriggerRows.map((trigger) => trigger.title));
|
||
const existingFamilyContactKeys = new Set(existingFamilyContactRows.map((contact) => getFamilyContactKey(contact.elderId, contact.name)));
|
||
const existingFamilyVisitKeys = new Set(
|
||
existingFamilyVisitRows
|
||
.map((visit) => (visit.contactId ? getFamilyVisitKey(visit.elderId, visit.contactId, visit.notes) : ""))
|
||
.filter(Boolean),
|
||
);
|
||
const existingFamilyFeedbackKeys = new Set(
|
||
existingFamilyFeedbackRows
|
||
.map((feedback) => (feedback.contactId ? getFamilyFeedbackKey(feedback.elderId, feedback.contactId, feedback.content) : ""))
|
||
.filter(Boolean),
|
||
);
|
||
|
||
const getContactId = (contactRows: typeof existingFamilyContactRows, elderId: string, contactName: string): string | undefined =>
|
||
contactRows.find((contact) => contact.elderId === elderId && contact.name === contactName)?.id;
|
||
const getMissingFamilyVisits = (contactRows: typeof existingFamilyContactRows): SeedFamilyVisit[] =>
|
||
DEFAULT_FAMILY_VISITS.filter((visit) => {
|
||
const elderId = elderIdByName.get(visit.elderName);
|
||
const contactId = elderId ? getContactId(contactRows, elderId, visit.contactName) : undefined;
|
||
return Boolean(elderId && contactId && !existingFamilyVisitKeys.has(getFamilyVisitKey(elderId, contactId, visit.notes)));
|
||
});
|
||
const getMissingFamilyFeedback = (contactRows: typeof existingFamilyContactRows): SeedFamilyFeedback[] =>
|
||
DEFAULT_FAMILY_FEEDBACK.filter((feedback) => {
|
||
const elderId = elderIdByName.get(feedback.elderName);
|
||
const contactId = elderId ? getContactId(contactRows, elderId, feedback.contactName) : undefined;
|
||
return Boolean(elderId && contactId && !existingFamilyFeedbackKeys.has(getFamilyFeedbackKey(elderId, contactId, feedback.content)));
|
||
});
|
||
|
||
const missingDevices = DEFAULT_DEVICE_ASSETS.filter((device) => !existingDeviceCodes.has(device.code));
|
||
const missingTickets = DEFAULT_MAINTENANCE_TICKETS.filter((ticket) => !existingTicketTitles.has(ticket.title));
|
||
const missingNotices = DEFAULT_NOTICES.filter((notice) => !existingNoticeTitles.has(notice.title));
|
||
const missingAlertRules = DEFAULT_ALERT_RULES.filter((rule) => !existingAlertRuleNames.has(rule.name));
|
||
const missingAlertTriggers = DEFAULT_ALERT_TRIGGERS.filter((trigger) => !existingAlertTriggerTitles.has(trigger.title));
|
||
const missingFamilyContacts = DEFAULT_FAMILY_CONTACTS.filter((contact) => {
|
||
const elderId = elderIdByName.get(contact.elderName);
|
||
return Boolean(elderId && !existingFamilyContactKeys.has(getFamilyContactKey(elderId, contact.name)));
|
||
});
|
||
const missingFamilyVisits = getMissingFamilyVisits(existingFamilyContactRows);
|
||
const missingFamilyFeedback = getMissingFamilyFeedback(existingFamilyContactRows);
|
||
|
||
if (
|
||
missingDevices.length === 0 &&
|
||
missingTickets.length === 0 &&
|
||
missingNotices.length === 0 &&
|
||
missingAlertRules.length === 0 &&
|
||
missingAlertTriggers.length === 0 &&
|
||
missingFamilyContacts.length === 0 &&
|
||
missingFamilyVisits.length === 0 &&
|
||
missingFamilyFeedback.length === 0
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const now = new Date();
|
||
await database.transaction(async (transaction) => {
|
||
const insertedDeviceRows = missingDevices.length > 0
|
||
? await transaction
|
||
.insert(deviceAssets)
|
||
.values(
|
||
missingDevices.map((device) => ({
|
||
organizationId,
|
||
name: device.name,
|
||
code: device.code,
|
||
category: device.category,
|
||
location: device.location,
|
||
status: device.status,
|
||
lastInspectedAt: device.lastInspectedHoursAgo === undefined ? undefined : new Date(now.getTime() - device.lastInspectedHoursAgo * 60 * 60 * 1000),
|
||
notes: device.notes,
|
||
})),
|
||
)
|
||
.returning()
|
||
: [];
|
||
if (insertedDeviceRows.length !== missingDevices.length) {
|
||
throw new Error("默认设备台账初始化失败");
|
||
}
|
||
const deviceIdByCode = new Map([...existingDeviceRows, ...insertedDeviceRows].map((device) => [device.code, device.id]));
|
||
|
||
if (missingTickets.length > 0) {
|
||
const ticketValues = missingTickets.map((ticket) => {
|
||
const deviceId = deviceIdByCode.get(ticket.deviceCode);
|
||
if (!deviceId) {
|
||
throw new Error("默认维修工单初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
deviceId,
|
||
title: ticket.title,
|
||
description: ticket.description,
|
||
priority: ticket.priority,
|
||
status: ticket.status,
|
||
assigneeLabel: ticket.assigneeLabel,
|
||
resolutionNotes: ticket.resolutionNotes,
|
||
resolvedAt: ticket.status === "resolved" || ticket.status === "closed" ? now : undefined,
|
||
};
|
||
});
|
||
await transaction.insert(maintenanceTickets).values(ticketValues);
|
||
}
|
||
|
||
if (missingNotices.length > 0) {
|
||
await transaction.insert(notices).values(
|
||
missingNotices.map((notice) => ({
|
||
organizationId,
|
||
title: notice.title,
|
||
content: notice.content,
|
||
audience: notice.audience,
|
||
status: notice.status,
|
||
publishedAt: notice.publishedHoursAgo === undefined ? undefined : new Date(now.getTime() - notice.publishedHoursAgo * 60 * 60 * 1000),
|
||
})),
|
||
);
|
||
}
|
||
|
||
const insertedAlertRuleRows = missingAlertRules.length > 0
|
||
? await transaction.insert(alertRules).values(missingAlertRules.map((rule) => ({ ...rule, organizationId }))).returning()
|
||
: [];
|
||
if (insertedAlertRuleRows.length !== missingAlertRules.length) {
|
||
throw new Error("默认预警规则初始化失败");
|
||
}
|
||
const alertRuleIdByName = new Map([...existingAlertRuleRows, ...insertedAlertRuleRows].map((rule) => [rule.name, rule.id]));
|
||
|
||
const alertTriggerValues = missingAlertTriggers
|
||
.map((trigger) => {
|
||
const ruleId = alertRuleIdByName.get(trigger.ruleName);
|
||
const elderId = trigger.elderName ? elderIdByName.get(trigger.elderName) : undefined;
|
||
if (!ruleId || (trigger.elderName && !elderId)) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
ruleId,
|
||
elderId,
|
||
title: trigger.title,
|
||
description: trigger.description,
|
||
status: trigger.status,
|
||
source: trigger.source,
|
||
handlingNotes: trigger.handlingNotes,
|
||
handledAt: trigger.status === "open" ? undefined : now,
|
||
createdAt: new Date(now.getTime() - trigger.createdHoursAgo * 60 * 60 * 1000),
|
||
updatedAt: now,
|
||
};
|
||
})
|
||
.filter((value): value is Exclude<typeof value, null> => value !== null);
|
||
if (alertTriggerValues.length > 0) {
|
||
await transaction.insert(alertTriggers).values(alertTriggerValues);
|
||
}
|
||
|
||
const insertedFamilyContactRows = missingFamilyContacts.length > 0
|
||
? await transaction
|
||
.insert(familyContacts)
|
||
.values(
|
||
missingFamilyContacts.map((contact) => {
|
||
const elderId = elderIdByName.get(contact.elderName);
|
||
if (!elderId) {
|
||
throw new Error("默认家属联系人初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
name: contact.name,
|
||
relationship: contact.relationship,
|
||
phone: contact.phone,
|
||
status: contact.status,
|
||
notes: contact.notes,
|
||
};
|
||
}),
|
||
)
|
||
.returning()
|
||
: [];
|
||
if (insertedFamilyContactRows.length !== missingFamilyContacts.length) {
|
||
throw new Error("默认家属联系人初始化失败");
|
||
}
|
||
const allFamilyContactRows = [...existingFamilyContactRows, ...insertedFamilyContactRows];
|
||
|
||
const familyVisitValues = getMissingFamilyVisits(allFamilyContactRows)
|
||
.map((visit) => {
|
||
const elderId = elderIdByName.get(visit.elderName);
|
||
const contactId = elderId ? getContactId(allFamilyContactRows, elderId, visit.contactName) : undefined;
|
||
if (!elderId || !contactId) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
contactId,
|
||
scheduledAt: new Date(now.getTime() + visit.scheduledHoursOffset * 60 * 60 * 1000),
|
||
status: visit.status,
|
||
notes: visit.notes,
|
||
handledAt: visit.status === "requested" ? undefined : now,
|
||
};
|
||
})
|
||
.filter((value): value is Exclude<typeof value, null> => value !== null);
|
||
if (familyVisitValues.length > 0) {
|
||
await transaction.insert(familyVisitAppointments).values(familyVisitValues);
|
||
}
|
||
|
||
const familyFeedbackValues = getMissingFamilyFeedback(allFamilyContactRows)
|
||
.map((feedback) => {
|
||
const elderId = elderIdByName.get(feedback.elderName);
|
||
const contactId = elderId ? getContactId(allFamilyContactRows, elderId, feedback.contactName) : undefined;
|
||
if (!elderId || !contactId) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
contactId,
|
||
feedbackType: feedback.feedbackType,
|
||
content: feedback.content,
|
||
status: feedback.status,
|
||
responseNotes: feedback.responseNotes,
|
||
handledAt: feedback.status === "open" ? undefined : now,
|
||
};
|
||
})
|
||
.filter((value): value is Exclude<typeof value, null> => value !== null);
|
||
if (familyFeedbackValues.length > 0) {
|
||
await transaction.insert(familyFeedback).values(familyFeedbackValues);
|
||
}
|
||
});
|
||
}
|
||
|
||
function hasRows(rows: unknown[]): boolean {
|
||
return rows.length > 0;
|
||
}
|
||
|
||
export async function seedDefaultWorkspaceData(organizationId: string): Promise<void> {
|
||
const database = getDatabase();
|
||
const [existingRooms, existingBeds, existingElders, existingAdmissions] = await Promise.all([
|
||
database.select({ id: rooms.id }).from(rooms).where(eq(rooms.organizationId, organizationId)).limit(1),
|
||
database.select({ id: beds.id }).from(beds).where(eq(beds.organizationId, organizationId)).limit(1),
|
||
database.select({ id: elders.id }).from(elders).where(eq(elders.organizationId, organizationId)).limit(1),
|
||
database.select({ id: admissions.id }).from(admissions).where(eq(admissions.organizationId, organizationId)).limit(1),
|
||
]);
|
||
|
||
if ([existingRooms, existingBeds, existingElders, existingAdmissions].some(hasRows)) {
|
||
await seedDefaultCollaborationWorkspaceData(organizationId);
|
||
await seedDefaultAiWorkspaceData(organizationId);
|
||
return;
|
||
}
|
||
|
||
await database.transaction(async (transaction) => {
|
||
const uniqueCampusNames = Array.from(new Set(DEFAULT_ROOMS.map((room) => room.campusName)));
|
||
const campusRows = await transaction
|
||
.insert(campuses)
|
||
.values(
|
||
uniqueCampusNames.map((name) => ({
|
||
organizationId,
|
||
name,
|
||
address: `${name}养老服务院区`,
|
||
})),
|
||
)
|
||
.returning();
|
||
if (campusRows.length !== uniqueCampusNames.length) {
|
||
throw new Error("默认院区初始化失败");
|
||
}
|
||
const campusIdByName = new Map(campusRows.map((campus) => [campus.name, campus.id]));
|
||
|
||
const uniqueBuildings = Array.from(
|
||
new Map(DEFAULT_ROOMS.map((room) => [`${room.campusName}/${room.buildingName}`, room])).values(),
|
||
);
|
||
const buildingRows = await transaction
|
||
.insert(buildings)
|
||
.values(
|
||
uniqueBuildings.map((building) => {
|
||
const campusId = campusIdByName.get(building.campusName);
|
||
if (!campusId) {
|
||
throw new Error("默认楼栋初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
campusId,
|
||
name: building.buildingName,
|
||
};
|
||
}),
|
||
)
|
||
.returning();
|
||
if (buildingRows.length !== uniqueBuildings.length) {
|
||
throw new Error("默认楼栋初始化失败");
|
||
}
|
||
const buildingIdByKey = new Map(
|
||
buildingRows.map((building, index) => {
|
||
const source = uniqueBuildings[index];
|
||
if (!source) {
|
||
throw new Error("默认楼栋初始化失败");
|
||
}
|
||
|
||
return [`${source.campusName}/${source.buildingName}`, building.id];
|
||
}),
|
||
);
|
||
|
||
const uniqueFloors = Array.from(
|
||
new Map(
|
||
DEFAULT_ROOMS.map((room) => [
|
||
`${room.campusName}/${room.buildingName}/${room.floorLevel}`,
|
||
room,
|
||
]),
|
||
).values(),
|
||
);
|
||
const floorRows = await transaction
|
||
.insert(floors)
|
||
.values(
|
||
uniqueFloors.map((floor) => {
|
||
const buildingId = buildingIdByKey.get(`${floor.campusName}/${floor.buildingName}`);
|
||
if (!buildingId) {
|
||
throw new Error("默认楼层初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
buildingId,
|
||
name: floor.floorName,
|
||
level: floor.floorLevel,
|
||
};
|
||
}),
|
||
)
|
||
.returning();
|
||
if (floorRows.length !== uniqueFloors.length) {
|
||
throw new Error("默认楼层初始化失败");
|
||
}
|
||
const floorIdByKey = new Map(
|
||
floorRows.map((floor, index) => {
|
||
const source = uniqueFloors[index];
|
||
if (!source) {
|
||
throw new Error("默认楼层初始化失败");
|
||
}
|
||
|
||
return [`${source.campusName}/${source.buildingName}/${source.floorLevel}`, floor.id];
|
||
}),
|
||
);
|
||
|
||
const roomRows = await transaction
|
||
.insert(rooms)
|
||
.values(
|
||
DEFAULT_ROOMS.map((room) => {
|
||
const floorId = floorIdByKey.get(`${room.campusName}/${room.buildingName}/${room.floorLevel}`);
|
||
if (!floorId) {
|
||
throw new Error("默认房间初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
floorId,
|
||
name: room.name,
|
||
code: room.code,
|
||
capacity: room.capacity,
|
||
};
|
||
}),
|
||
)
|
||
.returning();
|
||
if (roomRows.length !== DEFAULT_ROOMS.length) {
|
||
throw new Error("默认房间初始化失败");
|
||
}
|
||
|
||
const roomIdByCode = new Map(roomRows.map((room) => [room.code, room.id]));
|
||
const bedRows = await transaction
|
||
.insert(beds)
|
||
.values(
|
||
DEFAULT_BEDS.map((bed) => {
|
||
const roomId = roomIdByCode.get(bed.roomCode);
|
||
if (!roomId) {
|
||
throw new Error("默认床位初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
roomId,
|
||
code: bed.code,
|
||
status: bed.status,
|
||
notes: bed.notes,
|
||
};
|
||
}),
|
||
)
|
||
.returning();
|
||
if (bedRows.length !== DEFAULT_BEDS.length) {
|
||
throw new Error("默认床位初始化失败");
|
||
}
|
||
|
||
const elderRows = await transaction
|
||
.insert(elders)
|
||
.values(DEFAULT_ELDERS.map((elder) => ({ ...elder, organizationId })))
|
||
.returning();
|
||
if (elderRows.length !== DEFAULT_ELDERS.length) {
|
||
throw new Error("默认老人档案初始化失败");
|
||
}
|
||
|
||
const elderIdByName = new Map(elderRows.map((elder) => [elder.name, elder.id]));
|
||
const bedIdByCode = new Map(bedRows.map((bed) => [bed.code, bed.id]));
|
||
await transaction.insert(admissions).values(
|
||
DEFAULT_ADMISSIONS.map((admission) => {
|
||
const elderId = elderIdByName.get(admission.elderName);
|
||
const bedId = bedIdByCode.get(admission.bedCode);
|
||
if (!elderId || !bedId) {
|
||
throw new Error("默认入住初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
bedId,
|
||
status: admission.status,
|
||
dischargedAt: admission.status === "discharged" ? new Date() : undefined,
|
||
notes: admission.notes,
|
||
};
|
||
}),
|
||
);
|
||
|
||
const now = new Date();
|
||
await transaction.insert(careTasks).values(
|
||
DEFAULT_CARE_TASKS.map((task) => {
|
||
const elderId = elderIdByName.get(task.elderName);
|
||
if (!elderId) {
|
||
throw new Error("默认护理任务初始化失败");
|
||
}
|
||
|
||
const completedAt = task.completedHoursAgo === undefined ? undefined : new Date(now.getTime() - task.completedHoursAgo * 60 * 60 * 1000);
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
title: task.title,
|
||
careType: task.careType,
|
||
priority: task.priority,
|
||
status: task.status,
|
||
scheduledAt: new Date(now.getTime() + task.scheduledHoursOffset * 60 * 60 * 1000),
|
||
assigneeLabel: task.assigneeLabel,
|
||
executionNotes: task.executionNotes,
|
||
completedAt,
|
||
};
|
||
}),
|
||
);
|
||
|
||
await transaction.insert(healthProfiles).values(
|
||
DEFAULT_HEALTH_PROFILES.map((profile) => {
|
||
const elderId = elderIdByName.get(profile.elderName);
|
||
if (!elderId) {
|
||
throw new Error("默认健康档案初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
allergyNotes: profile.allergyNotes,
|
||
medicalHistory: profile.medicalHistory,
|
||
medicationNotes: profile.medicationNotes,
|
||
careRestrictions: profile.careRestrictions,
|
||
emergencyNotes: profile.emergencyNotes,
|
||
};
|
||
}),
|
||
);
|
||
|
||
const vitalRows = await transaction
|
||
.insert(vitalRecords)
|
||
.values(
|
||
DEFAULT_VITAL_RECORDS.map((vital) => {
|
||
const elderId = elderIdByName.get(vital.elderName);
|
||
if (!elderId) {
|
||
throw new Error("默认生命体征初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
recordedAt: new Date(now.getTime() - vital.recordedHoursAgo * 60 * 60 * 1000),
|
||
source: vital.source,
|
||
systolicBp: vital.systolicBp,
|
||
diastolicBp: vital.diastolicBp,
|
||
heartRate: vital.heartRate,
|
||
temperatureTenths: vital.temperatureTenths,
|
||
spo2: vital.spo2,
|
||
bloodGlucoseTenths: vital.bloodGlucoseTenths,
|
||
weightTenths: vital.weightTenths,
|
||
notes: vital.notes,
|
||
};
|
||
}),
|
||
)
|
||
.returning();
|
||
if (vitalRows.length !== DEFAULT_VITAL_RECORDS.length) {
|
||
throw new Error("默认生命体征初始化失败");
|
||
}
|
||
|
||
await transaction.insert(chronicConditions).values(
|
||
DEFAULT_CHRONIC_CONDITIONS.map((condition) => {
|
||
const elderId = elderIdByName.get(condition.elderName);
|
||
if (!elderId) {
|
||
throw new Error("默认慢病记录初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
name: condition.name,
|
||
status: condition.status,
|
||
treatmentNotes: condition.treatmentNotes,
|
||
followUpNotes: condition.followUpNotes,
|
||
};
|
||
}),
|
||
);
|
||
|
||
await transaction.insert(healthAnomalyReviews).values(
|
||
DEFAULT_HEALTH_REVIEWS.map((review) => {
|
||
const elderId = elderIdByName.get(review.elderName);
|
||
const vital = vitalRows[review.vitalIndex];
|
||
if (!elderId || !vital) {
|
||
throw new Error("默认健康异常复核初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
vitalRecordId: vital.id,
|
||
severity: review.severity,
|
||
status: review.status,
|
||
title: review.title,
|
||
description: review.description,
|
||
resolutionNotes: review.resolutionNotes,
|
||
};
|
||
}),
|
||
);
|
||
|
||
await transaction.insert(systemIncidents).values(
|
||
DEFAULT_SYSTEM_INCIDENTS.map((incident) => ({
|
||
organizationId,
|
||
severity: incident.severity,
|
||
status: incident.status,
|
||
title: incident.title,
|
||
description: incident.description,
|
||
source: incident.source,
|
||
acknowledgedAt:
|
||
incident.acknowledgedHoursAgo === undefined ? undefined : new Date(now.getTime() - incident.acknowledgedHoursAgo * 60 * 60 * 1000),
|
||
resolvedAt:
|
||
incident.resolvedHoursAgo === undefined ? undefined : new Date(now.getTime() - incident.resolvedHoursAgo * 60 * 60 * 1000),
|
||
createdAt: new Date(now.getTime() - incident.createdHoursAgo * 60 * 60 * 1000),
|
||
updatedAt: new Date(
|
||
now.getTime() -
|
||
(incident.resolvedHoursAgo ?? incident.acknowledgedHoursAgo ?? incident.createdHoursAgo) * 60 * 60 * 1000,
|
||
),
|
||
})),
|
||
);
|
||
|
||
const deviceRows = await transaction
|
||
.insert(deviceAssets)
|
||
.values(
|
||
DEFAULT_DEVICE_ASSETS.map((device) => ({
|
||
organizationId,
|
||
name: device.name,
|
||
code: device.code,
|
||
category: device.category,
|
||
location: device.location,
|
||
status: device.status,
|
||
lastInspectedAt:
|
||
device.lastInspectedHoursAgo === undefined ? undefined : new Date(now.getTime() - device.lastInspectedHoursAgo * 60 * 60 * 1000),
|
||
notes: device.notes,
|
||
})),
|
||
)
|
||
.returning();
|
||
if (deviceRows.length !== DEFAULT_DEVICE_ASSETS.length) {
|
||
throw new Error("默认设备台账初始化失败");
|
||
}
|
||
const deviceIdByCode = new Map(deviceRows.map((device) => [device.code, device.id]));
|
||
|
||
await transaction.insert(maintenanceTickets).values(
|
||
DEFAULT_MAINTENANCE_TICKETS.map((ticket) => {
|
||
const deviceId = deviceIdByCode.get(ticket.deviceCode);
|
||
if (!deviceId) {
|
||
throw new Error("默认维修工单初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
deviceId,
|
||
title: ticket.title,
|
||
description: ticket.description,
|
||
priority: ticket.priority,
|
||
status: ticket.status,
|
||
assigneeLabel: ticket.assigneeLabel,
|
||
resolutionNotes: ticket.resolutionNotes,
|
||
resolvedAt: ticket.status === "resolved" || ticket.status === "closed" ? now : undefined,
|
||
};
|
||
}),
|
||
);
|
||
|
||
await transaction.insert(notices).values(
|
||
DEFAULT_NOTICES.map((notice) => ({
|
||
organizationId,
|
||
title: notice.title,
|
||
content: notice.content,
|
||
audience: notice.audience,
|
||
status: notice.status,
|
||
publishedAt: notice.publishedHoursAgo === undefined ? undefined : new Date(now.getTime() - notice.publishedHoursAgo * 60 * 60 * 1000),
|
||
})),
|
||
);
|
||
|
||
const alertRuleRows = await transaction
|
||
.insert(alertRules)
|
||
.values(DEFAULT_ALERT_RULES.map((rule) => ({ ...rule, organizationId })))
|
||
.returning();
|
||
if (alertRuleRows.length !== DEFAULT_ALERT_RULES.length) {
|
||
throw new Error("默认预警规则初始化失败");
|
||
}
|
||
const alertRuleIdByName = new Map(alertRuleRows.map((rule) => [rule.name, rule.id]));
|
||
|
||
await transaction.insert(alertTriggers).values(
|
||
DEFAULT_ALERT_TRIGGERS.map((trigger) => {
|
||
const ruleId = alertRuleIdByName.get(trigger.ruleName);
|
||
const elderId = trigger.elderName ? elderIdByName.get(trigger.elderName) : undefined;
|
||
if (!ruleId || (trigger.elderName && !elderId)) {
|
||
throw new Error("默认预警触发记录初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
ruleId,
|
||
elderId,
|
||
title: trigger.title,
|
||
description: trigger.description,
|
||
status: trigger.status,
|
||
source: trigger.source,
|
||
handlingNotes: trigger.handlingNotes,
|
||
handledAt: trigger.status === "open" ? undefined : now,
|
||
createdAt: new Date(now.getTime() - trigger.createdHoursAgo * 60 * 60 * 1000),
|
||
updatedAt: now,
|
||
};
|
||
}),
|
||
);
|
||
|
||
const familyContactRows = await transaction
|
||
.insert(familyContacts)
|
||
.values(
|
||
DEFAULT_FAMILY_CONTACTS.map((contact) => {
|
||
const elderId = elderIdByName.get(contact.elderName);
|
||
if (!elderId) {
|
||
throw new Error("默认家属联系人初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
name: contact.name,
|
||
relationship: contact.relationship,
|
||
phone: contact.phone,
|
||
status: contact.status,
|
||
notes: contact.notes,
|
||
};
|
||
}),
|
||
)
|
||
.returning();
|
||
if (familyContactRows.length !== DEFAULT_FAMILY_CONTACTS.length) {
|
||
throw new Error("默认家属联系人初始化失败");
|
||
}
|
||
const familyContactIdByKey = new Map(
|
||
familyContactRows.map((contact) => [`${contact.elderId}/${contact.name}`, contact.id]),
|
||
);
|
||
|
||
await transaction.insert(familyVisitAppointments).values(
|
||
DEFAULT_FAMILY_VISITS.map((visit) => {
|
||
const elderId = elderIdByName.get(visit.elderName);
|
||
const contactId = elderId ? familyContactIdByKey.get(`${elderId}/${visit.contactName}`) : undefined;
|
||
if (!elderId || !contactId) {
|
||
throw new Error("默认探访预约初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
contactId,
|
||
scheduledAt: new Date(now.getTime() + visit.scheduledHoursOffset * 60 * 60 * 1000),
|
||
status: visit.status,
|
||
notes: visit.notes,
|
||
handledAt: visit.status === "requested" ? undefined : now,
|
||
};
|
||
}),
|
||
);
|
||
|
||
await transaction.insert(familyFeedback).values(
|
||
DEFAULT_FAMILY_FEEDBACK.map((feedback) => {
|
||
const elderId = elderIdByName.get(feedback.elderName);
|
||
const contactId = elderId ? familyContactIdByKey.get(`${elderId}/${feedback.contactName}`) : undefined;
|
||
if (!elderId || !contactId) {
|
||
throw new Error("默认家属反馈初始化失败");
|
||
}
|
||
|
||
return {
|
||
organizationId,
|
||
elderId,
|
||
contactId,
|
||
feedbackType: feedback.feedbackType,
|
||
content: feedback.content,
|
||
status: feedback.status,
|
||
responseNotes: feedback.responseNotes,
|
||
handledAt: feedback.status === "open" ? undefined : now,
|
||
};
|
||
}),
|
||
);
|
||
});
|
||
await seedDefaultCollaborationWorkspaceData(organizationId);
|
||
await seedDefaultAiWorkspaceData(organizationId);
|
||
}
|