La validacion rechaza 0 pero el mensaje decia "entre 0 y 24". Ojo que
"entre 1 y 24" habria sido igual de incorrecto: duracionHoras es Float y la
UI muestra las duraciones de menos de una hora en minutos, asi que 0,5 (media
hora) es un valor valido y esperado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- crearTurno verificaba disponibilidad y creaba el turno en dos operaciones
sueltas: dos clientes podian pasar la verificacion a la vez y sobrevender el
mismo horario. Ahora va todo en una transaccion, serializada por jefe con
pg_advisory_xact_lock. slotEstaDisponible acepta el cliente de la transaccion.
Probado contra un Postgres real: 5 reservas simultaneas del mismo slot con
capacidad 1 dan 1 ok + 4 conflictos y una sola fila; sacando el lock, entran
las 5.
- La deduplicacion del webhook estaba en memoria y solo servia con una
instancia. Pasa a la base con el modelo MensajeWhatsappProcesado: el insert
del id es atomico por el indice unico, asi que aguanta reintentos
concurrentes de Meta (verificado). Se limpian los ids de mas de 7 dias.
- postMensaje no reintentaba: ahora reintenta 3 veces ante 429, 5xx y fallas
de red, con backoff exponencial y jitter. Un 4xx nuestro no se reintenta.
- /api/cliente/servicios devolvia los trabajos de un jefe dado de baja; ahora
responde 404 si el jefe no existe o no esta activo.
test:envio suma los casos de reintento (35 asserts). El Prisma en memoria del
test del bot ahora entiende $transaction y $executeRaw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Los trabajos ahora son por jefe mecanico, asi que el cliente elige primero
el mecanico y despues el trabajo, igual que en el bot de WhatsApp. Antes
listaba todos los servicios del taller y recien despues pedia el jefe.
- src/lib/servicios.ts: regla unica de que trabajos ofrece un jefe = los
suyos + los globales del taller (jefeMecanicoId null, los carga el admin).
Asi no se rompe lo que ya existe en produccion, donde todos los servicios
son globales.
- /api/cliente/servicios ahora exige jefeMecanicoId y aplica esa regla.
- /api/cliente/slots y crearTurno rechazan un trabajo propio de OTRO jefe
(400): antes se podia reservar con el jefe A la duracion del trabajo del B.
- whatsapp-bot: usa la misma regla, asi la web y el bot ofrecen lo mismo.
- cliente/turnos: pasos invertidos, cabeceras y back acordes, y estado vacio
propio cuando el jefe no tiene trabajos.
- test-whatsapp-bot: soporte de OR en el Prisma en memoria, trabajo global en
el seed y escenario D (globales visibles, trabajo ajeno rechazado). 31 asserts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ultima fase del bot de reservas: conecta la maquina de conversacion ya
existente (whatsapp-bot.ts) con la WhatsApp Business Cloud API.
- src/lib/whatsapp.ts: traduce RespuestaBot -> payloads de Meta (texto e
interactive/button, con los limites de 3 botones / 20 chars / 4096 chars),
aplana el payload del webhook y valida X-Hub-Signature-256 en tiempo
constante.
- src/app/api/whatsapp/webhook/route.ts: GET de verificacion y POST de
mensajes. Valida la firma sobre el cuerpo crudo, deduplica por message id
(Meta reintenta) y siempre devuelve 200 para no entrar en loop de retries.
- scripts/test-whatsapp-envio.ts (npm run test:envio): 30 asserts sobre la
capa Meta con fetch stubbeado, sin DB ni credenciales.
- docs/whatsapp.md con el alta en el panel de Meta y las 4 env vars.
Falta solo cargar las credenciales de Meta en el .env de produccion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- lib/turnos.ts: crearTurno() compartido (valida servicio/jefe/slot); cliente/turnos POST lo reutiliza
- lib/telefono.ts: normalización de teléfono (E.164 sin '+') para matchear el wa_id
- lib/whatsapp-bot.ts: procesarMensaje(telefono, texto), state machine completa:
identidad por teléfono + verificación por email registrado / alta nueva,
elegir mecánico (botones) -> trabajo (letras) -> horario (letras, con "más") -> confirmar,
crea turno sin auto/km; expiración 30min, "cancelar" global, manejo de inválidos y 409.
Devuelve objetos-respuesta (texto/botones) -> independiente de Meta (lo traduce F0).
- scripts/test-whatsapp-bot.ts + npm run test:bot: corre la máquina de estados con un
Prisma en memoria (sin DB ni Meta), ejercitando scheduling y crearTurno reales. 27/27 OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FASE 1 — schema:
- Cliente.email opcional; nuevo telefonoNorm (E.164, único) para matchear por WhatsApp
- TipoServicio.jefeMecanicoId opcional (trabajos per-jefe; globales legacy quedan null)
- Turno.autoId/kilometros opcionales (reservas por WhatsApp sin auto/km)
- Nuevo modelo ConversacionWhatsapp (estado del bot por teléfono)
- admin/turnos/[id]: no permite COMPLETADO sin auto+km; Sheets tolera email nulo
FASE 3 — panel de trabajos del jefe:
- API jefe/servicios (GET/POST) y jefe/servicios/[id] (PATCH/DELETE), scoped al jefe
- DELETE desactiva si el trabajo ya tiene turnos asociados
- Página /jefe/servicios (alta/listado/activar/editar/eliminar) + nav Turnos|Trabajos
- Dashboard del jefe resiliente a turnos sin auto/km ("a completar")
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Background fijo con tallermg.jpeg + overlay rgba(0,0,0,0.35) en globals.css
- Favicon actualizado usando src/app/icon.jpeg (convención Next.js App Router)
- Eliminado bg-gray-50 de layouts para que el fondo sea visible
- Título del sitio actualizado a "Taller MG"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si alguien ingresaba un km fuera del rango integer de Postgres la app
crasheaba silenciosamente — el cliente no veía ningún mensaje. Se agrega
validación de rango (1–1.000.000) en frontend y backend, y se muestra
el error de la API debajo del botón de confirmar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Muestra error inline si el cliente ingresa letras u otros caracteres,
y bloquea el botón de confirmar hasta que el valor sea válido.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reemplaza lista de slots por calendario cuadrado mensual
- Sábados y domingos grisados, días sin slots deshabilitados
- Click en día hábil abre modal con horarios disponibles
- Slot dot indicator en días con turnos disponibles
- Amplía búsqueda a 60 días / 500 slots para cubrir todo el mes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza HistorialServicio por Turno en ambas APIs y frontends (cliente y admin).
Muestra: servicio, fecha, horario, estado (badge), km al ingreso, observaciones del jefe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El rango A:N confundía a la API cuando había datos manuales en K/L (GASTOS/VARIOS),
haciendo que la nueva fila empezara en columna K en vez de A.
INSERT_ROWS siempre inserta fila nueva independiente del estado del sheet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getHours() en Docker devuelve UTC → slots aparecían 3hs antes (5:30 en vez de 8:30).
Ahora usa getUTCHours/getUTCMinutes + offset -180min en todas las comparaciones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agrega campo comentario opcional al marcar turno COMPLETADO (textarea en UI, guardado en DB, escrito en col N del sheet)
- Corrige mapeo Google Sheets: email→I, telefono→J, vacíos en K-M, comentario→N
- Actualiza scheduling: inicio 8:30, fin 17:00, máx 5 turnos simultáneos
- entrypoint.sh: migrate deploy → db push para aplicar cambios de schema automáticamente
- Historial cliente: tarjetas visuales con header, fecha y observaciones destacadas
- Dashboard/turnos cliente: mejoras responsive mobile
- Fix display duración: 0.5 hs → "30 min"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando el admin marca un turno como COMPLETADO escribe una fila
en el sheet con patente, km, auto, cliente, fecha y trabajo.
Auth via service account con JWT nativo (sin googleapis).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Turno.kilometros (requerido al reservar)
- Auto.nroChasis (opcional)
- Combobox con 60 marcas mundiales y modelos precargados
- Modelo se habilita al seleccionar marca
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza Resend por nodemailer con Gmail SMTP. El to: ahora usa
el email real del cliente en lugar del hardcodeado de sandbox.
Variables de entorno: GMAIL_USER y GMAIL_APP_PASSWORD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Páginas cliente: login magic link, registro, validación de token,
dashboard de autos, historial por auto y reserva de turnos con selector
de servicio + slots disponibles.
Páginas admin: login, dashboard de turnos del día con cambio de estado,
historial de autos (buscar cliente → auto → ver/cargar registros),
gestión de tipos de servicio y vista de clientes.
Middleware JWT para proteger rutas /cliente/* y /admin/*.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>