feat: complete ops dashboard AI board

This commit is contained in:
2026-07-08 19:47:28 -07:00
parent 6a1add3422
commit a8776f9d79
14 changed files with 1401 additions and 189 deletions

View File

@@ -979,7 +979,7 @@ const DEFAULT_SYSTEM_INCIDENTS: SeedSystemIncident[] = [
},
];
const DEFAULT_DEVICE_ASSETS: SeedDeviceAsset[] = [
export const DEFAULT_DEVICE_ASSETS: SeedDeviceAsset[] = [
{
name: "A102 床头呼叫器",
code: "DEV-CALL-A102-2",
@@ -1018,7 +1018,7 @@ const DEFAULT_DEVICE_ASSETS: SeedDeviceAsset[] = [
},
];
const DEFAULT_MAINTENANCE_TICKETS: SeedMaintenanceTicket[] = [
export const DEFAULT_MAINTENANCE_TICKETS: SeedMaintenanceTicket[] = [
{
deviceCode: "DEV-CALL-A102-2",
title: "床头呼叫器按键失灵",
@@ -1048,7 +1048,7 @@ const DEFAULT_MAINTENANCE_TICKETS: SeedMaintenanceTicket[] = [
},
];
const DEFAULT_NOTICES: SeedNotice[] = [
export const DEFAULT_NOTICES: SeedNotice[] = [
{
title: "本周家属探访安排",
content: "周六上午开放护理楼一层和康复楼探访,请各护理组提前确认老人状态和陪同人员。",
@@ -1071,7 +1071,7 @@ const DEFAULT_NOTICES: SeedNotice[] = [
},
];
const DEFAULT_ALERT_RULES: SeedAlertRule[] = [
export const DEFAULT_ALERT_RULES: SeedAlertRule[] = [
{
name: "血氧低值连续告警",
ruleType: "health",
@@ -1098,7 +1098,7 @@ const DEFAULT_ALERT_RULES: SeedAlertRule[] = [
},
];
const DEFAULT_ALERT_TRIGGERS: SeedAlertTrigger[] = [
export const DEFAULT_ALERT_TRIGGERS: SeedAlertTrigger[] = [
{
ruleName: "血氧低值连续告警",
elderName: "钱松柏",
@@ -1130,20 +1130,20 @@ const DEFAULT_ALERT_TRIGGERS: SeedAlertTrigger[] = [
},
];
const DEFAULT_FAMILY_CONTACTS: SeedFamilyContact[] = [
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: "希望接收护理反馈。" },
];
const DEFAULT_FAMILY_VISITS: SeedFamilyVisit[] = [
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: "已完成探访和短住事项确认。" },
];
const DEFAULT_FAMILY_FEEDBACK: SeedFamilyFeedback[] = [
export const DEFAULT_FAMILY_FEEDBACK: SeedFamilyFeedback[] = [
{
elderName: "赵国华",
contactName: "赵蕾",
@@ -1510,6 +1510,271 @@ async function seedDefaultAiWorkspaceData(organizationId: string): Promise<void>
});
}
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;
}
@@ -1524,6 +1789,7 @@ export async function seedDefaultWorkspaceData(organizationId: string): Promise<
]);
if ([existingRooms, existingBeds, existingElders, existingAdmissions].some(hasRows)) {
await seedDefaultCollaborationWorkspaceData(organizationId);
await seedDefaultAiWorkspaceData(organizationId);
return;
}
@@ -1983,5 +2249,6 @@ export async function seedDefaultWorkspaceData(organizationId: string): Promise<
}),
);
});
await seedDefaultCollaborationWorkspaceData(organizationId);
await seedDefaultAiWorkspaceData(organizationId);
}