159 lines
4.8 KiB
TypeScript
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);
|
|
}
|