La pregunta de si montar un agendador o pagar uno se responde casi siempre mirando el precio, que es justo el criterio que menos importa. Lo que decide es qué estás agendando y dónde quieres que acaben esos datos. Aquí verás los dos casos que se confunden todo el rato, qué ganas exactamente teniendo las citas en tu base de datos y los dos problemas difíciles que nadie prevé antes de empezar. Los ejemplos van en Laravel porque es una de nuestras especialidades en AndorraDev, pero el modelo de disponibilidad y los dos problemas difíciles son los mismos en cualquier stack.
Calendly resuelve un problema que no siempre es el tuyo
Antes de decidir conviene separar dos casos que se parecen mucho y no lo son.
Uno: eres un comercial que necesita coordinar su agenda real con la de gente de fuera, con reprogramaciones, recordatorios y sincronización con el calendario donde ya vives. Ahí Calendly hace exactamente lo que necesitas y montarlo tú es tirar el tiempo.
Dos: tienes una página de "solicitar demo" y quieres que el visitante elija un hueco sin salir de tu web. Aquí el agendador es un formulario de captación con calendario, y el dato que entra es un lead, no un evento de agenda. Es el caso en el que compensa, y por una razón que no es el precio: el lead se queda en tu base de datos, entra en el mismo buzón que el resto de tus formularios y no obliga al visitante a saltar a un dominio ajeno justo en el momento de convertir.
Este artículo va del segundo caso.
Un agendador es disponibilidad menos reservas
Esa frase es literalmente el modelo entero, y por eso cabe en tan poco código.
Declaras las franjas en las que atiendes, la duración de cada cita, el descanso entre citas, con cuánta antelación mínima se puede reservar y hasta cuántos días vista. Con eso generas los huecos posibles de un día y le restas los que ya están reservados. Lo que queda es lo que se pinta.
'demo' => [
'duration' => 30, // minutos por reunión
'buffer' => 0, // descanso entre reuniones
'days_ahead' => 21, // hasta dónde se puede reservar
'min_notice' => 120, // antelación mínima en minutos
// 1 = lunes ... 7 = domingo
'availability' => [
1 => [['10:00', '14:00'], ['16:00', '20:30']],
5 => [['10:00', '14:00']],
],
],
Declararlo en configuración en vez de en una interfaz de administración es una decisión, y para este caso es la correcta: tu disponibilidad comercial cambia dos veces al año, no todos los días. Construir un panel para editarla es más código que mantener que la propia funcionalidad.
Los huecos se calculan, no se almacenan
Si la disponibilidad es una resta, el lado izquierdo no hace falta guardarlo. Eso elimina de un plumazo la tabla de huecos, el proceso que la rellena y todos los problemas de sincronización que traen los dos.
No hay una tabla de huecos que haya que rellenar por adelantado ni un proceso nocturno que genere el calendario del mes que viene. Solo se guardan las reservas. Los huecos libres son una función de la configuración y de esa tabla, evaluada en el momento.
Eso significa que cambiar tu disponibilidad tiene efecto inmediato, que no hay nada que resincronizar y que no existe el estado inconsistente clásico de "la tabla de huecos dice una cosa y las reservas dicen otra".
Dos reservas simultáneas es el único problema difícil
Calcular huecos, restar reservas y pintar un calendario son operaciones deterministas: dan lo mismo hoy que dentro de un año. Solo hay un punto en todo el sistema donde el resultado depende de a qué velocidad ocurren las cosas.
Dos visitantes pueden estar mirando la misma pantalla y pulsar el mismo hueco en el mismo segundo. La solución que funciona tiene dos capas y hacen falta las dos.
La primera es un índice único en base de datos sobre el par (tipo de reunión, hora de inicio). Es la garantía dura: aunque toda la lógica falle, la base de datos no permite dos reservas para el mismo hueco.
La segunda es una recomprobación en el momento de confirmar, justo antes de guardar, para que el segundo usuario reciba un mensaje entendible en vez de un error:
if (! $this->generator()->isFree($this->selectedSlot)) {
$this->addError('selectedSlot', __('Ese hueco acaba de ocuparse.'));
$this->selectedSlot = null;
return;
}
Sin el índice, la comprobación sola tiene una ventana de carrera. Sin la comprobación, el índice solo produce un error feo. Con las dos, el caso está cubierto y además es amable.
El índice único sobre (tipo de reunión, hora de inicio) es la garantía dura contra la doble reserva, y conviene que siga siéndolo cuando amplíes. Si más adelante añades cancelaciones, decide entonces si el estado entra en el índice o si cancelar borra la fila. Tomada al ampliar, la decisión es trivial; tomada con datos dentro, ya no.
El huso horario es donde se rompen estos sistemas
Casi todos los agendadores que fallan lo hacen por lo mismo, y no es la concurrencia. Es que en algún punto alguien guardó una hora sin decir de dónde era, y a partir de ahí el error viaja sin que nadie lo note hasta que un cliente se presenta con una hora de diferencia.
Un agendador maneja tres relojes a la vez: el del organizador, que es donde están declaradas las franjas; el del visitante, que es donde va a estar cuando suene la cita; y UTC, que es donde hay que guardar todo para poder comparar.
La regla que evita el noventa por ciento de los problemas es sencilla: guarda siempre en UTC y muestra siempre diciendo en qué huso muestras. Un selector que pinta "10:00" sin decir de dónde es una trampa: el visitante de otro país elige creyendo que son las diez de su mañana y descubre la hora real en el correo de confirmación.
Ese cartelito de "horario de Andorra (CET)" junto al selector cuesta una línea y ahorra reuniones perdidas.
Dónde está el límite de un componente y empieza el producto
Aquí es donde se decide de verdad, así que conviene tenerlo delante. Un agendador ligero cubre el caso de captación entero y deja fuera, a propósito, cuatro cosas que son funcionalidad de producto.
La sincronización con tu calendario externo. El agendador conoce sus propias reservas, no las reuniones que tengas en Google Calendar. Para captación se resuelve declarando franjas algo más conservadoras que tu agenda real, y es la diferencia de fondo con una herramienta de suscripción.
La reprogramación autónoma. El correo de confirmación invita a responder, que para un volumen de demos comerciales es suficiente y además abre conversación. Si tu negocio son citas de servicio con volumen, ahí sí quieres un enlace y un flujo propio.
Los recordatorios y la invitación de calendario. Un trabajo programado y un archivo .ics. No es difícil y se añade cuando duelan las ausencias, que suele ser bastante después de lanzar.
El envío en cola. Los correos salen en el momento, que con volumen bajo es lo correcto y es una pieza menos que mantener. Cuando el volumen lo pida, encolar esa llamada es una línea.
Cuándo compensa cada opción
Con todo lo anterior sobre la mesa, la decisión se vuelve bastante limpia.
Compensa montarlo tú si el agendador es una página de captación, si tu disponibilidad es estable, si te importa que el visitante no salga de tu dominio y si el dato tiene que quedarse en tu base porque va a alimentar tu CRM. Son unas setecientas líneas y una tabla.
Compensa pagar la suscripción si necesitas sincronización real con el calendario donde vives, reprogramación autónoma, recordatorios automáticos, cobro en el momento de reservar o reparto entre varias personas. Todo eso es funcionalidad de producto, no de componente, y reconstruirla es un proyecto.
Y hay una tercera respuesta que es la que damos más veces: empezar por lo simple y añadir. Añadir recordatorios cuando duelan las ausencias, y sincronización cuando de verdad tengas conflictos de agenda, es mucho más barato que quitarte una dependencia externa a la que ya has enganchado tres integraciones.
Y si no trabajas con Laravel
Las decisiones se trasladan tal cual:
- Guarda solo las reservas y calcula los huecos. Una tabla de huecos precalculados es estado que se desincroniza.
- Pon el índice único en la base de datos, no solo la comprobación en el código.
- Todo en UTC, y di siempre en qué huso muestras.
- Reinicia y calcula de forma perezosa, en el momento de la petición, en vez de con tareas programadas.
- Y decide pronto si va a haber cancelaciones, porque condiciona el esquema.
Dónde está el código
El paquete está en Packagist, el código en GitHub y la documentación del paquete recoge todas las opciones. Es MIT, una sola tabla y sin dependencias de terceros.
Está en producción en la página de demo de Crowd Legal, donde cada reserva llega al mismo buzón que el resto de formularios de contacto, porque una demo agendada es un lead más.
Si tu negocio vive de las citas y esto se te queda corto, eso ya es otra cosa y es exactamente lo que hacemos en plataformas de reservas. Si nos cuentas el volumen y el flujo, te decimos si te vale un componente o necesitas producto.