feat: build collaboration workspaces

This commit is contained in:
2026-07-03 03:02:36 -07:00
parent bf3dd256ab
commit 4ba2a11b88
53 changed files with 9557 additions and 29 deletions

View File

@@ -3,15 +3,23 @@ import { eq } from "drizzle-orm";
import { getDatabase } from "@/modules/core/server/db";
import {
admissions,
alertRules,
alertTriggers,
beds,
buildings,
campuses,
careTasks,
chronicConditions,
deviceAssets,
elders,
familyContacts,
familyFeedback,
familyVisitAppointments,
floors,
healthAnomalyReviews,
healthProfiles,
maintenanceTickets,
notices,
rooms,
systemIncidents,
vitalRecords,
@@ -116,6 +124,80 @@ type SeedSystemIncident = {
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";
};
const DEFAULT_ROOMS: SeedRoom[] = [
{ campusName: "主院区", buildingName: "护理楼", floorName: "一层", floorLevel: 1, code: "A101", name: "阳光房", capacity: 2 },
{ campusName: "主院区", buildingName: "护理楼", floorName: "一层", floorLevel: 1, code: "A102", name: "康养房", capacity: 2 },
@@ -871,6 +953,189 @@ const DEFAULT_SYSTEM_INCIDENTS: SeedSystemIncident[] = [
},
];
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: "适用于步态训练和扶行评估。",
},
];
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: "已完成速度校准,安全带状态正常。",
},
];
const DEFAULT_NOTICES: SeedNotice[] = [
{
title: "本周家属探访安排",
content: "周六上午开放护理楼一层和康复楼探访,请各护理组提前确认老人状态和陪同人员。",
audience: "护理组、前台接待",
status: "published",
publishedHoursAgo: 4,
},
{
title: "夜间重点巡房提醒",
content: "钱松柏、赵国华、马淑芬列入今晚重点巡房名单,请夜班护理组按时记录。",
audience: "夜班护理组",
status: "published",
publishedHoursAgo: 10,
},
{
title: "设备巡检培训材料",
content: "呼叫器、供氧接口和血氧监测仪巡检 SOP 已更新,待主管确认后发布。",
audience: "设备运维组",
status: "draft",
},
];
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: "通知设备运维组完成派单。",
},
];
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,
},
];
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: "希望接收护理反馈。" },
];
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: "已完成探访和短住事项确认。" },
];
const DEFAULT_FAMILY_FEEDBACK: SeedFamilyFeedback[] = [
{
elderName: "赵国华",
contactName: "赵蕾",
feedbackType: "care",
content: "希望夜间巡房后能同步重点观察结果。",
status: "in_progress",
responseNotes: "护理主管已跟进,今晚开始补充巡房备注。",
},
{
elderName: "林玉琴",
contactName: "林晓",
feedbackType: "environment",
content: "短住房间清洁和呼叫器说明比较清楚。",
status: "resolved",
responseNotes: "已记录为接待服务反馈。",
},
];
function hasRows(rows: unknown[]): boolean {
return rows.length > 0;
}
@@ -1187,5 +1452,160 @@ export async function seedDefaultWorkspaceData(organizationId: string): Promise<
),
})),
);
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,
};
}),
);
});
}