queue-med/server/services/autoAbsent.ts

159 lines
4.8 KiB
TypeScript

/**
* Auto-Absent Timer Service
* Vérifie toutes les 30 secondes les patients en statut "called" depuis plus de N minutes.
* Si autoAbsentMinutes > 0 pour le cabinet, le patient est marqué absent et le suivant est appelé.
*/
import { getDb } from "../db.js";
import { clinics, queueEntries } from "../schema.js";
import { eq, and, inArray } from "drizzle-orm";
import { childLogger } from "../_core/logger.js";
const log = childLogger("auto-absent");
// Socket.io helpers (utilise le global injecté par le serveur principal)
function emitToClinic(clinicId: number, event: string, data: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const io = (global as any).__socketIo;
if (io) io.to(`clinic:${clinicId}`).emit(event, data);
}
function emitToPatient(patientToken: string, event: string, data: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const io = (global as any).__socketIo;
if (io) io.to(`patient:${patientToken}`).emit(event, data);
}
let intervalId: ReturnType<typeof setInterval> | null = null;
/** Démarre le job de vérification des absents automatiques */
export function startAutoAbsentJob(): void {
if (intervalId) return; // already running
intervalId = setInterval(checkAutoAbsent, 30_000);
log.info("job started (interval 30s)");
}
/** Arrête le job */
export function stopAutoAbsentJob(): void {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
log.info("job stopped");
}
}
async function checkAutoAbsent(): Promise<void> {
const db = await getDb();
if (!db) return;
try {
// Récupérer tous les cabinets avec autoAbsentMinutes > 0
const activeClinics = await db
.select({
id: clinics.id,
autoAbsentMinutes: clinics.autoAbsentMinutes,
avgConsultationMinutes: clinics.avgConsultationMinutes,
name: clinics.name,
})
.from(clinics)
.where(and(
eq(clinics.isActive, true),
eq(clinics.isQueueOpen, true),
));
const clinicsWithTimer = activeClinics.filter(
(c) => (c.autoAbsentMinutes ?? 0) > 0
);
if (clinicsWithTimer.length === 0) return;
const now = new Date();
for (const clinic of clinicsWithTimer) {
const thresholdMs = (clinic.autoAbsentMinutes ?? 3) * 60 * 1000;
// Récupérer les patients en statut "called" pour ce cabinet
const calledEntries = await db
.select()
.from(queueEntries)
.where(and(
eq(queueEntries.clinicId, clinic.id),
eq(queueEntries.status, "called"),
));
for (const entry of calledEntries) {
if (!entry.calledAt) continue;
const elapsed = now.getTime() - entry.calledAt.getTime();
if (elapsed >= thresholdMs) {
// Marquer absent
await db
.update(queueEntries)
.set({ status: "absent" })
.where(eq(queueEntries.id, entry.id));
log.info(
{
clinicId: clinic.id,
ticketNumber: entry.ticketNumber,
elapsedMinutes: Math.round(elapsed / 60000),
},
"patient marked absent"
);
// Notifier le patient (s'il est encore connecté)
if (entry.patientToken) {
emitToPatient(entry.patientToken, "patient:auto_absent", {
ticketNumber: entry.ticketNumber,
message: `Votre ticket n°${entry.ticketNumber} a été annulé car vous n'étiez pas présent.`,
});
}
// Reordonner la file et notifier le cabinet
await reorderAndNotify(clinic.id, db);
}
}
}
} catch (err) {
log.error({ err }, "check failed");
}
}
async function reorderAndNotify(clinicId: number, db: Awaited<ReturnType<typeof getDb>>): Promise<void> {
if (!db) return;
// Récupérer les patients en attente pour ce cabinet
const waitingEntries = await db
.select()
.from(queueEntries)
.where(and(
eq(queueEntries.clinicId, clinicId),
eq(queueEntries.status, "waiting"),
));
// Renuméroter
let pos = 1;
for (const e of waitingEntries.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))) {
await db
.update(queueEntries)
.set({ position: pos })
.where(eq(queueEntries.id, e.id));
pos++;
}
// Calculer l'état de la file pour la notification WebSocket
const allActive = await db
.select()
.from(queueEntries)
.where(and(
eq(queueEntries.clinicId, clinicId),
inArray(queueEntries.status, ["waiting", "called", "in_consultation"]),
));
const state = {
totalWaiting: allActive.filter((e) => e.status === "waiting").length,
currentTicket: allActive.find((e) => e.status === "called")?.ticketNumber ?? null,
clinicId,
};
emitToClinic(clinicId, "queue:update", state);
emitToClinic(clinicId, "display:update", state);
}