From 8167f6c901532132ead1a67a72fddc95750b4753 Mon Sep 17 00:00:00 2001 From: Beldor Date: Mon, 22 Jun 2026 22:42:43 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20scheduling=20en=20UTC=20expl=C3=ADcito?= =?UTF-8?q?=20para=20Argentina=20(UTC-3),=20sin=20depender=20del=20TZ=20de?= =?UTF-8?q?l=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getHours() en Docker devuelve UTC → slots aparecían 3hs antes (5:30 en vez de 8:30). Ahora usa getUTCHours/getUTCMinutes + offset -180min en todas las comparaciones. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/scheduling.ts | 77 +++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/src/lib/scheduling.ts b/src/lib/scheduling.ts index 8816563..13bfeb6 100644 --- a/src/lib/scheduling.ts +++ b/src/lib/scheduling.ts @@ -8,31 +8,54 @@ const FIN_M = 0; const INICIO_MINUTOS = INICIO_H * 60 + INICIO_M; // 510 const FIN_MINUTOS = FIN_H * 60 + FIN_M; // 1020 +// Argentina es siempre UTC-3 (sin horario de verano desde 2008) +const ARG_OFFSET_MIN = -180; + +// Minutos del día en hora Argentina (0-1439) +function minutosArgentina(fecha: Date): number { + const utcMin = fecha.getUTCHours() * 60 + fecha.getUTCMinutes(); + return ((utcMin + ARG_OFFSET_MIN) + 1440) % 1440; +} + +// Día de la semana en Argentina (0=Dom, 6=Sab) +function diaArgentina(fecha: Date): number { + const arg = new Date(fecha.getTime() + ARG_OFFSET_MIN * 60 * 1000); + return arg.getUTCDay(); +} + +// Componentes de fecha Argentina (para construir fechas UTC) +function fechaArgentina(fecha: Date): { year: number; month: number; date: number } { + const arg = new Date(fecha.getTime() + ARG_OFFSET_MIN * 60 * 1000); + return { year: arg.getUTCFullYear(), month: arg.getUTCMonth(), date: arg.getUTCDate() }; +} + +// Devuelve la fecha UTC correspondiente a las 8:30 Argentina del día que contiene `fecha` +function inicioJornada(fecha: Date): Date { + const { year, month, date } = fechaArgentina(fecha); + // 8:30 Argentina + 3h = 11:30 UTC + return new Date(Date.UTC(year, month, date, INICIO_H - ARG_OFFSET_MIN / 60, INICIO_M, 0, 0)); +} + function esDiaHabil(fecha: Date): boolean { - const dia = fecha.getDay(); + const dia = diaArgentina(fecha); return dia >= 1 && dia <= 5; } -function minutosDelDia(fecha: Date): number { - return fecha.getHours() * 60 + fecha.getMinutes(); -} - function horasDentroDeJornada(inicio: Date, fin: Date): boolean { - return ( - minutosDelDia(inicio) >= INICIO_MINUTOS && - minutosDelDia(fin) <= FIN_MINUTOS && - inicio.toDateString() === fin.toDateString() - ); + const iMin = minutosArgentina(inicio); + const fMin = minutosArgentina(fin); + const iArg = fechaArgentina(inicio); + const fArg = fechaArgentina(fin); + const mismodia = + iArg.year === fArg.year && iArg.month === fArg.month && iArg.date === fArg.date; + return mismodia && iMin >= INICIO_MINUTOS && fMin <= FIN_MINUTOS; } async function turnosSolapados(inicio: Date, fin: Date): Promise { const count = await prisma.turno.count({ where: { estado: { in: ["PENDIENTE", "CONFIRMADO"] }, - AND: [ - { fechaInicio: { lt: fin } }, - { fechaFin: { gt: inicio } }, - ], + AND: [{ fechaInicio: { lt: fin } }, { fechaFin: { gt: inicio } }], }, }); return count; @@ -51,33 +74,31 @@ export async function obtenerSlotsDisponibles( const ahora = new Date(); const duracionMs = duracionHoras * 60 * 60 * 1000; - const fechaLimite = new Date(ahora); - fechaLimite.setDate(fechaLimite.getDate() + diasAdelante); + const fechaLimite = new Date(ahora.getTime() + diasAdelante * 24 * 60 * 60 * 1000); + // Avanzar al próximo slot (:00 o :30 Argentina) a partir de ahora let cursor = new Date(ahora); - // Avanzar al siguiente slot de :00 o :30 - cursor.setMinutes(cursor.getMinutes() >= 30 ? 60 : 30, 0, 0); + const argMNow = minutosArgentina(cursor) % 60; + const avanzarMin = argMNow < 30 ? 30 - argMNow : 60 - argMNow; + cursor = new Date(cursor.getTime() + avanzarMin * 60 * 1000); + cursor.setUTCSeconds(0, 0); while (cursor < fechaLimite && slots.length < 20) { if (!esDiaHabil(cursor)) { - cursor.setDate(cursor.getDate() + 1); - cursor.setHours(INICIO_H, INICIO_M, 0, 0); + cursor = inicioJornada(new Date(cursor.getTime() + 24 * 60 * 60 * 1000)); continue; } - if (minutosDelDia(cursor) < INICIO_MINUTOS) { - cursor.setHours(INICIO_H, INICIO_M, 0, 0); + const minActual = minutosArgentina(cursor); + if (minActual < INICIO_MINUTOS) { + cursor = inicioJornada(cursor); continue; } const fin = new Date(cursor.getTime() + duracionMs); - if ( - fin.toDateString() !== cursor.toDateString() || - minutosDelDia(fin) > FIN_MINUTOS - ) { - cursor.setDate(cursor.getDate() + 1); - cursor.setHours(INICIO_H, INICIO_M, 0, 0); + if (!horasDentroDeJornada(cursor, fin)) { + cursor = inicioJornada(new Date(cursor.getTime() + 24 * 60 * 60 * 1000)); continue; }