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,
};
}),
);
});
}

View File

@@ -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",

View File

@@ -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" }),