Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

slots-engine

Motor de disponibilidad de citas. Le das el horario semanal de un negocio y lo que ya está ocupado, y te devuelve los huecos libres.

Es una función pura: sin red, sin base de datos, sin leer el reloj. Todo lo que necesita entra por parámetro, así que se puede testear entera y razonar sobre ella sin montar nada.

  • Cero dependencias. Solo Intl, que ya viene en el runtime.
  • Correcto en los cambios de hora. Un negocio que abre a las 9:00 abre a las 9:00 también el lunes siguiente al cambio de horario — aunque en UTC sea otra hora distinta.
  • TypeScript estricto, con noUncheckedIndexedAccess.
  • 32 tests, incluidos los casos de horario de verano y las zonas con desfase de media hora.

Extraído de Foreus, donde decide si un agente de voz le promete o no una hora a un paciente que ha llamado por teléfono.

Instalación

npm install slots-engine

Requiere Node 22 o superior.

Uso

import { findFreeSlots, isSlotAvailable, type BusinessHours } from "slots-engine";

// Lunes a viernes de 9 a 14 y de 16 a 20. Sábado por la mañana. Domingo cerrado.
const horario: BusinessHours = {
  monday:    [{ start: "09:00", end: "14:00" }, { start: "16:00", end: "20:00" }],
  tuesday:   [{ start: "09:00", end: "14:00" }, { start: "16:00", end: "20:00" }],
  wednesday: [{ start: "09:00", end: "14:00" }, { start: "16:00", end: "20:00" }],
  thursday:  [{ start: "09:00", end: "14:00" }, { start: "16:00", end: "20:00" }],
  friday:    [{ start: "09:00", end: "14:00" }],
  saturday:  [{ start: "10:00", end: "14:00" }],
};

const huecos = findFreeSlots({
  from: new Date(),
  to: new Date(Date.now() + 7 * 86_400_000),
  timeZone: "Europe/Madrid",
  hours: horario,
  busy: [                                     // citas ya reservadas
    { start: new Date("2026-08-17T08:00:00Z"), end: new Date("2026-08-17T09:00:00Z") },
  ],
  durationMinutes: 45,
  limit: 3,
});
// → [Date, Date, Date] — los tres primeros inicios libres

Y antes de escribir la reserva en la base de datos:

const sigueLibre = isSlotAvailable({
  start: huecoElegido,
  durationMinutes: 45,
  timeZone: "Europe/Madrid",
  hours: horario,
  busy: ocupacionActual,   // releída justo ahora
});

Los huecos que propone findFreeSlots pueden haber sido ocupados mientras el cliente decidía. isSlotAvailable es la comprobación de última hora — y aun así, la garantía real de que no habrá dos reservas para el mismo momento es un índice único en la base de datos. Esta librería calcula; no serializa.

API

findFreeSlots(input): Date[]

Devuelve los inicios de cita libres, alineados a la granularidad y con espacio para la duración completa.

Campo Tipo Por defecto Qué es
from / to Date Ventana de búsqueda. Nunca se proponen huecos anteriores a from.
timeZone string Zona IANA del negocio, p. ej. "Europe/Madrid".
hours BusinessHours Horario semanal. Un día ausente o vacío es día cerrado.
busy Interval[] Lo ya ocupado. No hace falta que venga ordenado ni sin solapes.
durationMinutes number Duración de la cita a encajar.
granularityMinutes number 30 A qué marcas se alinean los inicios.
limit number 3 Cuántos devolver como máximo.

isSlotAvailable(input): boolean

¿Cabe una cita de durationMinutes empezando exactamente en start? Comprueba a la vez el horario de atención y la ocupación.

Primitivas de intervalos

  • businessIntervals(from, to, timeZone, hours) — el horario semanal convertido a intervalos absolutos en UTC.
  • mergeIntervals(intervals) — une los solapados y los contiguos.
  • subtractIntervals(base, busy) — resta la ocupación de la disponibilidad.

Utilidades de fecha con zona horaria

zonedParts, zonedTimeToUtc, startOfLocalDay, weekdayKey, localDateKey, parseHhMm, minutesToHhMm, addMinutes.

Por qué la zona horaria es el problema difícil

Un Date de JavaScript es un instante absoluto, no una fecha de calendario. "Las 9:00 en Madrid" son las 07:00 UTC en agosto y las 08:00 UTC en enero.

Por eso aquí nunca se suman 24 horas para avanzar un día: se recalcula el día de calendario local y se vuelve a convertir a UTC. Y zonedTimeToUtc hace dos pasadas, porque el desfase que hay que aplicar depende del propio instante que se está calculando — usar el desfase de la medianoche daría una hora desplazada justo el día del cambio.

Es la clase de detalle que no falla en desarrollo y falla dos domingos al año en producción.

Tests

npm test

Licencia

MIT © Erik Goj

About

Motor puro de disponibilidad de citas: horario semanal + ocupación → huecos libres. Correcto en cambios de hora. Cero dependencias.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages