import { Router } from "express"; import type { Express, Request, Response, NextFunction } from "express"; import type { Response as ExpressResponse } from "express"; import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; import { medScheduleStorage } from "./storage"; interface MedRequest extends AuthenticatedRequest { medOrganizationId?: number; } let cachedMedOrgId: number | null = null; async function resolveMedOrganizationId(): Promise { if (cachedMedOrgId) return cachedMedOrgId; cachedMedOrgId = await medScheduleStorage.ensureMedScheduleOrganization(); return cachedMedOrgId; } function medOrgResolver() { return async (_req: Request, _res: Response, next: NextFunction) => { try { await resolveMedOrganizationId(); next(); } catch (err) { next(err as Error); } }; } function requireMedOrganization() { return async (req: MedRequest, res: Response, next: NextFunction) => { const medOrgId = await resolveMedOrganizationId(); req.medOrganizationId = medOrgId; // Если запрос от аутентифицированного пользователя, убедимся, что он принадлежит к org MedSchedule if (req.user && req.user.organizationId !== medOrgId) { return res.status(403).json({ success: false, error: "Этот раздел доступен только пользователям MedSchedule", }); } next(); }; } function parseDate(value: unknown): Date | null { if (typeof value !== "string" || !value) return null; const d = new Date(value); if (isNaN(d.getTime())) return null; return d; } export function registerMedScheduleRoutes(app: Express) { const router = Router(); router.use(authenticateToken); router.use(tenantIsolation); router.use(medOrgResolver()); router.use(requireMedOrganization()); // ===================== // Family groups // ===================== router.get("/family", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const groups = await medScheduleStorage.getFamilyGroupsByUser(req.user!.id, orgId); res.json({ success: true, groups }); }); router.post("/family", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { name } = req.body; if (!name || typeof name !== "string") { return res.status(400).json({ success: false, error: "Название семьи обязательно" }); } const group = await medScheduleStorage.createFamilyGroup({ name, organizationId: orgId, createdBy: req.user!.id, }); res.status(201).json({ success: true, group }); }); router.get("/family/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const group = await medScheduleStorage.getFamilyGroupById(Number(req.params.id), orgId); if (!group) return res.status(404).json({ success: false, error: "Семья не найдена" }); const members = await medScheduleStorage.getFamilyMembersWithBotBindings(group.id, orgId); res.json({ success: true, group, members }); }); router.patch("/family/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const group = await medScheduleStorage.updateFamilyGroup(Number(req.params.id), orgId, req.body); if (!group) return res.status(404).json({ success: false, error: "Семья не найдена" }); res.json({ success: true, group }); }); router.delete("/family/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; await medScheduleStorage.deleteFamilyGroup(Number(req.params.id), orgId); res.json({ success: true }); }); router.post("/family/join", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { inviteCode } = req.body; if (!inviteCode || typeof inviteCode !== "string") { return res.status(400).json({ success: false, error: "Код приглашения обязателен" }); } const group = await medScheduleStorage.getFamilyGroupByInviteCode(inviteCode.toUpperCase(), orgId); if (!group) return res.status(404).json({ success: false, error: "Семья не найдена" }); // Создаём member для текущего пользователя const member = await medScheduleStorage.createFamilyMember({ familyId: group.id, userId: req.user!.id, profileName: `${req.user!.firstName ?? ""} ${req.user!.lastName ?? ""}`.trim() || "Я", role: "member", createdBy: req.user!.id, }); res.json({ success: true, group, member }); }); // ===================== // Family members // ===================== router.get("/family/:familyId/members", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const members = await medScheduleStorage.getFamilyMembersWithBotBindings(Number(req.params.familyId), orgId); res.json({ success: true, members }); }); router.post("/family/:familyId/members", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { profileName, profileType, dateOfBirth, gender, notes, role } = req.body; if (!profileName || typeof profileName !== "string") { return res.status(400).json({ success: false, error: "Имя обязательно" }); } const member = await medScheduleStorage.createFamilyMember({ familyId: Number(req.params.familyId), userId: null, profileName, profileType: profileType || "person", dateOfBirth: dateOfBirth || null, gender: gender || null, notes: notes || null, role: role || "member", createdBy: req.user!.id, }); res.status(201).json({ success: true, member }); }); router.patch("/family/:familyId/members/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const member = await medScheduleStorage.updateFamilyMember( Number(req.params.id), Number(req.params.familyId), orgId, req.body ); if (!member) return res.status(404).json({ success: false, error: "Участник не найден" }); res.json({ success: true, member }); }); router.delete("/family/:familyId/members/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; await medScheduleStorage.deleteFamilyMember(Number(req.params.id), Number(req.params.familyId), orgId); res.json({ success: true }); }); // ===================== // Medications // ===================== router.get("/medications", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, memberId } = req.query; let meds; if (memberId) { meds = await medScheduleStorage.getMedicationsByMember(Number(memberId), Number(familyId || 0), orgId); } else if (familyId) { meds = await medScheduleStorage.getMedicationsByFamily(Number(familyId), orgId); } else { return res.status(400).json({ success: false, error: "Требуется familyId или memberId" }); } res.json({ success: true, medications: meds }); }); router.post("/medications", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, memberId, name, dosage, form, schedule, instructions, color } = req.body; if (!familyId || !memberId || !name || !schedule) { return res.status(400).json({ success: false, error: "familyId, memberId, name и schedule обязательны" }); } const medication = await medScheduleStorage.createMedication({ familyId, memberId, name, dosage: dosage || null, form: form || null, schedule, instructions: instructions || null, color: color || "#4CAF50", createdBy: req.user!.id, }); res.status(201).json({ success: true, medication }); }); router.patch("/medications/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const medication = await medScheduleStorage.updateMedication(Number(req.params.id), familyId, orgId, req.body); if (!medication) return res.status(404).json({ success: false, error: "Лекарство не найдено" }); res.json({ success: true, medication }); }); router.delete("/medications/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); await medScheduleStorage.deleteMedication(Number(req.params.id), familyId, orgId); res.json({ success: true }); }); router.post("/medications/:id/generate-logs", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, from, to } = req.body; if (!familyId || !from || !to) { return res.status(400).json({ success: false, error: "familyId, from и to обязательны" }); } const logs = await medScheduleStorage.generateIntakeLogs( Number(req.params.id), new Date(from), new Date(to) ); res.json({ success: true, logs }); }); // ===================== // Intake logs // ===================== router.get("/intakes", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, from, to } = req.query; if (!familyId || !from || !to) { return res.status(400).json({ success: false, error: "familyId, from и to обязательны" }); } const intakes = await medScheduleStorage.getIntakesForDateRange( Number(familyId), orgId, new Date(from as string), new Date(to as string) ); res.json({ success: true, intakes }); }); router.post("/intakes/:id/taken", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, note } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const intake = await medScheduleStorage.markIntakeTaken(Number(req.params.id), familyId, orgId, note); if (!intake) return res.status(404).json({ success: false, error: "Приём не найден" }); res.json({ success: true, intake }); }); router.post("/intakes/:id/skipped", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, note } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const intake = await medScheduleStorage.markIntakeSkipped(Number(req.params.id), familyId, orgId, note); if (!intake) return res.status(404).json({ success: false, error: "Приём не найден" }); res.json({ success: true, intake }); }); router.post("/intakes/:id/snooze", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, minutes } = req.body; if (!familyId || !minutes) return res.status(400).json({ success: false, error: "familyId и minutes обязательны" }); const intake = await medScheduleStorage.snoozeIntake(Number(req.params.id), familyId, orgId, Number(minutes)); if (!intake) return res.status(404).json({ success: false, error: "Приём не найден" }); res.json({ success: true, intake }); }); // ===================== // Cabinet // ===================== router.get("/cabinet", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.query; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const items = await medScheduleStorage.getCabinetItems(Number(familyId), orgId); res.json({ success: true, items }); }); router.post("/cabinet", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { id, familyId, ...data } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const item = await medScheduleStorage.upsertCabinetItem( familyId, orgId, { id, ...data }, req.user!.id ); if (!item) return res.status(404).json({ success: false, error: "Не удалось сохранить" }); res.json({ success: true, item }); }); router.post("/cabinet/:id/stock", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, change, reason, note } = req.body; if (!familyId || change === undefined) { return res.status(400).json({ success: false, error: "familyId и change обязательны" }); } const item = await medScheduleStorage.addCabinetStock( Number(req.params.id), familyId, orgId, Number(change), reason || "manual", note, req.user!.id ); if (!item) return res.status(404).json({ success: false, error: "Позиция не найдена" }); res.json({ success: true, item }); }); router.delete("/cabinet/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); await medScheduleStorage.deleteCabinetItem(Number(req.params.id), familyId, orgId); res.json({ success: true }); }); // ===================== // Medical records // ===================== router.get("/records", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, memberId } = req.query; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const records = await medScheduleStorage.getMedicalRecords( Number(familyId), orgId, memberId ? Number(memberId) : undefined ); res.json({ success: true, records }); }); router.post("/records", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, memberId, title, recordType, recordDate, doctorName, clinicName, rawText, aiSummary, aiFindings, fileUrls } = req.body; if (!familyId || !memberId || !title || !recordType) { return res.status(400).json({ success: false, error: "familyId, memberId, title и recordType обязательны" }); } const record = await medScheduleStorage.createMedicalRecord({ familyId, memberId, title, recordType, recordDate: recordDate || null, doctorName: doctorName || null, clinicName: clinicName || null, rawText: rawText || null, aiSummary: aiSummary || null, aiFindings: aiFindings || null, fileUrls: fileUrls || null, addedBy: req.user!.id, }); res.status(201).json({ success: true, record }); }); router.patch("/records/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const record = await medScheduleStorage.updateMedicalRecord(Number(req.params.id), familyId, orgId, req.body); if (!record) return res.status(404).json({ success: false, error: "Запись не найдена" }); res.json({ success: true, record }); }); router.delete("/records/:id", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.body; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); await medScheduleStorage.deleteMedicalRecord(Number(req.params.id), familyId, orgId); res.json({ success: true }); }); // ===================== // Daily check-ins // ===================== router.get("/checkins", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId } = req.query; if (!familyId) return res.status(400).json({ success: false, error: "familyId обязателен" }); const checkins = await medScheduleStorage.getPendingCheckins(Number(familyId), orgId); res.json({ success: true, checkins }); }); router.post("/checkins", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, memberId, question, checkInDate } = req.body; if (!familyId || !question || !checkInDate) { return res.status(400).json({ success: false, error: "familyId, question и checkInDate обязательны" }); } const checkin = await medScheduleStorage.createDailyCheckin({ familyId, memberId: memberId || null, checkInDate, question, }); res.status(201).json({ success: true, checkin }); }); router.post("/checkins/:id/answer", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, answerText, aiSummary, aiAlert } = req.body; if (!familyId || !answerText) { return res.status(400).json({ success: false, error: "familyId и answerText обязательны" }); } const checkin = await medScheduleStorage.submitCheckinAnswer( Number(req.params.id), familyId, orgId, answerText, aiSummary, aiAlert ); if (!checkin) return res.status(404).json({ success: false, error: "Опрос не найден" }); res.json({ success: true, checkin }); }); // ===================== // Calendar // ===================== router.get("/calendar", async (req: MedRequest, res: ExpressResponse) => { const orgId = req.medOrganizationId!; const { familyId, from, to } = req.query; if (!familyId || !from || !to) { return res.status(400).json({ success: false, error: "familyId, from и to обязательны" }); } const intakes = await medScheduleStorage.getIntakesForDateRange( Number(familyId), orgId, new Date(from as string), new Date(to as string) ); const events = intakes.map((i) => ({ id: i.intake.id, title: i.medication.name, memberName: i.member.profileName, scheduledAt: i.intake.scheduledAt.toISOString(), status: i.intake.status, medicationId: i.medication.id, memberId: i.member.id, color: i.medication.color, })); res.json({ success: true, events }); }); app.use("/api/med", router); }