465 lines
19 KiB
TypeScript
465 lines
19 KiB
TypeScript
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);
|
||
}
|