feat: build collaboration workspaces
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user