feat: bootstrap taller-turnos project
Estructura inicial con Next.js 14, PostgreSQL/Prisma 7, autenticación por magic link (clientes) y JWT (admins), lógica de scheduling con 4 slots paralelos y API routes completas para cliente y panel admin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
const SLOTS_PARALELOS = 4; // 2 jefes × 2 autos c/u
|
||||
const HORA_INICIO = 8;
|
||||
const HORA_FIN = 18;
|
||||
|
||||
function esDiaHabil(fecha: Date): boolean {
|
||||
const dia = fecha.getDay(); // 0=Dom, 6=Sab
|
||||
return dia >= 1 && dia <= 5;
|
||||
}
|
||||
|
||||
function horasDentroDeJornada(inicio: Date, fin: Date): boolean {
|
||||
const h1 = inicio.getHours() + inicio.getMinutes() / 60;
|
||||
const h2 = fin.getHours() + fin.getMinutes() / 60;
|
||||
return h1 >= HORA_INICIO && h2 <= HORA_FIN;
|
||||
}
|
||||
|
||||
// Devuelve cuántos turnos activos se superponen con el rango dado
|
||||
async function turnosSolapados(inicio: Date, fin: Date): Promise<number> {
|
||||
const count = await prisma.turno.count({
|
||||
where: {
|
||||
estado: { in: ["PENDIENTE", "CONFIRMADO"] },
|
||||
AND: [
|
||||
{ fechaInicio: { lt: fin } },
|
||||
{ fechaFin: { gt: inicio } },
|
||||
],
|
||||
},
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
export interface SlotDisponible {
|
||||
fechaInicio: Date;
|
||||
fechaFin: Date;
|
||||
}
|
||||
|
||||
// Busca los próximos N slots disponibles para una duración dada (en horas)
|
||||
export async function obtenerSlotsDisponibles(
|
||||
duracionHoras: number,
|
||||
diasAdelante = 14
|
||||
): Promise<SlotDisponible[]> {
|
||||
const slots: SlotDisponible[] = [];
|
||||
const ahora = new Date();
|
||||
const duracionMs = duracionHoras * 60 * 60 * 1000;
|
||||
|
||||
const fechaLimite = new Date(ahora);
|
||||
fechaLimite.setDate(fechaLimite.getDate() + diasAdelante);
|
||||
|
||||
let cursor = new Date(ahora);
|
||||
// Empezar desde el siguiente slot redondo (media hora)
|
||||
cursor.setMinutes(cursor.getMinutes() >= 30 ? 60 : 30, 0, 0);
|
||||
|
||||
while (cursor < fechaLimite && slots.length < 20) {
|
||||
if (!esDiaHabil(cursor)) {
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
cursor.setHours(HORA_INICIO, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const hInicio = cursor.getHours() + cursor.getMinutes() / 60;
|
||||
if (hInicio < HORA_INICIO) {
|
||||
cursor.setHours(HORA_INICIO, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fin = new Date(cursor.getTime() + duracionMs);
|
||||
const hFin = fin.getHours() + fin.getMinutes() / 60;
|
||||
|
||||
if (hFin > HORA_FIN) {
|
||||
// Pasar al día siguiente
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
cursor.setHours(HORA_INICIO, 0, 0, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ocupados = await turnosSolapados(cursor, fin);
|
||||
if (ocupados < SLOTS_PARALELOS) {
|
||||
slots.push({ fechaInicio: new Date(cursor), fechaFin: new Date(fin) });
|
||||
}
|
||||
|
||||
// Avanzar 30 minutos
|
||||
cursor = new Date(cursor.getTime() + 30 * 60 * 1000);
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
export async function slotEstaDisponible(inicio: Date, fin: Date): Promise<boolean> {
|
||||
if (!esDiaHabil(inicio) || !horasDentroDeJornada(inicio, fin)) return false;
|
||||
const ocupados = await turnosSolapados(inicio, fin);
|
||||
return ocupados < SLOTS_PARALELOS;
|
||||
}
|
||||
Reference in New Issue
Block a user