From 5fe7e5a1b8e8bdeca0316172cd2957053cbc9574 Mon Sep 17 00:00:00 2001 From: TalexDreamSoul Date: Thu, 2 Jul 2026 19:16:29 -0700 Subject: [PATCH] fix: tighten real operations data paths --- app/(app)/app/beds/page.tsx | 20 +- app/(app)/app/dashboard/page.tsx | 22 +- app/(app)/app/elders/page.tsx | 8 +- app/api/admissions/route.ts | 11 +- app/api/facilities/beds/route.ts | 23 ++- app/api/facilities/rooms/route.ts | 103 ++++------ app/api/settings/join-requests/[id]/route.ts | 13 +- modules/core/server/operations.ts | 203 +++++++++++++++++++ 8 files changed, 286 insertions(+), 117 deletions(-) create mode 100644 modules/core/server/operations.ts diff --git a/app/(app)/app/beds/page.tsx b/app/(app)/app/beds/page.tsx index 0bce349..32cd359 100644 --- a/app/(app)/app/beds/page.tsx +++ b/app/(app)/app/beds/page.tsx @@ -1,8 +1,13 @@ import { redirect } from "next/navigation"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { + listOperationalAdmissions, + listOperationalBeds, + listOperationalElders, + listOperationalRooms, +} from "@/modules/core/server/operations"; import { hasPermission } from "@/modules/core/server/permissions"; -import { readData } from "@/modules/core/server/store"; import { BedsWorkspace } from "@/modules/facilities/components/BedsWorkspace"; export default async function BedsPage(): Promise { @@ -15,14 +20,13 @@ export default async function BedsPage(): Promise { redirect("/app/dashboard"); } - const data = await readData(); const organizationId = context.organization?.id; - const rooms = organizationId ? data.rooms.filter((room) => room.organizationId === organizationId) : data.rooms; - const beds = organizationId ? data.beds.filter((bed) => bed.organizationId === organizationId) : data.beds; - const admissions = organizationId - ? data.admissions.filter((admission) => admission.organizationId === organizationId) - : data.admissions; - const elders = organizationId ? data.elders.filter((elder) => elder.organizationId === organizationId) : data.elders; + const [rooms, beds, admissions, elders] = await Promise.all([ + listOperationalRooms(organizationId), + listOperationalBeds(organizationId), + listOperationalAdmissions(organizationId), + listOperationalElders(organizationId), + ]); return ( { redirect("/login"); } - const data = await readData(); const organizationId = context.organization?.id; - const elders = organizationId ? data.elders.filter((elder) => elder.organizationId === organizationId) : data.elders; - const beds = organizationId ? data.beds.filter((bed) => bed.organizationId === organizationId) : data.beds; - const admissions = organizationId - ? data.admissions.filter((admission) => admission.organizationId === organizationId) - : data.admissions; - const incidents = organizationId - ? data.incidents.filter((incident) => !incident.organizationId || incident.organizationId === organizationId) - : data.incidents; + const [elders, beds, admissions, incidents] = await Promise.all([ + listOperationalElders(organizationId), + listOperationalBeds(organizationId), + listOperationalAdmissions(organizationId), + listOperationalIncidents(organizationId), + ]); const activeElders = elders.filter((elder) => elder.status === "active").length; const pendingElders = elders.filter((elder) => elder.status === "pending").length; diff --git a/app/(app)/app/elders/page.tsx b/app/(app)/app/elders/page.tsx index abe3f37..422d4d9 100644 --- a/app/(app)/app/elders/page.tsx +++ b/app/(app)/app/elders/page.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; -import { readData } from "@/modules/core/server/store"; +import { listOperationalElders } from "@/modules/core/server/operations"; import { EldersClient } from "@/modules/elders/components/EldersClient"; export default async function EldersPage(): Promise { @@ -10,11 +10,7 @@ export default async function EldersPage(): Promise { redirect("/login"); } - const data = await readData(); - const organizationId = context.organization?.id; - const elders = organizationId - ? data.elders.filter((elder) => elder.organizationId === organizationId) - : data.elders; + const elders = await listOperationalElders(context.organization?.id); return ; } diff --git a/app/api/admissions/route.ts b/app/api/admissions/route.ts index 3242ae3..dc096e1 100644 --- a/app/api/admissions/route.ts +++ b/app/api/admissions/route.ts @@ -4,8 +4,8 @@ import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/ap import { recordAuditLog } from "@/modules/core/server/audit"; import { requirePermission } from "@/modules/core/server/auth"; import { getDatabase } from "@/modules/core/server/db"; +import { listOperationalAdmissions } from "@/modules/core/server/operations"; import { admissions, beds, elders } from "@/modules/core/server/schema"; -import { readData } from "@/modules/core/server/store"; type AdmissionRow = typeof admissions.$inferSelect; @@ -39,13 +39,8 @@ export async function GET(): Promise { return auth.response; } - const data = await readData(); - const organizationId = auth.context.organization?.id; - return jsonSuccess("入住记录已加载", { - admissions: organizationId - ? data.admissions.filter((admission) => admission.organizationId === organizationId) - : data.admissions, - }); + const admissionsData = await listOperationalAdmissions(auth.context.organization?.id); + return jsonSuccess("入住记录已加载", { admissions: admissionsData }); } export async function POST(request: Request): Promise { diff --git a/app/api/facilities/beds/route.ts b/app/api/facilities/beds/route.ts index 8de0d56..a868fb2 100644 --- a/app/api/facilities/beds/route.ts +++ b/app/api/facilities/beds/route.ts @@ -4,8 +4,8 @@ import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/ap import { recordAuditLog } from "@/modules/core/server/audit"; import { requirePermission } from "@/modules/core/server/auth"; import { getDatabase } from "@/modules/core/server/db"; +import { listOperationalBeds } from "@/modules/core/server/operations"; import { beds, rooms } from "@/modules/core/server/schema"; -import { readData } from "@/modules/core/server/store"; import type { BedStatus } from "@/modules/core/types"; const BED_STATUS_VALUES: BedStatus[] = ["available", "occupied", "maintenance", "disabled"]; @@ -19,8 +19,12 @@ function readString(source: Record, key: string): string { return typeof value === "string" ? value.trim() : ""; } -function readBedStatus(value: string): BedStatus { - return BED_STATUS_VALUES.find((status) => status === value) ?? "available"; +function readOptionalBedStatus(value: string): BedStatus | null | undefined { + if (!value) { + return undefined; + } + + return BED_STATUS_VALUES.find((status) => status === value) ?? null; } export async function GET(): Promise { @@ -33,11 +37,8 @@ export async function GET(): Promise { return auth.response; } - const data = await readData(); - const organizationId = auth.context.organization?.id; - return jsonSuccess("床位列表已加载", { - beds: organizationId ? data.beds.filter((bed) => bed.organizationId === organizationId) : data.beds, - }); + const bedsData = await listOperationalBeds(auth.context.organization?.id); + return jsonSuccess("床位列表已加载", { beds: bedsData }); } export async function POST(request: Request): Promise { @@ -65,6 +66,10 @@ export async function POST(request: Request): Promise { if (!roomId || !code) { return jsonFailure("房间和床位编号不能为空"); } + const status = readOptionalBedStatus(readString(body, "status")); + if (status === null) { + return jsonFailure("床位状态无效"); + } const database = getDatabase(); const roomRows = await database @@ -83,7 +88,7 @@ export async function POST(request: Request): Promise { organizationId, roomId, code, - status: readBedStatus(readString(body, "status")), + status: status ?? "available", notes: readString(body, "notes"), }) .returning(); diff --git a/app/api/facilities/rooms/route.ts b/app/api/facilities/rooms/route.ts index 2d0f4ca..1d21fb8 100644 --- a/app/api/facilities/rooms/route.ts +++ b/app/api/facilities/rooms/route.ts @@ -1,13 +1,11 @@ -import { randomUUID } from "node:crypto"; - -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; import { recordAuditLog } from "@/modules/core/server/audit"; import { requirePermission } from "@/modules/core/server/auth"; import { getDatabase } from "@/modules/core/server/db"; -import { campuses, buildings, floors, rooms } from "@/modules/core/server/schema"; -import { readData } from "@/modules/core/server/store"; +import { listOperationalRooms } from "@/modules/core/server/operations"; +import { floors, rooms } from "@/modules/core/server/schema"; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -18,10 +16,14 @@ function readString(source: Record, key: string): string { return typeof value === "string" ? value.trim() : ""; } -function readPositiveNumber(source: Record, key: string, fallback: number): number { +function readOptionalPositiveNumber(source: Record, key: string): number | null | undefined { const value = source[key]; + if (value === undefined || value === null || value === "") { + return undefined; + } + const numeric = typeof value === "number" ? value : Number(value); - return Number.isInteger(numeric) && numeric > 0 ? numeric : fallback; + return Number.isInteger(numeric) && numeric > 0 ? numeric : null; } export async function GET(): Promise { @@ -34,11 +36,8 @@ export async function GET(): Promise { return auth.response; } - const data = await readData(); - const organizationId = auth.context.organization?.id; - return jsonSuccess("房间列表已加载", { - rooms: organizationId ? data.rooms.filter((room) => room.organizationId === organizationId) : data.rooms, - }); + const roomsData = await listOperationalRooms(auth.context.organization?.id); + return jsonSuccess("房间列表已加载", { rooms: roomsData }); } export async function POST(request: Request): Promise { @@ -63,63 +62,37 @@ export async function POST(request: Request): Promise { const roomName = readString(body, "name"); const roomCode = readString(body, "code"); - if (!roomName || !roomCode) { - return jsonFailure("房间名称和编号不能为空"); + const floorId = readString(body, "floorId"); + if (!roomName || !roomCode || !floorId) { + return jsonFailure("房间名称、编号和楼层不能为空"); } + const capacityInput = readOptionalPositiveNumber(body, "capacity"); + if (capacityInput === null) { + return jsonFailure("房间容量需为正整数"); + } + const capacity = capacityInput ?? 1; const database = getDatabase(); - const createdRows = await database.transaction(async (transaction) => { - const campusRows = await transaction.select().from(campuses).where(eq(campuses.organizationId, organizationId)).limit(1); - const campus = - campusRows[0] ?? - ( - await transaction - .insert(campuses) - .values({ organizationId, name: "默认院区", address: "" }) - .returning() - )[0]; - if (!campus) { - throw new Error("院区初始化失败"); - } + const floorRows = await database + .select({ id: floors.id }) + .from(floors) + .where(and(eq(floors.id, floorId), eq(floors.organizationId, organizationId))) + .limit(1); + const floor = floorRows[0]; + if (!floor) { + return jsonFailure("楼层不存在", 404); + } - const buildingRows = await transaction.select().from(buildings).where(eq(buildings.campusId, campus.id)).limit(1); - const building = - buildingRows[0] ?? - ( - await transaction - .insert(buildings) - .values({ organizationId, campusId: campus.id, name: "默认楼栋" }) - .returning() - )[0]; - if (!building) { - throw new Error("楼栋初始化失败"); - } - - const floorRows = await transaction.select().from(floors).where(eq(floors.buildingId, building.id)).limit(1); - const floor = - floorRows[0] ?? - ( - await transaction - .insert(floors) - .values({ organizationId, buildingId: building.id, name: "默认楼层", level: 1 }) - .returning() - )[0]; - if (!floor) { - throw new Error("楼层初始化失败"); - } - - return await transaction - .insert(rooms) - .values({ - id: randomUUID(), - organizationId, - floorId: floor.id, - name: roomName, - code: roomCode, - capacity: readPositiveNumber(body, "capacity", 1), - }) - .returning(); - }); + const createdRows = await database + .insert(rooms) + .values({ + organizationId, + floorId: floor.id, + name: roomName, + code: roomCode, + capacity, + }) + .returning(); const room = createdRows[0]; if (!room) { diff --git a/app/api/settings/join-requests/[id]/route.ts b/app/api/settings/join-requests/[id]/route.ts index 2ffece9..1901374 100644 --- a/app/api/settings/join-requests/[id]/route.ts +++ b/app/api/settings/join-requests/[id]/route.ts @@ -64,18 +64,9 @@ export async function PATCH(request: Request, context: RouteContext): Promise { + const database = getDatabase(); + const [elderRows, activeAdmissionRows] = await Promise.all([ + database + .select() + .from(elders) + .where(organizationId ? eq(elders.organizationId, organizationId) : sql`true`) + .orderBy(desc(elders.createdAt)), + database + .select({ + bedCode: beds.code, + bedId: admissions.bedId, + elderId: admissions.elderId, + roomName: rooms.name, + }) + .from(admissions) + .innerJoin(beds, eq(beds.id, admissions.bedId)) + .innerJoin(rooms, eq(rooms.id, beds.roomId)) + .where( + organizationId + ? and(eq(admissions.organizationId, organizationId), eq(admissions.status, "active")) + : eq(admissions.status, "active"), + ) + .orderBy(desc(admissions.admittedAt)), + ]); + const bedLabelByElderId = new Map(); + for (const row of activeAdmissionRows) { + if (bedLabelByElderId.has(row.elderId)) { + continue; + } + + bedLabelByElderId.set(row.elderId, { + room: row.roomName, + bed: row.bedCode, + bedId: row.bedId, + }); + } + + return elderRows.map((row) => toElder(row, bedLabelByElderId.get(row.id))); +} + +export async function listOperationalRooms(organizationId?: string): Promise { + const database = getDatabase(); + const [roomRows, bedRows] = await Promise.all([ + database + .select() + .from(rooms) + .where(organizationId ? eq(rooms.organizationId, organizationId) : sql`true`) + .orderBy(desc(rooms.createdAt)), + database + .select() + .from(beds) + .where(organizationId ? eq(beds.organizationId, organizationId) : sql`true`), + ]); + const bedCounts = new Map(); + for (const bed of bedRows) { + const current = bedCounts.get(bed.roomId) ?? { occupied: 0, total: 0 }; + current.total += 1; + if (bed.status === "occupied") { + current.occupied += 1; + } + bedCounts.set(bed.roomId, current); + } + + return roomRows.map((room) => { + const counts = bedCounts.get(room.id) ?? { occupied: 0, total: 0 }; + return { + id: room.id, + organizationId: room.organizationId, + name: room.name, + code: room.code, + capacity: room.capacity, + status: room.status, + bedCount: counts.total, + occupiedBedCount: counts.occupied, + createdAt: iso(room.createdAt), + updatedAt: iso(room.updatedAt), + }; + }); +} + +export async function listOperationalBeds(organizationId?: string): Promise { + const database = getDatabase(); + const bedRows = await database + .select({ + bed: beds, + currentElderName: sql`( + select ${elders.name} + from ${admissions} + inner join ${elders} on ${elders.id} = ${admissions.elderId} + where ${admissions.bedId} = ${beds.id} and ${admissions.status} = 'active' + order by ${admissions.admittedAt} desc + limit 1 + )`, + roomName: rooms.name, + }) + .from(beds) + .innerJoin(rooms, eq(rooms.id, beds.roomId)) + .where(organizationId ? eq(beds.organizationId, organizationId) : sql`true`) + .orderBy(desc(beds.createdAt)); + + return bedRows.map((row) => ({ + id: row.bed.id, + organizationId: row.bed.organizationId, + roomId: row.bed.roomId, + roomName: row.roomName, + code: row.bed.code, + status: row.bed.status, + notes: row.bed.notes, + currentElderName: row.currentElderName ?? undefined, + createdAt: iso(row.bed.createdAt), + updatedAt: iso(row.bed.updatedAt), + })); +} + +export async function listOperationalAdmissions(organizationId?: string): Promise { + const database = getDatabase(); + const admissionRows = await database + .select({ + admission: admissions, + bedCode: beds.code, + elderName: elders.name, + roomName: rooms.name, + }) + .from(admissions) + .innerJoin(elders, eq(elders.id, admissions.elderId)) + .innerJoin(beds, eq(beds.id, admissions.bedId)) + .innerJoin(rooms, eq(rooms.id, beds.roomId)) + .where(organizationId ? eq(admissions.organizationId, organizationId) : sql`true`) + .orderBy(desc(admissions.admittedAt)); + + return admissionRows.map((row) => ({ + id: row.admission.id, + organizationId: row.admission.organizationId, + elderId: row.admission.elderId, + elderName: row.elderName, + bedId: row.admission.bedId, + bedCode: row.bedCode, + roomName: row.roomName, + status: row.admission.status, + admittedAt: iso(row.admission.admittedAt), + dischargedAt: row.admission.dischargedAt ? iso(row.admission.dischargedAt) : undefined, + notes: row.admission.notes, + })); +} + +export async function listOperationalIncidents(organizationId?: string): Promise { + const database = getDatabase(); + const incidentRows = await database + .select() + .from(systemIncidents) + .where(organizationId ? or(isNull(systemIncidents.organizationId), eq(systemIncidents.organizationId, organizationId)) : sql`true`) + .orderBy(desc(systemIncidents.createdAt)); + + return incidentRows.map((incident) => ({ + id: incident.id, + organizationId: incident.organizationId ?? undefined, + severity: incident.severity, + status: incident.status, + title: incident.title, + description: incident.description, + source: incident.source, + createdAt: iso(incident.createdAt), + updatedAt: iso(incident.updatedAt), + })); +}