VDM — Virgen del Milagro

Cliente · 2026

Sistema de gestión que usa todos los días una empresa de transporte de pasajeros y turismo.

Demo interactiva próximamente. Escribime y te la muestro en vivo.Ver el flujo de un viajeCódigo privado

Estado
Uso diario desde agosto 2026
Rol
Diseño y desarrollo, solo
Período
Jun 2026 – hoy
rutas HTTP
71
API en Express y TypeScript, en 10 routers por feature
migraciones SQL
15
idempotentes y comentadas, con índices justificados
deploys del front a producción
55
Vercel, del 31 de julio al 9 de octubre de 2026
pantallas con acceso protegido
8
login sin contraseñas con lista blanca

El proyecto en 30 segundos

  1. El problema

    La oficina de una empresa de transporte de pasajeros y turismo organizaba la flota, los viajes y las rendiciones en planillas de papel y muy poco Excel.

  2. Lo que armé

    Un sistema de gestión propio: panel de ocupación de la flota, viajes con varios coches y choferes, cobros en pesos o dólares, rendición de cada chofer, reportes y planos de butacas. El navegador nunca habla con la base: todo pasa por una API que concentra las reglas.

  3. En qué quedó

    La oficina lo usa todos los días desde agosto de 2026. Todavía no tiene tests automatizados.

Qué pasa cuando la oficina carga un viaje

  1. Oficina (navegador)
    Asigna

    Elige un coche o un chofer que ya tiene otro viaje en esa franja.

  2. Panel (Next.js)
    Avisa

    Marca lo que ya está tomado mientras se asigna y muestra la alerta; la persona puede seguir.

  3. API (Express)
    Revisa

    Confirma el choque por intersección de intervalos, ignorando los viajes dados de baja, y responde 409 salvo que llegue «Crear igual».

  4. Base (Supabase)
    Espera

    No se guarda nada hasta que la persona confirma.


Resultado: El sistema avisa y la persona decide: sin confirmación explícita no se guarda, y con ella el viaje se crea igual.

Sobre el proyecto

Hasta agosto de 2026, la oficina de Virgen del Milagro organizaba sus viajes en planillas de papel. Diseñé y desarrollé solo el sistema que usa desde entonces todos los días, y propuse la mayoría de sus funcionalidades. El panel principal muestra la ocupación de la flota; cada viaje lleva varios coches y choferes, con una sugerencia de flota que cubre a los pasajeros con la menor cantidad de coches. El navegador nunca habla con la base: todo pasa por un backend que concentra la lógica de negocio.

Para tener en cuenta

No tiene tests automatizados ni CI: el build de Vercel y de Railway es el único control, y los flujos críticos los pruebo a mano. Las escrituras de varios pasos (baja de un viaje, rendiciones) no son transaccionales y el tope de cobros se chequea sin bloqueo, así que dos cobros simultáneos podrían competir. El editor de planos está operativo y el cliente todavía no cargó planos; el endpoint que se los da a mi SaaS está implementado, y no consta todavía su uso en producción. La sincronización entre usuarios es por consulta cada 5 segundos (no hay websockets). No hay cifras de rendimiento: no medí latencias ni hice pruebas de carga.

A · Operación

Flota, viajes y reservas

El sistema avisa y la persona decide.

409respuesta del servidor ante un choque de coche o chofer, salvo que la persona confirme «Crear igual»
  • Panel de ocupación de la flota: un carril por coche o por chofer, de «Hoy» hora por hora a «7 días», con la línea de «ahora» y cuántos están libres, en ruta o bloqueados (taller, licencia). La lógica es pura y está separada de React.
  • Cada viaje lleva varios coches y cada coche sus choferes, con alta en tres pasos.
  • Superposición: mientras se asigna, marca el coche o el chofer que ya está tomado; el servidor lo confirma por intersección de intervalos y rechaza salvo que llegue «Crear igual». Es un requerimiento de despacho: se avisa, no se bloquea.
  • Sugerencia de flota: propone qué coches libres mandar para cubrir a los pasajeros con la menor cantidad de coches y el menor sobrante de plazas (programación dinámica).
  • Revisión de cambios antes de guardar: lo anterior tachado, lo nuevo y lo que provoca (con qué viaje choca un coche, si alcanzan las plazas).
  • El estado del viaje (Pendiente, En viaje, Finalizado) se calcula con el reloj en cada lectura; la oficina puede fijarlo a mano, dar de baja un viaje y reactivarlo.
Ver detalle técnico
  • Filtros facetados con conteos, buscador global (Ctrl/Cmd+K) con fechas en lenguaje natural y calendario de día, semana y mes.
  • Se pensó primero para escritorio, que es como se usa casi siempre; en el celular el estado y el detalle de cada tramo suben como hoja inferior.
  • Next.js 16
  • React 19
  • CSS Modules
  • Programación dinámica
B · Cobros

Cobros en pesos o dólares y rendiciones

La moneda es del viaje, y lo cobrado se recalcula siempre desde los cobros.

ARS / USDun viaje pactado en dólares se cobra en dólares; la cotización se anota en cada cobro
  • Cobros parciales en cuotas, en la moneda del viaje; el equivalente en pesos se calcula solo si el viaje es en USD y hay cotización.
  • Tope: no se cobra más de lo pactado. Rechaza el cobro y dice cuánto queda, siempre comparando en la moneda del viaje.
  • La seña y el estado de pago (Impago, Señado, Pagado) se derivan de los cobros con un único escritor, también cuando cambia el precio del viaje.
  • Rendición del viaje: lo que se le dio al chofer, lo que gastó y el saldo (recibido menos gastos), dicho en palabras: si el chofer devuelve plata o hay que reintegrarle.
  • Gastos de ruta categorizados, con detalle por renglón, y una «cuenta dibujada» con barras punteadas para lo que todavía no se agregó.
Ver detalle técnico
  • Grilla de rendiciones con los viajes sin rendir y conciliación.
  • Migración de datos: la seña cargada en el alta pasó a ser el cobro Nº 1 real, con una tabla de respaldo y una consulta de control (migración idempotente).
  • PostgreSQL
  • Multimoneda
  • Zod
  • Rendiciones
C · Reportes

De la cifra al caso

Un aviso que dice «85 impagas» abre exactamente esas 85.

4períodos (mes, 6 meses, 12 meses, año) que gobiernan toda la pantalla
  • Embudo pactado, cobrado, egresos y resultado, con tendencia mes a mes y el reparto de egresos con los viajes que lo explican.
  • Detalle por viaje o agrupado por chofer, coche o cliente, con filtros por chofer, coche, cliente y quién cargó el viaje.
  • Avisos de lo que quedó abierto: viajes que ya salieron y deben plata, que volvieron sin rendir (en rojo desde los 50 días) y los de hoy sin coche o chofer. Cada aviso abre Reservas filtrado con el mismo criterio con que se contó.
  • Modo censura: un botón de ojo tapa todos los montos y el navegador recuerda la elección.
  • Exportación a CSV con separador y codificación pensados para que Excel en español lo abra bien.
  • Reportes
  • CSV
  • React 19
  • Huso horario de Argentina
D · Planos

Editor visual de planos de butacas

La pieza más compleja del front, y un contrato con otro sistema.

API keycomparada en tiempo constante, con rate limit propio (60 pedidos cada 5 minutos)
  • Dibuja la distribución de cada coche en uno o dos pisos (butacas común, semicama, cama y ejecutivo, baño, escalera, bodega), con la capacidad y el conteo por categoría en vivo.
  • Modelo puro de grilla (cada acción es una función de plano a plano), con deshacer y rehacer, zoom y paneo, imán y guías de alineación, copiar y pegar, menú de clic derecho y atajos de teclado.
  • Numera las butacas solo, avisa cuántas tocaría antes de hacerlo y marca números repetidos, piezas encimadas y butacas sin número.
  • Los coches iguales comparten una plantilla y un coche puntual puede tener un plano propio, con «volver a la plantilla».
  • Un endpoint le da el plano vigente de cada coche a mi SaaS (basecode.com.ar), protegido con una API key.
  • Editor visual
  • TypeScript
  • jsonb
  • API key
E · Backend y datos

Seguridad, base y sincronización

El navegador nunca habla con la base: todo pasa por una API que concentra las reglas.

RLSabierto a la clave pública: se reprodujo antes de cerrarlo y se verificó el cierre
  • Express es la única puerta a Supabase: la service role key no sale del servidor y la configuración se valida al arrancar (si falta una variable, el proceso no arranca).
  • El router maneja HTTP; el service valida con zod y lanza errores tipados: 4xx coherentes y 500 con una referencia que identifica el error.
  • Login sin contraseñas: código de 6 dígitos por mail y una lista blanca que se consulta antes de pedirlo y otra vez al verificarlo, y que falla cerrada. La sesión es un JWT con el algoritmo fijado y el acceso se revalida en cada pedido (caché de 30 segundos).
  • Gestión de accesos con salvaguardas: nadie puede suspenderse a sí mismo y nunca queda la lista sin un acceso activo.
  • Rate limit en capas (por IP, por usuario y por endpoint) y CORS por lista.
  • 15 migraciones SQL idempotentes, con CHECK que repiten la validación de la API y una función PL/pgSQL que reemplaza las asignaciones de un viaje en una sola transacción.
  • Pulso: los cambios de otras personas aparecen sin websockets. Cada 5 segundos, con la pestaña visible, el panel pide una marca por familia de datos y relee solo la que cambió, sobre una caché que se pinta al instante y se actualiza por detrás.
Ver detalle técnico
  • La auditoría de seguridad registró 13 hallazgos de seguridad y 15 de correctitud y rendimiento.
  • Si el pulso falla, el panel barre cada 30 segundos; cada familia falla por su cuenta.
  • Express
  • Supabase
  • PostgreSQL
  • JWT
  • Zod

Decisiones y qué resignan

Decisión humana

Se avisa, no se bloquea

Elegí
Marcar el choque y dejar que la persona decida (409 con «Crear igual»).

Resigné
Un choque real puede quedar guardado: la regla es de despacho, no del sistema.
Por qué

En una empresa de transporte hay casos en que un coche o un chofer se comparte a propósito. Por eso el servidor rechaza con un 409 y el contrato admite una confirmación explícita. Lo que se resigna es que el sistema, por sí solo, no garantiza que no haya choques.

Seguridad

Una sola puerta a la base

Elegí
Todo pasa por un backend Express; el navegador nunca habla con Supabase.

Resigné
Una pieza más para operar y un salto más en cada pedido.
Por qué

La service role key no sale del servidor, las reglas de negocio no se delegan al front y la configuración se valida al arrancar. A cambio hay que mantener y desplegar un backend aparte.

Simplicidad

Polling, no websockets

Elegí
Un pulso cada 5 segundos, solo con la pestaña visible, que devuelve una marca por familia.

Resigné
Hasta 5 segundos de demora, y una consulta de conteo exacto por latido que no medí.
Por qué

Con una o pocas personas cargando viajes a la vez, consultar cada 5 segundos es lo más simple y barato. Con más usuarios convendría Supabase Realtime, LISTEN/NOTIFY o SSE.

Consistencia

Lo cobrado se recalcula siempre

Elegí
Un único escritor deriva la seña y el estado de pago desde los cobros.

Resigné
Lee y escribe sin bloqueo: dos cobros simultáneos podrían competir.
Por qué

Así el estado de pago nunca queda desfasado del detalle de cobros, aunque cambie el precio del viaje. La deuda conocida es pasar el alta de cobros a una función transaccional con un índice único por viaje y número de cobro.

Seguridad

Login sin contraseñas con lista blanca

Elegí
Código por mail, lista que falla cerrada y acceso revalidado en cada pedido.

Resigné
Un cambio de acceso tarda hasta 30 segundos en aplicarse, y no hay roles: todos los usuarios ven todo.
Por qué

No hay contraseñas que filtrar y nadie entra sin invitación. La falta de roles es una decisión documentada para un equipo chico de oficina; se revisa si el equipo crece.

Lo que sigue

En este orden y por esta razón: primero poder cambiar sin miedo, después cerrar deudas, recién entonces medir.

  1. Ahora

    Hoy el build de Vercel y de Railway es el único control: sin red de seguridad no se puede cambiar con confianza.


    • Tests de las reglas puras (estado del viaje, sugerencia de flota) y de la superposición y el tope de cobros, con un cliente de Supabase de prueba.
    • GitHub Actions con tipos, lint, chequeo de diseño y tests en cada push.
  2. Después

    Cierra las deudas que ya declaro en «Para tener en cuenta».


    • Pasar el alta de cobros, las rendiciones y la baja de viajes a funciones transaccionales, con un índice único por viaje y número de cobro.
    • Versionar el DDL de las 9 tablas base, para poder reproducir la base fuera de producción.
  3. Más adelante

    Con la base estable tiene sentido medir y escalar; hoy no hay cifras de rendimiento y no las invento.


    • Una prueba de carga sobre el pulso y las reservas, con p95 y el plan de ejecución de la consulta de solapamiento.
    • Pasar la sincronización de polling a Supabase Realtime.

Cómo usé IA

Lo desarrollé con Claude Code, guiado por instrucciones propias: un CLAUDE.md con reglas duras (el navegador nunca habla con la base, la service role key no sale del backend, el vocabulario del dominio) y ocho reglas por área que se cargan según la ruta del archivo. Los cambios grandes salen de planes por fases, con una tabla fase → commit. El design system está escrito en un design.md y un chequeo propio de 15 invariantes en Node; cuando la primera versión del documento declaraba colores que el código no tenía, se reescribió con la regla «manda el código». Elijo el modelo según la tarea: uno más chico para los arreglos y uno más grande para el saneamiento del diseño.