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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ export const PERMISSION_DEFINITIONS: PermissionDefinition[] = [
|
||||
{ id: "care:manage", label: "处理护理任务", category: "护理", description: "启动、完成和维护护理服务执行记录。" },
|
||||
{ id: "health:read", label: "查看健康数据", category: "健康", description: "查看老人健康档案、生命体征和异常复核。" },
|
||||
{ id: "health:manage", label: "管理健康数据", category: "健康", description: "维护健康档案、生命体征、慢病和异常复核。" },
|
||||
{ id: "device:read", label: "查看设备运维", category: "协同", description: "查看设备台账和维修工单。" },
|
||||
{ id: "device:manage", label: "管理设备运维", category: "协同", description: "维护设备台账、维修工单和处理状态。" },
|
||||
{ id: "notice:read", label: "查看公告通知", category: "协同", description: "查看公告和阅读状态。" },
|
||||
{ id: "notice:manage", label: "管理公告通知", category: "协同", description: "创建、发布、撤回和删除公告。" },
|
||||
{ id: "alert:read", label: "查看规则预警", category: "协同", description: "查看预警规则和触发记录。" },
|
||||
{ id: "alert:manage", label: "管理规则预警", category: "协同", description: "维护预警规则并处理触发记录。" },
|
||||
{ id: "family:read", label: "查看家属服务", category: "协同", description: "查看家属联系人、探访预约和服务反馈。" },
|
||||
{ id: "family:manage", label: "管理家属服务", category: "协同", description: "维护家属联系人、探访预约和反馈处理。" },
|
||||
{ id: "facility:read", label: "查看房间床位", category: "床位", description: "查看房间、床位和占用状态。" },
|
||||
{ id: "facility:manage", label: "管理房间床位", category: "床位", description: "维护院区、房间、床位和床位状态。" },
|
||||
{ id: "admission:read", label: "查看入住", category: "入住", description: "查看入住、换床、退住历史。" },
|
||||
@@ -97,6 +105,14 @@ export const SYSTEM_ROLE_DEFINITIONS: SeedRole[] = [
|
||||
"care:manage",
|
||||
"health:read",
|
||||
"health:manage",
|
||||
"device:read",
|
||||
"device:manage",
|
||||
"notice:read",
|
||||
"notice:manage",
|
||||
"alert:read",
|
||||
"alert:manage",
|
||||
"family:read",
|
||||
"family:manage",
|
||||
"facility:read",
|
||||
"facility:manage",
|
||||
"admission:read",
|
||||
@@ -119,6 +135,14 @@ export const SYSTEM_ROLE_DEFINITIONS: SeedRole[] = [
|
||||
"care:manage",
|
||||
"health:read",
|
||||
"health:manage",
|
||||
"device:read",
|
||||
"device:manage",
|
||||
"notice:read",
|
||||
"notice:manage",
|
||||
"alert:read",
|
||||
"alert:manage",
|
||||
"family:read",
|
||||
"family:manage",
|
||||
"facility:read",
|
||||
"facility:manage",
|
||||
"admission:read",
|
||||
@@ -133,13 +157,35 @@ export const SYSTEM_ROLE_DEFINITIONS: SeedRole[] = [
|
||||
key: "caregiver",
|
||||
scope: "organization",
|
||||
description: "照护人员,查看床位和维护老人照护信息。",
|
||||
permissions: ["care:read", "care:manage", "health:read", "health:manage", "facility:read", "admission:read", "elder:read", "elder:update"],
|
||||
permissions: [
|
||||
"care:read",
|
||||
"care:manage",
|
||||
"health:read",
|
||||
"health:manage",
|
||||
"device:read",
|
||||
"alert:read",
|
||||
"family:read",
|
||||
"facility:read",
|
||||
"admission:read",
|
||||
"elder:read",
|
||||
"elder:update",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "viewer",
|
||||
scope: "organization",
|
||||
description: "普通用户,只读查看老人、床位和入住信息。",
|
||||
permissions: ["care:read", "health:read", "facility:read", "admission:read", "elder:read"],
|
||||
permissions: [
|
||||
"care:read",
|
||||
"health:read",
|
||||
"device:read",
|
||||
"notice:read",
|
||||
"alert:read",
|
||||
"family:read",
|
||||
"facility:read",
|
||||
"admission:read",
|
||||
"elder:read",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "family",
|
||||
|
||||
@@ -30,6 +30,17 @@ export const healthReviewStatusEnum = pgEnum("health_review_status", ["pending",
|
||||
export const careTaskTypeEnum = pgEnum("care_task_type", ["daily_care", "meal", "medication", "rehab", "inspection", "cleaning", "other"]);
|
||||
export const careTaskPriorityEnum = pgEnum("care_task_priority", ["low", "normal", "high", "urgent"]);
|
||||
export const careTaskStatusEnum = pgEnum("care_task_status", ["pending", "in_progress", "completed", "cancelled"]);
|
||||
export const deviceAssetStatusEnum = pgEnum("device_asset_status", ["active", "maintenance", "disabled", "retired"]);
|
||||
export const maintenanceTicketStatusEnum = pgEnum("maintenance_ticket_status", ["open", "assigned", "resolved", "closed", "cancelled"]);
|
||||
export const maintenanceTicketPriorityEnum = pgEnum("maintenance_ticket_priority", ["low", "normal", "high", "urgent"]);
|
||||
export const noticeStatusEnum = pgEnum("notice_status", ["draft", "published", "retracted"]);
|
||||
export const alertRuleTypeEnum = pgEnum("alert_rule_type", ["health", "care", "device", "incident", "admission", "other"]);
|
||||
export const alertRuleStatusEnum = pgEnum("alert_rule_status", ["enabled", "disabled"]);
|
||||
export const alertTriggerStatusEnum = pgEnum("alert_trigger_status", ["open", "acknowledged", "resolved", "closed"]);
|
||||
export const familyContactStatusEnum = pgEnum("family_contact_status", ["active", "suspended", "archived"]);
|
||||
export const familyVisitStatusEnum = pgEnum("family_visit_status", ["requested", "approved", "completed", "cancelled"]);
|
||||
export const familyFeedbackTypeEnum = pgEnum("family_feedback_type", ["service", "care", "meal", "environment", "other"]);
|
||||
export const familyFeedbackStatusEnum = pgEnum("family_feedback_status", ["open", "in_progress", "resolved", "closed"]);
|
||||
|
||||
export const systemSettings = pgTable("system_settings", {
|
||||
id: text("id").primaryKey().default("global"),
|
||||
@@ -323,6 +334,150 @@ export const careTasks = pgTable("care_tasks", {
|
||||
scheduleLookup: index("care_tasks_schedule_lookup").on(table.organizationId, table.scheduledAt),
|
||||
}));
|
||||
|
||||
export const deviceAssets = pgTable("device_assets", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
code: text("code").notNull(),
|
||||
category: text("category").notNull().default(""),
|
||||
location: text("location").notNull().default(""),
|
||||
status: deviceAssetStatusEnum("status").notNull().default("active"),
|
||||
lastInspectedAt: timestamp("last_inspected_at", { withTimezone: true }),
|
||||
notes: text("notes").notNull().default(""),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
codeUnique: uniqueIndex("device_assets_code_organization_unique").on(table.organizationId, table.code),
|
||||
statusLookup: index("device_assets_status_lookup").on(table.organizationId, table.status),
|
||||
}));
|
||||
|
||||
export const maintenanceTickets = pgTable("maintenance_tickets", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
deviceId: uuid("device_id").references(() => deviceAssets.id, { onDelete: "set null" }),
|
||||
title: text("title").notNull(),
|
||||
description: text("description").notNull().default(""),
|
||||
priority: maintenanceTicketPriorityEnum("priority").notNull().default("normal"),
|
||||
status: maintenanceTicketStatusEnum("status").notNull().default("open"),
|
||||
assigneeLabel: text("assignee_label").notNull().default(""),
|
||||
resolutionNotes: text("resolution_notes").notNull().default(""),
|
||||
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
queueLookup: index("maintenance_tickets_queue_lookup").on(table.organizationId, table.status, table.priority),
|
||||
deviceLookup: index("maintenance_tickets_device_lookup").on(table.organizationId, table.deviceId),
|
||||
}));
|
||||
|
||||
export const notices = pgTable("notices", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
content: text("content").notNull().default(""),
|
||||
audience: text("audience").notNull().default("全体员工"),
|
||||
status: noticeStatusEnum("status").notNull().default("draft"),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true }),
|
||||
createdByAccountId: uuid("created_by_account_id").references(() => accounts.id),
|
||||
updatedByAccountId: uuid("updated_by_account_id").references(() => accounts.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
statusLookup: index("notices_status_lookup").on(table.organizationId, table.status),
|
||||
}));
|
||||
|
||||
export const noticeReadReceipts = pgTable("notice_read_receipts", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
noticeId: uuid("notice_id").notNull().references(() => notices.id, { onDelete: "cascade" }),
|
||||
accountId: uuid("account_id").notNull().references(() => accounts.id, { onDelete: "cascade" }),
|
||||
readAt: timestamp("read_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
accountNoticeUnique: uniqueIndex("notice_read_receipts_account_notice_unique").on(table.noticeId, table.accountId),
|
||||
noticeLookup: index("notice_read_receipts_notice_lookup").on(table.organizationId, table.noticeId),
|
||||
}));
|
||||
|
||||
export const alertRules = pgTable("alert_rules", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
ruleType: alertRuleTypeEnum("rule_type").notNull().default("other"),
|
||||
severity: incidentSeverityEnum("severity").notNull().default("warning"),
|
||||
status: alertRuleStatusEnum("status").notNull().default("enabled"),
|
||||
conditionSummary: text("condition_summary").notNull().default(""),
|
||||
suggestion: text("suggestion").notNull().default(""),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
statusLookup: index("alert_rules_status_lookup").on(table.organizationId, table.status, table.ruleType),
|
||||
}));
|
||||
|
||||
export const alertTriggers = pgTable("alert_triggers", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
ruleId: uuid("rule_id").references(() => alertRules.id, { onDelete: "set null" }),
|
||||
elderId: uuid("elder_id").references(() => elders.id, { onDelete: "set null" }),
|
||||
title: text("title").notNull(),
|
||||
description: text("description").notNull().default(""),
|
||||
status: alertTriggerStatusEnum("status").notNull().default("open"),
|
||||
source: text("source").notNull().default(""),
|
||||
handledByAccountId: uuid("handled_by_account_id").references(() => accounts.id),
|
||||
handledAt: timestamp("handled_at", { withTimezone: true }),
|
||||
handlingNotes: text("handling_notes").notNull().default(""),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
queueLookup: index("alert_triggers_queue_lookup").on(table.organizationId, table.status),
|
||||
ruleLookup: index("alert_triggers_rule_lookup").on(table.organizationId, table.ruleId),
|
||||
}));
|
||||
|
||||
export const familyContacts = pgTable("family_contacts", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
elderId: uuid("elder_id").notNull().references(() => elders.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
relationship: text("relationship").notNull(),
|
||||
phone: text("phone").notNull(),
|
||||
status: familyContactStatusEnum("status").notNull().default("active"),
|
||||
notes: text("notes").notNull().default(""),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
elderLookup: index("family_contacts_elder_lookup").on(table.organizationId, table.elderId, table.status),
|
||||
}));
|
||||
|
||||
export const familyVisitAppointments = pgTable("family_visit_appointments", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
elderId: uuid("elder_id").notNull().references(() => elders.id, { onDelete: "cascade" }),
|
||||
contactId: uuid("contact_id").references(() => familyContacts.id, { onDelete: "set null" }),
|
||||
scheduledAt: timestamp("scheduled_at", { withTimezone: true }).notNull(),
|
||||
status: familyVisitStatusEnum("status").notNull().default("requested"),
|
||||
notes: text("notes").notNull().default(""),
|
||||
handledByAccountId: uuid("handled_by_account_id").references(() => accounts.id),
|
||||
handledAt: timestamp("handled_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
scheduleLookup: index("family_visit_appointments_schedule_lookup").on(table.organizationId, table.status, table.scheduledAt),
|
||||
}));
|
||||
|
||||
export const familyFeedback = pgTable("family_feedback", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
elderId: uuid("elder_id").notNull().references(() => elders.id, { onDelete: "cascade" }),
|
||||
contactId: uuid("contact_id").references(() => familyContacts.id, { onDelete: "set null" }),
|
||||
feedbackType: familyFeedbackTypeEnum("feedback_type").notNull().default("other"),
|
||||
content: text("content").notNull(),
|
||||
status: familyFeedbackStatusEnum("status").notNull().default("open"),
|
||||
responseNotes: text("response_notes").notNull().default(""),
|
||||
handledByAccountId: uuid("handled_by_account_id").references(() => accounts.id),
|
||||
handledAt: timestamp("handled_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
queueLookup: index("family_feedback_queue_lookup").on(table.organizationId, table.status, table.feedbackType),
|
||||
}));
|
||||
|
||||
export const admissions = pgTable("admissions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
|
||||
Reference in New Issue
Block a user