Files
iistwin/server/medschedule/routes.ts

465 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { medScheduleStorage } from "./storage";
interface MedRequest extends AuthenticatedRequest {
medOrganizationId?: number;
}
let cachedMedOrgId: number | null = null;
async function resolveMedOrganizationId(): Promise<number> {
if (cachedMedOrgId) return cachedMedOrgId;
cachedMedOrgId = await medScheduleStorage.ensureMedScheduleOrganization();
return cachedMedOrgId;
}
function requireMedOrganization() {
return async (req: MedRequest, res: Response, next: NextFunction) => {
const medOrgId = await resolveMedOrganizationId();
req.medOrganizationId = medOrgId;
// MedSchedule API работает в контексте org medschedule, а не текущей org пользователя
req.organizationId = medOrgId;
// Доступ разрешён либо членам org medschedule, либо администраторам приложения
if (req.user && req.user.organizationId !== medOrgId && req.user.appRole !== "admin") {
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(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);
}