Files
taller_automotriz/src/lib/scheduling.ts
T
Beldor 0792ac3767
Deploy / deploy (push) Successful in 6m52s
Add jefe mecánico to client booking flow
Cliente now picks a jefe mecánico after choosing the service so the
calendar shows that specific jefe's availability. Each jefe has its own
configurable parallel slot capacity (replacing the global SLOTS_PARALELOS
= 5). Admins manage jefes from /admin/jefes; jefes log in to /jefe to
see their daily agenda.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 22:28:44 -03:00

144 lines
4.4 KiB
TypeScript

import { prisma } from "./prisma";
const INICIO_H = 8;
const INICIO_M = 30;
const FIN_H = 17;
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 = diaArgentina(fecha);
return dia >= 1 && dia <= 5;
}
function horasDentroDeJornada(inicio: Date, fin: Date): boolean {
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,
jefeMecanicoId: string
): Promise<number> {
const count = await prisma.turno.count({
where: {
jefeMecanicoId,
estado: { in: ["PENDIENTE", "CONFIRMADO"] },
AND: [{ fechaInicio: { lt: fin } }, { fechaFin: { gt: inicio } }],
},
});
return count;
}
async function obtenerJefeActivo(jefeMecanicoId: string) {
return prisma.jefeMecanico.findUnique({
where: { id: jefeMecanicoId },
select: { slotsParalelos: true, activo: true },
});
}
export interface SlotDisponible {
fechaInicio: Date;
fechaFin: Date;
}
export async function obtenerSlotsDisponibles(
jefeMecanicoId: string,
duracionHoras: number,
diasAdelante = 14,
limite = 300
): Promise<SlotDisponible[]> {
const jefe = await obtenerJefeActivo(jefeMecanicoId);
if (!jefe || !jefe.activo) return [];
const cap = jefe.slotsParalelos;
const slots: SlotDisponible[] = [];
const ahora = new Date();
const duracionMs = duracionHoras * 60 * 60 * 1000;
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);
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 < limite) {
if (!esDiaHabil(cursor)) {
cursor = inicioJornada(new Date(cursor.getTime() + 24 * 60 * 60 * 1000));
continue;
}
const minActual = minutosArgentina(cursor);
if (minActual < INICIO_MINUTOS) {
cursor = inicioJornada(cursor);
continue;
}
const fin = new Date(cursor.getTime() + duracionMs);
if (!horasDentroDeJornada(cursor, fin)) {
cursor = inicioJornada(new Date(cursor.getTime() + 24 * 60 * 60 * 1000));
continue;
}
const ocupados = await turnosSolapados(cursor, fin, jefeMecanicoId);
if (ocupados < cap) {
slots.push({ fechaInicio: new Date(cursor), fechaFin: new Date(fin) });
}
cursor = new Date(cursor.getTime() + 30 * 60 * 1000);
}
return slots;
}
export async function slotEstaDisponible(
inicio: Date,
fin: Date,
jefeMecanicoId: string
): Promise<boolean> {
if (!esDiaHabil(inicio) || !horasDentroDeJornada(inicio, fin)) return false;
const jefe = await obtenerJefeActivo(jefeMecanicoId);
if (!jefe || !jefe.activo) return false;
const ocupados = await turnosSolapados(inicio, fin, jefeMecanicoId);
return ocupados < jefe.slotsParalelos;
}