0792ac3767
Deploy / deploy (push) Successful in 6m52s
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>
144 lines
4.4 KiB
TypeScript
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;
|
|
}
|