SOP — Módulo de Citas de Patio Abierto
Tipo: procedimiento operativo estándar. Estado: demo-ready (Garita Inc.2). Ruta:
/citas. Módulo:citas(lib/modules.ts). Este SOP describe la operación del agendamiento de arribos; NO cubre el registro de ingreso físico (ver garita) ni la integración con Supabase en Fase 2.
0. Propósito y alcance
El módulo de citas permite programar la llegada de camiones que transportan
contenedores al recinto. Una cita es un PLAN futuro (no un evento ocurrido): asocia
un contenedor esperado con una ventana de arribo y un acarreador. Su cumplimiento se
DERIVA del evento INGRESO_PATIO real; nunca se fabrica.
Alcance: programación de citas (crearCita), consulta de la cola ordenada
(getCitas + estadoCita) y derivación de estado en pantalla. Queda fuera: cancelación
de citas (estado cancelada — mutación explícita pendiente de Fase 2), integración con
Carta Porte o SAT, y acceso externo vía API.
1. Roles y permisos
| Rol | patio:ver | ingreso:registrar | Acceso a /citas | Puede agendar |
|---|---|---|---|---|
admin | SI | SI | SI | SI |
supervisor | SI | SI | SI | SI |
operador_patio | SI | SI | SI | SI |
operador_garita | SI | NO | SI (solo lectura) | NO |
cliente_readonly | SI | NO | SI (solo lectura) | NO |
Reglas de autorización:
- Todo usuario autenticado con permiso
patio:verDEBE poder ver la lista de citas de su tenant (tabla o tarjetas en móvil). - El formulario "Agendar cita" DEBE mostrarse únicamente a roles con
ingreso:registrar. El componente<Protect permiso={PERMISO.INGRESO_REGISTRAR}>lo oculta en la UI; la Server ActionagendarCitalo valida server-side de forma independiente (defensa real). cliente_readonlyyoperador_garitaNO DEBEN ver el formulario, pero DEBEN ver la cola de citas.
Fuente normativa:
lib/auth/roles.ts(PERMISOS_POR_ROL). Los roles son provisionales — a confirmar con el cliente antes de producción.
2. Precondiciones
Antes de ejecutar el procedimiento de agendamiento DEBEN cumplirse:
- El usuario DEBE tener sesión activa (Supabase Auth en producción; selector de rol demo en entorno de desarrollo).
- El usuario DEBE tener el permiso
ingreso:registrar(roles: admin, supervisor, operador_patio). - El código ISO 6346 del contenedor esperado DEBE estar disponible (11 caracteres:
ABCD1234567). - La ventana de arribo (inicio y fin) DEBE conocerse de antemano.
- La plataforma DEBE estar accesible (estado
VERDEdel semáforo de capacidad).
3. Procedimiento — agendar una cita
- Navegar a
/citasdesde el sidebar (módulo "Citas"). - Verificar que el formulario "Agendar cita" es visible. Si no lo es, el rol activo
no tiene
ingreso:registrar; escalar al supervisor. - Completar el campo Contenedor esperado (ISO 6346) con el código de 11 caracteres
(
ABCD1234567). El campo es obligatorio y DEBE pasar la validación ISO 6346 estricta. - Completar el campo Transportista con el nombre del acarreador de drayage (no la naviera). Campo obligatorio.
- Completar Ventana de arribo — inicio (
datetime-local). Obligatorio. - Completar Ventana de arribo — fin (
datetime-local). DEBE ser posterior al inicio. - Completar Placa del tractocamión (formato
ABC-12-34). Campo opcional. - Presionar Agendar cita. El botón queda deshabilitado durante el envío (spinner).
- Verificar el toast de confirmación:
ok: true→ "Cita agendada · {ISO}" — la cita aparece en la cola.ok: false→ el toast muestra el error; corregir el campo indicado y reintentar.
- Confirmar que la cita recién agendada aparece en la lista con estado
programada.
4. Procedimiento — consultar la cola de citas
- Navegar a
/citas. - La página carga
getCitas()+getEventosPatio()de forma paralela (Server Component,force-dynamic— nunca prerenderizado). - El estado de cada cita se DERIVA en el servidor con
estadoCita(cita, eventos, ahora):- programada: ventana no ha vencido y no existe
INGRESO_PATIOpara ese contenedor posterior acreadaAt. - cumplida: existe un evento
INGRESO_PATIOcon el mismocontenedor_isoyocurrido_at >= creadaAt. El ingreso real determina el estado; nunca se fabrica. - no_show: ventana ya venció (
ahora > ventanaFin) y no hubo ingreso. - cancelada: requiere mutación explícita (no disponible en esta fase).
- programada: ventana no ha vencido y no existe
- La lista se presenta ordenada por
ventanaInicioascendente (programadas primero). En móvil: tarjetas apiladas. En escritorio: tabla densa. - El código ISO de cada cita es un enlace a
/contenedor/{iso}para ver el detalle del contenedor.
5. Reglas e invariantes
Las siguientes reglas DEBEN cumplirse en todo momento:
- El estado de una cita NUNCA se almacena; se DERIVA de los eventos del patio en cada
carga. No DEBE haber un campo
estadopersistido en la cita. creadaAtse sella en el servidor al momento de llamarcrearCita(). El cliente NO DEBE enviarlo ni modificarlo.- La validación ISO 6346 DEBE ejecutarse en la Server Action (
agendarCita), conisoEstricto: true, independientemente de la validación de la UI. - El permiso
ingreso:registrarDEBE verificarse server-side enagendarCitaantes de cualquier mutación. El<Protect>de la UI es cosmético. - Las citas se aíslan por tenant (
rfc). Una cita creada por el tenant A NO DEBE ser visible para el tenant B. ventanaFinDEBE ser estrictamente posterior aventanaInicio. La action rechaza el caso contrario.- El transportista registrado DEBE ser el acarreador de drayage, no la naviera del contenedor.
- La cola de citas NO DEBE prerenderizarse (
export const dynamic = "force-dynamic"). Los estados derivados cambian con el tiempo real.
Lo que NO DEBE ocurrir:
- NO DEBE inventarse el estado
cumplidasin un eventoINGRESO_PATIOreal. - NO DEBE registrarse la misma cita dos veces (el operador DEBE verificar la cola antes de agendar).
- NO DEBE omitirse el contenedor o el transportista; son campos requeridos.
- NO DEBE aceptarse un código ISO con formato incorrecto (p.ej. letras minúsculas, longitud errónea, dígito verificador inválido).
6. Manejo de errores y escalamiento
| Error / mensaje | Causa probable | Acción |
|---|---|---|
| "Indica el contenedor esperado" | Campo vacío | Completar el código ISO. |
| "Código ISO 6346 inválido (esperado ABCD1234567)" | Formato incorrecto | Verificar el BL o el booking; corregir. |
| "Indica el transportista" | Campo vacío | Completar con el acarreador. |
| "Indica la ventana de arribo (inicio y fin)" | Fecha inválida o vacía | Seleccionar fecha/hora en ambos campos. |
| "El fin de la ventana debe ser posterior al inicio" | Fechas invertidas | Corregir ventanaFin. |
| "No tienes permiso para esta acción." | Rol sin ingreso:registrar | Escalar al supervisor o admin. |
| Toast de error no esperado / 500 | Error de servidor | Recargar la página; si persiste, escalar a G/CEO. |
Escalamiento:
- Errores de validación: el operador los resuelve con los datos correctos.
- Error 500 persistente o la lista no carga: escalar a G (ingeniería).
- Problema de permisos (rol incorrecto asignado): escalar al admin del tenant.
- Cita con estado incorrecto (p.ej.
programadacuando el camión ya llegó): verificar que el ingreso fue registrado en Garita (/garita). El estado se actualiza en la siguiente carga de/citas.
7. Notas de demo vs. producción
Las siguientes diferencias son ILUSTRATIVAS del modo demo y NO representan el comportamiento en producción.
| Aspecto | Demo (actual) | Producción (Fase 2) |
|---|---|---|
| Persistencia | In-memory en globalThis; se pierde al reiniciar el proceso serverless | Tabla citas en Postgres con FK a contenedor y tenant (Supabase) |
| Citas seed | 3 citas ilustrativas visibles solo para RFC_DEMO (ILUSTRATIVO) | Sin seed; datos reales del operador |
| Transportistas seed | "Transportes Manzanillo, S.A. de C.V.", "MCT Logistic", "TransNeyno" (ILUSTRATIVO) | Los que capture el operador; validables contra catálogo real |
| Cancelación | No implementada (cancelada no se puede derivar ni crear) | Mutación explícita + permiso; gate de negocio pendiente |
| Multi-process | Citas no comparten entre procesos Node distintos | Postgres como store compartido |
| Auditoría | creadaAt en el objeto; sin actor | Actor embebido (ActorEvento) en el registro de producción |
Gate D-011: todo dato presentado en pantalla DEBE ser capturado por el operador o derivado de eventos reales. Los datos seed son ILUSTRATIVOS y están marcados como tales en el código. En producción NO DEBE haber datos sintéticos.
8. Checklist operativo (uso en turno)
- Acceso a
/citascon el rol correcto verificado - Código ISO 6346 del contenedor confirmado (BL / booking)
- Transportista / acarreador identificado (no naviera)
- Ventana de arribo (inicio y fin) acordada con el transportista
- Cita agendada — toast
okrecibido - Cita visible en la cola con estado
programada - Al arribo: ingreso registrado en
/garita→ estado pasará acumplidaen la siguiente carga de/citas - Si el camión no llega antes de
ventanaFin: estado pasa automáticamente ano_show
Archivos clave: app/citas/page.tsx · components/patio/cita-form.tsx ·
components/patio/actions.ts (agendarCita) · components/patio/demo-store.ts
(crearCita, getCitas) · lib/patio/citas.ts (estadoCita, ordenarCitas) ·
lib/auth/roles.ts (PERMISOS_POR_ROL)