import { and, desc, eq, inArray } from "drizzle-orm"; import { getDatabase } from "@/modules/core/server/db"; import { accounts, elders, familyContacts, familyFeedback, familyVisitAppointments } from "@/modules/core/server/schema"; import type { FamilyContact, FamilyContactInput, FamilyFeedback, FamilyFeedbackInput, FamilyServiceData, FamilyVisitAppointment, FamilyVisitInput, } from "@/modules/family/types"; export type FamilyMutationFailure = { success: false; reason: string; status: number; }; export function isFamilyMutationFailure(value: unknown): value is FamilyMutationFailure { return ( typeof value === "object" && value !== null && "success" in value && (value as { success?: unknown }).success === false ); } function iso(value: Date): string { return value.toISOString(); } function optionalIso(value: Date | null): string | undefined { return value ? iso(value) : undefined; } function toContact(row: typeof familyContacts.$inferSelect, elderNameById: Map): FamilyContact { return { id: row.id, organizationId: row.organizationId, elderId: row.elderId, elderName: elderNameById.get(row.elderId) ?? "", name: row.name, relationship: row.relationship, phone: row.phone, status: row.status, notes: row.notes, createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt), }; } function toVisit( row: typeof familyVisitAppointments.$inferSelect, elderNameById: Map, contactNameById: Map, accountNameById: Map, ): FamilyVisitAppointment { const contactId = row.contactId ?? undefined; const handledByAccountId = row.handledByAccountId ?? undefined; return { id: row.id, organizationId: row.organizationId, elderId: row.elderId, elderName: elderNameById.get(row.elderId) ?? "", contactId, contactName: contactId ? contactNameById.get(contactId) ?? "" : "", scheduledAt: iso(row.scheduledAt), status: row.status, notes: row.notes, handledByAccountId, handledByName: handledByAccountId ? accountNameById.get(handledByAccountId) : undefined, handledAt: optionalIso(row.handledAt), createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt), }; } function toFeedback( row: typeof familyFeedback.$inferSelect, elderNameById: Map, contactNameById: Map, accountNameById: Map, ): FamilyFeedback { const contactId = row.contactId ?? undefined; const handledByAccountId = row.handledByAccountId ?? undefined; return { id: row.id, organizationId: row.organizationId, elderId: row.elderId, elderName: elderNameById.get(row.elderId) ?? "", contactId, contactName: contactId ? contactNameById.get(contactId) ?? "" : "", feedbackType: row.feedbackType, content: row.content, status: row.status, responseNotes: row.responseNotes, handledByAccountId, handledByName: handledByAccountId ? accountNameById.get(handledByAccountId) : undefined, handledAt: optionalIso(row.handledAt), createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt), }; } async function validateElderAndContact(input: { contactId?: string; elderId: string; organizationId: string; }): Promise<{ contactNameById: Map; elderNameById: Map; } | FamilyMutationFailure> { const database = getDatabase(); const [elderRows, contactRows] = await Promise.all([ database.select({ id: elders.id, name: elders.name }).from(elders).where(and(eq(elders.id, input.elderId), eq(elders.organizationId, input.organizationId))), input.contactId ? database.select({ id: familyContacts.id, name: familyContacts.name }).from(familyContacts).where(and(eq(familyContacts.id, input.contactId), eq(familyContacts.organizationId, input.organizationId))) : [], ]); const elder = elderRows[0]; if (!elder) { return { success: false, reason: "老人档案不存在", status: 404 }; } if (input.contactId && !contactRows[0]) { return { success: false, reason: "家属联系人不存在", status: 404 }; } return { elderNameById: new Map([[elder.id, elder.name]]), contactNameById: new Map(contactRows.map((contact) => [contact.id, contact.name])), }; } export async function listFamilyServiceData(organizationId: string): Promise { const database = getDatabase(); const [elderRows, contactRows, visitRows, feedbackRows] = await Promise.all([ database.select({ id: elders.id, name: elders.name }).from(elders).where(eq(elders.organizationId, organizationId)).orderBy(desc(elders.createdAt)), database.select().from(familyContacts).where(eq(familyContacts.organizationId, organizationId)).orderBy(desc(familyContacts.updatedAt)), database.select().from(familyVisitAppointments).where(eq(familyVisitAppointments.organizationId, organizationId)).orderBy(desc(familyVisitAppointments.scheduledAt)), database.select().from(familyFeedback).where(eq(familyFeedback.organizationId, organizationId)).orderBy(desc(familyFeedback.updatedAt)), ]); const contactIds = Array.from( new Set( [...visitRows.map((visit) => visit.contactId), ...feedbackRows.map((feedback) => feedback.contactId)].filter((id): id is string => typeof id === "string"), ), ); const accountIds = Array.from( new Set( [...visitRows.map((visit) => visit.handledByAccountId), ...feedbackRows.map((feedback) => feedback.handledByAccountId)].filter( (id): id is string => typeof id === "string", ), ), ); const [linkedContactRows, accountRows] = await Promise.all([ contactIds.length > 0 ? database.select({ id: familyContacts.id, name: familyContacts.name }).from(familyContacts).where(inArray(familyContacts.id, contactIds)) : [], accountIds.length > 0 ? database.select({ id: accounts.id, name: accounts.name }).from(accounts).where(inArray(accounts.id, accountIds)) : [], ]); const elderNameById = new Map(elderRows.map((elder) => [elder.id, elder.name])); const contactNameById = new Map([...contactRows.map((contact) => [contact.id, contact.name] as const), ...linkedContactRows.map((contact) => [contact.id, contact.name] as const)]); const accountNameById = new Map(accountRows.map((account) => [account.id, account.name])); return { metrics: { activeContacts: contactRows.filter((contact) => contact.status === "active").length, pendingVisits: visitRows.filter((visit) => visit.status === "requested").length, openFeedback: feedbackRows.filter((feedback) => feedback.status === "open" || feedback.status === "in_progress").length, resolvedFeedback: feedbackRows.filter((feedback) => feedback.status === "resolved" || feedback.status === "closed").length, }, elderOptions: elderRows, contacts: contactRows.map((contact) => toContact(contact, elderNameById)), visits: visitRows.map((visit) => toVisit(visit, elderNameById, contactNameById, accountNameById)), feedback: feedbackRows.map((feedback) => toFeedback(feedback, elderNameById, contactNameById, accountNameById)), }; } export async function createFamilyContact(input: FamilyContactInput & { organizationId: string }): Promise { const maps = await validateElderAndContact({ elderId: input.elderId, organizationId: input.organizationId }); if (isFamilyMutationFailure(maps)) { return maps; } const database = getDatabase(); const rows = await database.insert(familyContacts).values({ ...input }).returning(); const contact = rows[0]; return contact ? toContact(contact, maps.elderNameById) : { success: false, reason: "家属联系人创建失败", status: 500 }; } export async function updateFamilyContact(input: FamilyContactInput & { id: string; organizationId: string }): Promise { const maps = await validateElderAndContact({ elderId: input.elderId, organizationId: input.organizationId }); if (isFamilyMutationFailure(maps)) { return maps; } const database = getDatabase(); const rows = await database .update(familyContacts) .set({ elderId: input.elderId, name: input.name, relationship: input.relationship, phone: input.phone, status: input.status, notes: input.notes, updatedAt: new Date(), }) .where(and(eq(familyContacts.id, input.id), eq(familyContacts.organizationId, input.organizationId))) .returning(); const contact = rows[0]; return contact ? toContact(contact, maps.elderNameById) : { success: false, reason: "家属联系人不存在", status: 404 }; } export async function deleteFamilyContact(input: { id: string; organizationId: string }): Promise { const database = getDatabase(); const rows = await database.delete(familyContacts).where(and(eq(familyContacts.id, input.id), eq(familyContacts.organizationId, input.organizationId))).returning(); const contact = rows[0]; return contact ? toContact(contact, new Map()) : { success: false, reason: "家属联系人不存在", status: 404 }; } export async function createFamilyVisit(input: FamilyVisitInput & { accountId: string; organizationId: string }): Promise { const maps = await validateElderAndContact(input); if (isFamilyMutationFailure(maps)) { return maps; } const database = getDatabase(); const handledAt = input.status === "approved" || input.status === "completed" || input.status === "cancelled" ? new Date() : undefined; const rows = await database .insert(familyVisitAppointments) .values({ organizationId: input.organizationId, elderId: input.elderId, contactId: input.contactId, scheduledAt: new Date(input.scheduledAt), status: input.status, notes: input.notes, handledByAccountId: handledAt ? input.accountId : undefined, handledAt, }) .returning(); const visit = rows[0]; return visit ? toVisit(visit, maps.elderNameById, maps.contactNameById, new Map()) : { success: false, reason: "探访预约创建失败", status: 500 }; } export async function updateFamilyVisit(input: FamilyVisitInput & { accountId: string; id: string; organizationId: string }): Promise { const maps = await validateElderAndContact(input); if (isFamilyMutationFailure(maps)) { return maps; } const database = getDatabase(); const handledAt = input.status === "approved" || input.status === "completed" || input.status === "cancelled" ? new Date() : null; const rows = await database .update(familyVisitAppointments) .set({ elderId: input.elderId, contactId: input.contactId ?? null, scheduledAt: new Date(input.scheduledAt), status: input.status, notes: input.notes, handledByAccountId: handledAt ? input.accountId : null, handledAt, updatedAt: new Date(), }) .where(and(eq(familyVisitAppointments.id, input.id), eq(familyVisitAppointments.organizationId, input.organizationId))) .returning(); const visit = rows[0]; return visit ? toVisit(visit, maps.elderNameById, maps.contactNameById, new Map()) : { success: false, reason: "探访预约不存在", status: 404 }; } export async function deleteFamilyVisit(input: { id: string; organizationId: string }): Promise { const database = getDatabase(); const rows = await database .delete(familyVisitAppointments) .where(and(eq(familyVisitAppointments.id, input.id), eq(familyVisitAppointments.organizationId, input.organizationId))) .returning(); const visit = rows[0]; return visit ? toVisit(visit, new Map(), new Map(), new Map()) : { success: false, reason: "探访预约不存在", status: 404 }; } export async function createFamilyFeedback(input: FamilyFeedbackInput & { accountId: string; organizationId: string }): Promise { const maps = await validateElderAndContact(input); if (isFamilyMutationFailure(maps)) { return maps; } const database = getDatabase(); const handledAt = input.status === "resolved" || input.status === "closed" || input.status === "in_progress" ? new Date() : undefined; const rows = await database .insert(familyFeedback) .values({ organizationId: input.organizationId, elderId: input.elderId, contactId: input.contactId, feedbackType: input.feedbackType, content: input.content, status: input.status, responseNotes: input.responseNotes, handledByAccountId: handledAt ? input.accountId : undefined, handledAt, }) .returning(); const feedback = rows[0]; return feedback ? toFeedback(feedback, maps.elderNameById, maps.contactNameById, new Map()) : { success: false, reason: "家属反馈创建失败", status: 500 }; } export async function updateFamilyFeedback(input: FamilyFeedbackInput & { accountId: string; id: string; organizationId: string }): Promise { const maps = await validateElderAndContact(input); if (isFamilyMutationFailure(maps)) { return maps; } const database = getDatabase(); const handledAt = input.status === "resolved" || input.status === "closed" || input.status === "in_progress" ? new Date() : null; const rows = await database .update(familyFeedback) .set({ elderId: input.elderId, contactId: input.contactId ?? null, feedbackType: input.feedbackType, content: input.content, status: input.status, responseNotes: input.responseNotes, handledByAccountId: handledAt ? input.accountId : null, handledAt, updatedAt: new Date(), }) .where(and(eq(familyFeedback.id, input.id), eq(familyFeedback.organizationId, input.organizationId))) .returning(); const feedback = rows[0]; return feedback ? toFeedback(feedback, maps.elderNameById, maps.contactNameById, new Map()) : { success: false, reason: "家属反馈不存在", status: 404 }; } export async function deleteFamilyFeedback(input: { id: string; organizationId: string }): Promise { const database = getDatabase(); const rows = await database.delete(familyFeedback).where(and(eq(familyFeedback.id, input.id), eq(familyFeedback.organizationId, input.organizationId))).returning(); const feedback = rows[0]; return feedback ? toFeedback(feedback, new Map(), new Map(), new Map()) : { success: false, reason: "家属反馈不存在", status: 404 }; }