/** * 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 | 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 { 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>): Promise { 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); }