Secuencias de Pantalla — Módulo Caja
Análisis Funcional: Módulo Caja — parte 5 de 7. ← Anterior · Índice · Siguiente: UX de múltiples cajas y fondeo →
Este documento recorre pantalla por pantalla el módulo Caja tal como corre hoy en main (E4, E4-multi y E4b, todas mergeadas): la ruta real, el permiso que la gatea, qué muestra, qué acciones ofrece, en qué estados puede quedar y a dónde lleva cada acción. Cada afirmación sobre comportamiento cita el archivo y la línea donde vive.
Lo que sigue sin implementar queda marcado explícitamente como 📋 propuesta de UX, sin implementar cuando es un diseño de este propio paquete de documentos que nadie pidió todavía, o como 📌 pendiente cuando es una pieza que el cliente sí pidió pero no entró en ningún alcance cerrado — nunca se marca así algo que ya corre en producción.
4.1 Mapa de navegación
Lectura del mapa: el módulo Caja no es un flujo lineal — es una pantalla operativa (/caja) alimentada por tres pantallas de configuración de Admin y por el Prefondeo País, y consumida por una pantalla de operación (/pago) que no tiene ningún vínculo visual con ella (la flecha punteada PAGO -.-> CAJA es un camino que el usuario tiene que descubrir leyendo un mensaje de error, no un link — ver 4.3). Prefondeo se convirtió en dos pantallas separadas desde el 2026-08-06 (/prefondeo y /prefondeo-pais), sin tabs ni layout compartido entre ellas — dos ítems de sidebar independientes.
4.2 Caja: ruta /caja
Ruta: /caja, nombre caja (frontend/src/router/index.ts:64-69). Gate de entrada: meta.module = 'caja', acción Leer. El guard global rebota a la primera ruta legible del usuario si no la tiene (frontend/src/router/index.ts:331-334). Ítem de sidebar: "Caja", ícono Banknote, visible solo con caja:read (frontend/src/layouts/AppLayout.vue:85).
Gates internos (la pantalla se ve entera, las acciones no):
| Acción | Permiso | Dónde se evalúa |
|---|---|---|
| Ver saldo, movimientos y exportar | caja:read | Ruta + CashBoxController::authorizeStationScope (backend/app/Http/Controllers/CashBoxController.php:621-638) |
| Abrir / Cerrar caja | apertura_cierre_caja:write | CajaView.vue:20 (canOperate) y CashBoxController.php:124,221 |
| Fondear / Ajustar | fondeo_caja:write | CajaView.vue:21 (canFund) y CashBoxController.php:273,336 |
4.2.1 Entrada y estados de carga
Al montar, la pantalla dispara en paralelo GET /caja/disponibles y GET /caja/motivos para FONDEO y AJUSTE (CajaView.vue:349-351). /caja/disponibles devuelve todas las cajas de todas las sucursales donde el usuario tiene caja:read, cada una con saldo, moneda y sesión abierta (CashBoxController::disponibles(), :49-88).
| Estado | Condición | Qué se ve | Cita |
|---|---|---|---|
| Cargando | loading = true | Texto centrado "Cargando..." | CajaView.vue:361 |
| Error de carga | Falla /caja/disponibles | "No se pudo cargar el estado de la caja. Probá de nuevo." en rojo, sin botón de reintento | CajaView.vue:77,362 |
| Vacío | cashBoxes vacío o null | "No se pudo determinar una caja con módulo Caja activo para tu usuario." | CajaView.vue:363-365 |
| Selector | más de una caja y ninguna elegida | Grilla de tarjetas, una por caja | CajaView.vue:31,368-387 |
| Operativa | una sola caja (auto) o tarjeta clickeada | Vista de saldo + movimientos | CajaView.vue:71-72,390 |
| Sin permiso de ruta | caja:read ausente | Nunca llega: el guard redirige antes de montar | router/index.ts:331-334 |
El estado vacío es el mismo mensaje para tres causas distintas: el país no tiene modulo_caja, la sucursal no tiene cajas dadas de alta, o el usuario no tiene sucursales asignadas. Ver Hallazgos.
4.2.2 Selector de cajas
Aparece solo con más de una caja disponible. Cada tarjeta es un <button> con: nombre de la sucursal (uppercase, chico), "Caja N", saldo con su moneda, y un badge Caja abierta / Caja cerrada con la paleta semántica (status-success / status-cancelled) (CajaView.vue:368-387).
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ CENTRAL │ │ CENTRAL │ │ SUCURSAL NORTE │
│ Caja 1 │ │ Caja 2 │ │ Caja 1 │
│ 12.500,00 │ │ 0,00 │ │ 3.200,00 │
│ ● Caja abierta │ │ ○ Caja cerrada │ │ ○ Caja cerrada │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘- Elegir una tarjeta → vista operativa de esa caja +
GET /caja/{id}/movimientos(CajaView.vue:83-86). - La elección no se persiste entre visitas a la pantalla: cada entrada a
/cajacon más de una caja vuelve a pedirla (decisión #2 del diseño de múltiples cajas). El porqué está en 5.1. - Con una sola caja no hay selector ni botón "Cambiar de caja" — se entra directo, exactamente como antes de E4-multi (
CajaView.vue:71-72). - No distingue "abierta por vos" de "abierta por otro usuario" — ambos badges son idénticos. Ver 5.1.3.
4.2.3 Vista operativa
Dos columnas (md:grid-cols-[1fr_1.4fr]): tarjeta de saldo y acciones a la izquierda, panel de movimientos a la derecha (CajaView.vue:390).
┌─ CENTRAL · Caja 1 ──────── Cambiar de caja ─┐ ┌─ Desde [ ] Hasta [ ] [Excel] [PDF] ───────┐
│ │ ├───────────────────────────────────────────┤
│ 12.500,00 │ │ TIPO │ MONTO │ CUÁNDO │ QUIÉN │
│ ● Caja abierta │ ├───────────────────────────────────────────┤
│ │ │ Pago │ -775,00 │ 02/08 10 │ back@… │
│ [ Cerrar caja ] │ │ TC 7.7500 │
│ ───────────────────────────────────────── │ │ MTCN 8834201921 │
│ MOVIMIENTO MANUAL │ │ Fondeo │ +100,00 │ 02/08 09 │ super@… │
│ [ Fondeo ][ Ajuste ] │ │ Fondeo inicial │
│ [ Monto ] │ └───────────────────────────────────────────┘
│ [ Motivo ▾ ] │ │ 8 movimientos [Anterior][Siguiente]│
│ [ Fondear ] │ └───────────────────────────────────────────┘
└─────────────────────────────────────────────┘Botón "Cambiar de caja" — solo con más de una caja disponible; vuelve al selector recargando /caja/disponibles (refresca saldos de paso) sin recargar la página (CajaView.vue:92-94,394-400).
Saldo — número grande, 2 decimales, formateado con Intl.NumberFormat('es-GT') con su moneda: desde el 2026-08-06, /caja/disponibles devuelve currency (CashBoxController.php:78, comentario explícito: "sin esto el frontend no puede rotular los montos") y CajaView.vue la usa tanto en el saldo (:50,55-57,402) como en cada movimiento con su propia moneda histórica (:591). La deuda que documentaba la versión anterior de este análisis —la moneda ausente en toda la pantalla— está resuelta.
Saldo residual en otra moneda — si el país cambió de moneda de pago y quedó plata en la anterior, una línea de advertencia lo muestra aparte, sin que desaparezca del cajón (CashBoxController::residualBalances(), CashBoxService.php:50-62; render en CajaView.vue:405-411).
Badge de estado — Caja abierta (verde) / Caja cerrada (gris), derivado de selected.session (CajaView.vue:413-420).
Botón principal, mutuamente excluyente (CajaView.vue:422-437):
| Estado de la caja | Botón | Estilo |
|---|---|---|
| Cerrada | Abrir caja | Sólido, acento de marca |
| Abierta | Cerrar caja | Contorno, alert-danger |
Sin apertura_cierre_caja:write no se renderiza ninguno de los dos (CajaView.vue:422).
Bloque "Movimiento manual" — visible solo con fondeo_caja:write y caja abierta (CajaView.vue:439). Es un solo formulario compartido por Fondeo y Ajuste, con un toggle segmentado arriba (:442-457). El criterio de diseño completo está en 5.2. El motivo sale de un NSelect cerrado —sin tag ni filterable— alimentado por el catálogo del backend (:494-499), y "Otro" despliega un campo de detalle obligatorio (:500-507).
Aviso de fondeo por encima del prefondeo disponible del país — banner inline persistente dentro de la tarjeta de saldo, con ícono TriangleAlert, que se muestra cuando el fondeo devuelve un warning y se descarta con el botón × (CajaView.vue:470-490). No hay auto-dismiss ni notificación de éxito duplicada: si hay warning, la notificación flotante "Caja fondeada" no se dispara (:288-294).
Panel de movimientos — GET /caja/{id}/movimientos, columnas Tipo / Monto / Cuándo / Quién. Los pagos muestran TC y MTCN inline debajo del tipo (CajaView.vue:575-580), y cualquier movimiento con motivo lo muestra en una línea chica atenuada, con el detalle si es "Otro" (:583-585). El monto se colorea verde si es ≥ 0 y rojo si es negativo (:587-592).
Paginado. El backend pagina de a 25 (CashBoxController::movimientos(), :100-102) y desde el 2026-08-06 la pantalla sí renderiza controles: total de movimientos, página actual y botones Anterior/Siguiente cuando hay más de una página (CajaView.vue:121-139,599-629). La versión anterior de este análisis documentaba esto como un bug de UI sin resolver — ya no lo es.
Estados del panel de movimientos:
| Estado | Qué se ve | Cita |
|---|---|---|
| Cargando | Fila única "Cargando..." | CajaView.vue:564-566 |
| Vacío | "Sin movimientos todavía." | CajaView.vue:567-569 |
| Error | Ninguno — falla en silencio y deja la tabla vacía, indistinguible del estado vacío real | CajaView.vue:130-138 |
Export — rango datetime-local desde/hasta (default: hoy 00:00 → ahora) y dos botones, Excel y PDF. El rango incluye hora a propósito: un arqueo de turno necesita minutos, no días (CajaView.vue:316-339, CashBoxController::reporte(), :531-606). Errores del export se muestran inline debajo de los botones (CajaView.vue:551). El PDF sale con membrete de marca vía ReportExportService, igual que Reportes y Auditoría. El export no ofrece filtrar ni exportar por tipo o motivo desde esta pantalla, aunque el backend ya acepta esos filtros (ver Hallazgos y 5.3.4).
4.2.4 Modal "Abrir caja"
Un solo paso, sin autorización de Supervisor y sin monto editable (CajaView.vue:635-672).
┌────────────────────────────────┐
│ Abrir caja │
│ │
│ Saldo inicial │
│ 12.500,00 │
│ Es el saldo con el que quedó │
│ la caja al cerrar la última │
│ vez. │
│ │
│ Si la plata que hay en la caja │
│ no coincide con este monto, │
│ abrí igual y registrá la │
│ diferencia como un ajuste. Si │
│ entra plata nueva, cargala │
│ como fondeo. │
│ │
│ [ Abrir caja ] [ Cancelar ] │
└────────────────────────────────┘- El saldo mostrado es
selected.balance— el mismo saldo derivado que ya trae/caja/disponibles, que es exactamente lo que hereda la apertura (CajaView.vue:642). - No distingue la primera apertura de una caja (saldo 0 "de verdad") de una apertura normal con saldo heredado: el texto es el mismo en los dos casos, a diferencia de lo que diseñaba 5.6 para el caso de primera apertura.
- No muestra la procedencia del saldo heredado (fecha y usuario del cierre anterior) — el texto es genérico ("Es el saldo con el que quedó la caja al cerrar la última vez"), sin la fecha/hora ni el email que proponía el documento 5. Ver Hallazgos.
- Un solo botón primario (
Abrir caja) y Cancelar; sin paso intermedio. - Errores del backend (
CASH_ALREADY_OPEN,USER_ALREADY_HAS_OPEN_CASH_BOX) se muestran inline dentro del modal (CajaView.vue:654). - Es un
<div class="fixed inset-0">a mano, sinrole="dialog",aria-modal, focus trap ni cierre conEscape(:635) — la reescritura de E4b no aprovechó para migrarlo aNModal. Sigue siendo la misma deuda de accesibilidad que documentaba la versión anterior de este análisis, ahora sobre un modal más simple. Ver 5.8.3.
4.2.5 Modal "Cerrar caja"
ConfirmDialog (NModal + NCard con role="alertdialog"), texto: "Se va a registrar el saldo final de la caja y no vas a poder cargar más movimientos hasta volver a abrirla. La plata queda en la caja: la próxima apertura arranca con ese mismo saldo.", botón de confirmación en variante danger (CajaView.vue:674-683).
Este texto ya refleja el modelo con arrastre de saldo — la mención antigua a "un retiro" y a que "la caja va a quedar en cero" se sacó cuando se implementó E4b.1, y el mensaje sobre un comprobante de cierre que no existe (ver 06, pregunta #9) se eliminó junto con ella: la notificación de éxito hoy es un simple Caja cerrada (:206), sin mencionar ningún comprobante.
Al confirmar, el backend registra closing_balance sin crear ningún movimiento (CashBoxController::cierre(), :221-271). A diferencia del diseño propuesto en 5.6.4, el diálogo implementado no repite el monto exacto ni el nombre de la caja en el cuerpo — es un texto fijo, no interpolado.
4.2.6 Errores del backend, y dónde los ve el usuario
Cada código de error del backend es un estado de UI distinto:
| Código | HTTP | Origen | Dónde aparece | Texto que ve el usuario |
|---|---|---|---|---|
CASH_ALREADY_OPEN | 409 | CashBoxController.php:137-141,203-208 | Inline en el modal de apertura | "Ya hay una sesión de caja abierta para esta caja." |
USER_ALREADY_HAS_OPEN_CASH_BOX | 409 | CashBoxController.php:156-163,209-214 | Inline en el modal de apertura | "Ya tenés la caja N de {sucursal} abierta a tu nombre. Cerrala antes de abrir otra." |
CASH_NOT_OPEN | 409 | CashBoxController.php:233-237 | Notificación flotante de error | "No hay una sesión de caja abierta para esta sucursal." |
CASH_SESSION_CLOSED | 422 | CashBoxController.php:285-289 (fondeo), :348-352 (ajuste) | Inline bajo el formulario de movimiento manual | "No se puede fondear/ajustar una caja sin sesión abierta." |
ADJUSTMENT_WOULD_GO_NEGATIVE | 422 | CashBoxController.php:360-365 | Inline bajo el formulario | "Este ajuste dejaría la caja en saldo negativo." |
PERMISSION_DENIED | 403 | CashBoxController.php:626-634,640-646 | Inline o notificación según la acción | "No tenés permiso para esta acción sobre esta caja." |
Ya no existe SUPERVISOR_INVALID: no hay segundo usuario que autenticar en la apertura.
CASH_ALREADY_OPEN ya dice "esta caja" en vez de "esta sucursal" — se corrigió al implementar E4b.1. CASH_NOT_OPEN sigue diciendo "esta sucursal": quedó desactualizado y es ahora la única inconsistencia entre los dos mensajes (antes los dos decían lo mismo por igual). Ver Hallazgos.
El aviso de fondeo por encima del prefondeo disponible del país (OVER_COUNTRY_PREFUNDING) no es un error: responde 201, el movimiento ya se creó, y se muestra como banner persistente dentro de la tarjeta de saldo, no en esta tabla de errores. Ver 4.2.3 y 5.4.
4.3 Pago: ruta /pago (lo que toca Caja)
Ruta: /pago, meta.module = 'pago' (router/index.ts:32-36). Es la pantalla operativa del rol Backoffice: buscar una transacción y pagarla o cancelarla.
Acá solo interesa su relación con Caja, y la relación es asimétrica: cuando el país tiene modulo_caja activo, cada pago descuenta la caja que el operador abrió a su propio nombre (TransactionController::applyCashBoxDiscount, backend/app/Http/Controllers/TransactionController.php:319-402) — pero la pantalla no muestra nada de eso.
4.3.1 Lo que hoy muestra la pantalla sobre Caja
Nada. PagoView.vue no consulta /caja/disponibles, no muestra saldo de caja, no muestra qué caja tiene abierta el operador, y no indica que un pago va a mover efectivo físico. La única mención de dinero es el monto de la transacción, formateado en USD fijo (PagoView.vue:170-172). La existencia de la caja se revela solo cuando el pago falla.
Tampoco muestra nada sobre documentos obligatorios de ADR-008 fuera del propio bloque DocumentacionPrePago (que sí tiene tratamiento dedicado, PagoView.vue:283-292) — eso no cambió.
4.3.2 Errores de saldo, sesión y documentos
| Código | HTTP | Cuándo | Tratamiento en la UI |
|---|---|---|---|
INVALID_STATUS_TRANSITION | 409 | Otro usuario resolvió la transacción primero | ✅ Tratamiento dedicado: notificación de warning + re-búsqueda automática (PagoView.vue:141-148) |
INSUFFICIENT_PREFUNDING | 422 | El Prefondeo País no cubre amount (solo en países sin módulo Caja) | ✅ Tratamiento dedicado: título propio y saldo disponible, sin exponer la comisión (PagoView.vue:149-156) |
MISSING_REQUIRED_DOCUMENTS | 422 | Faltan carta/identificación (ADR-008) — cubre la carrera de dos operadores confirmando casi a la vez | ✅ Tratamiento dedicado: título propio (PagoView.vue:157-166) |
NO_CASH_BOX_OPEN_FOR_USER | 422 | El usuario no tiene ninguna sesión OPEN a su nombre | ❌ Sin tratamiento propio: cae en el else genérico "No se pudo procesar el pago" con el mensaje del backend (PagoView.vue:167-173) |
CASH_BOX_COUNTRY_MISMATCH | 422 | La caja abierta es de otro país que la transacción | ❌ Mismo else genérico |
INSUFFICIENT_CASH_BALANCE | 422 | El saldo de la caja no cubre el monto convertido | ❌ Mismo else genérico |
NO_EXCHANGE_RATE | 422 | El país paga en moneda local y no hay TC vigente | ❌ Mismo else genérico |
El mensaje del backend para NO_CASH_BOX_OPEN_FOR_USER sí es bueno y accionable — "No tenés ninguna caja abierta a tu nombre — abrí una desde la pantalla Caja antes de pagar." (TransactionController.php:335) — pero llega como cuerpo de una notificación genérica, sin link a /caja. Con C5 implementado (el pago con módulo Caja activo ya no valida prefondeo), los tres errores de saldo/sesión de caja son hoy el único bloqueo posible de un pago en un país con Caja, y ninguno tiene tratamiento propio — se invirtió exactamente la prioridad de lo que sí está resuelto (INSUFFICIENT_PREFUNDING, que en Guatemala ya no puede ocurrir).
Propuesta de UX, sin implementar — mostrar en
/pagoun indicador permanente de la caja abierta ("Cobrando desde: Central · Caja 2 — saldo 12.500,00 GTQ") y dar aNO_CASH_BOX_OPEN_FOR_USER/CASH_BOX_COUNTRY_MISMATCH/INSUFFICIENT_CASH_BALANCEun tratamiento de primera clase, con CTA a/caja. El fundamento está en 5.1; sigue sin estar en ningún alcance escrito.
4.4 Prefondeo: dos rutas, dos pantallas
Desde el 2026-08-06 el prefondeo son dos pantallas separadas, sin tabs ni layout compartido — dos ítems de sidebar independientes ("Prefondeo Holding" con ícono Wallet, "Prefondeo País" con ícono Landmark, AppLayout.vue:79-80). La propuesta de este documento de usar un TabbedSectionLayout con redirect dinámico (ver la versión anterior de este análisis) no se adoptó — la solución real es más simple: dos rutas de primer nivel.
4.4.1 Prefondeo Holding: ruta /prefondeo
Ruta: /prefondeo, meta.module = 'prefondeo' (router/index.ts:49-54).
PrefondeoView.vue es la pantalla original de E4 re-rotulada, no reescrita: título "Prefondeo Holding" (:76), saldo global de todos los países en USD (PrefundingService::globalBalance(), backend/app/Services/PrefundingService.php:40-49; render PrefondeoView.vue:93-99), listado histórico de asientos y —solo con prefondeo:write— un alta de un solo campo (:101-120).
| Estado | Qué se ve | Cita |
|---|---|---|
| Selector de país | Solo si el usuario tiene acceso a más de uno | PrefondeoView.vue:83-89 |
| Cargando | Saldo en "..." y fila "Cargando..." | PrefondeoView.vue:98,133-135 |
| Error | Fila "No se pudo cargar el prefondeo. Probá de nuevo." | PrefondeoView.vue:34,136-138 |
| Vacío | "Sin asientos todavía." | PrefondeoView.vue:139-141 |
| Sin permiso de escritura | El bloque "Nuevo asiento" no se renderiza | PrefondeoView.vue:101 |
Validación de alta en el cliente: monto ≤ 0 → "Ingresá un monto mayor a 0." (:47-50). Éxito → "Asiento cargado." y recarga. No hay export ni paginado en esta pantalla, ni confirmación antes de cargar un asiento (:110-116) — un dígito de más se corrige con otro asiento y no tiene consecuencia operativa inmediata, a diferencia del asiento de Prefondeo País (ver 4.4.2).
Este saldo sigue siendo el que descuenta amount + fee en cada pago (TransactionController.php:218-231).
4.4.2 Prefondeo País: ruta /prefondeo-pais
Ruta: /prefondeo-pais, meta.module = 'prefondeo_pais' (router/index.ts:59-63). Permiso propio (prefondeo_pais), no compartido con la Holding — la opera tesorería (Supervisor y Admin en el seed), no necesariamente quien administra el pool de la Holding.
PrefondeoPaisView.vue muestra el saldo del país (solo principales, puede ser negativo) y, debajo, su desglose en dos números derivados:
PREFONDEO PAÍS — GUATEMALA
100.000,00 USD
Saldo del país
├─ Distribuido en cajas 40.000,00 USD
└─ Disponible para distribuir 60.000,00 USD
Repartir plata a una caja no baja el saldo del país — recién
baja cuando se paga una transacción.| Bloque | Fuente |
|---|---|
| Saldo del país | CountryPrefundingService::balance() (backend/app/Services/CountryPrefundingService.php:28-38) — puede ser negativo, se muestra en rojo (PrefondeoPaisView.vue:108-122) |
| Distribuido en cajas | CountryPrefundingService::distributedInCashBoxes() (:50-71) — suma los saldos de todas las cajas del país, filtrando por la moneda de pago vigente |
| Disponible para distribuir | La resta de los dos anteriores — puede ser negativo si las cajas ya tienen más plata que el país (:78-81) |
El desglose completo (CountryPrefundingController::index() → breakdown, :58-66) viaja en un solo request; PrefondeoPaisView.vue lo oculta cuando el país no tiene módulo Caja activo (showDistribution, :32) — en ese caso la pantalla se ve igual que Prefondeo Holding, solo el saldo, sin las dos filas derivadas.
Comparado con el diseño original de este paquete (5.7.2), lo implementado es más simple: sin badges nombrados (Sobregiro, Sobredistribuido), sin el link [Ver] hacia Reportes → Cajas, sin íconos Wallet/Banknote — solo color rojo sobre el número y un párrafo explicativo cuando corresponde (PrefondeoPaisView.vue:116-122,141-143). El bloque "Circuito de fondos" que proponía 5.7.4 no se implementó — ninguna pantalla conecta visualmente las tres capas hoy.
El alta de asiento es monto (>0) + motivo opcional en texto libre —no el catálogo cerrado de Caja, que es de otro dominio (PrefondeoPaisView.vue:147-171)— y tampoco pide confirmación antes de guardar, a diferencia de lo que proponía 5.7.3.
| Estado | Cita |
|---|---|
| Cargando | PrefondeoPaisView.vue:39,185-187 |
| Error | PrefondeoPaisView.vue:48,188-190 |
| Vacío | PrefondeoPaisView.vue:191-193 |
4.5 Reportes - tab Cajas: ruta /reportes/cajas
Ruta: /reportes/cajas, tercera tab de la sección Reportes, meta.module = 'reportes' (router/index.ts:102-110). Comparte permiso con Estado de Situación y Volumen: no hay un permiso separado para el reporte de Cajas (ver Hallazgos).
La pantalla tiene dos bloques, cada uno con su propio par de botones de export (frontend/src/views/reportes/CajasView.vue):
Filtros comunes (CajasView.vue:125-157): país (solo si el usuario tiene más de uno), PDV (opcional, default "Todos los PDVs"), y rango desde/hasta con hora. Cambiar cualquier filtro resetea el detalle a la página 1 y recarga los dos bloques (CajasView.vue:67-74).
Resumen — una fila por caja: PDV, Caja N, Saldo actual, Estado (badge Abierta/Cerrada), y totales del período: Fondeo, Ajuste, Pagos (cantidad · total) (CajasView.vue:188-227, CajaReportesController::buildResumenRows(), :186-218). Los totales se calculan por movimiento, no por sesión.
Detalle — log completo de movimientos de todas las cajas que matchean: Fecha, PDV, Caja, Tipo (con MTCN inline en los pagos), Monto, Usuario. Paginado real con n-pagination (CajasView.vue:250-291) — a diferencia del listado de /caja.
| Estado | Qué se ve | Cita |
|---|---|---|
| Cargando | "Cargando..." (reemplaza ambos bloques) | CajasView.vue:161 |
| Error | "No se pudo cargar el reporte de cajas. Probá de nuevo." | CajasView.vue:61,162 |
| Sin país elegido | Caja punteada: "Elegí un país para ver su reporte de cajas." | CajasView.vue:163-165 |
| Resumen vacío | "Sin cajas en este alcance." | CajasView.vue:202-204 |
| Detalle vacío | "Sin movimientos en este rango." | CajasView.vue:263-265 |
| Error de export | Línea roja arriba de todo, compartida por los cuatro botones | CajasView.vue:159 |
El detalle no muestra ni filtra por motivo de los movimientos manuales — ni columna en pantalla ni parámetro de filtro en CajaReportesController::resolveScope() (:260-307), aunque el export PDF/Excel de detalle sí incluye una columna "Motivo" (CajaReportesController::detalleExport(), :153,162). Sigue siendo un pendiente real: el catálogo cerrado de motivos (2.3/2.4) habilitó el filtro, pero nadie lo conectó acá — ver Hallazgos.
4.6 Admin - Ubicaciones - Estaciones / PDV: ruta /admin/ubicaciones/estaciones
Ruta: /admin/ubicaciones/estaciones, meta.module = 'abm_estaciones' (router/index.ts:201-206). Segunda tab de la sección Ubicaciones.
Es el único lugar donde se dan de alta cajas físicas. La columna "Cajas" de cada fila (EstacionesView.vue:221-234):
- Aparece solo para sucursales cuyo país tiene
modulo_cajaactivo; el resto muestra—(EstacionesView.vue:41-43,222,233). - Muestra la cantidad de cajas, o
…mientras carga. El conteo se pide con una request por sucursal elegible (GET /admin/estaciones/{id}/cajas), en paralelo, después del listado principal (EstacionesView.vue:45-57). - Con
abm_estaciones:writeagrega un botón "+ agregar caja", que pasa a "Agregando..." mientras corre (EstacionesView.vue:224-231). Crea la caja con el número secuencial siguiente, sin pedir nombre.
| Estado | Comportamiento | Cita |
|---|---|---|
| Conteo no disponible | Falla en silencio, la celda queda en … para siempre | EstacionesView.vue:52-55 |
| Alta fallida | Escribe en el error global del listado ("No se pudo agregar la caja."), arriba de la tabla, lejos del botón | EstacionesView.vue:64-66 |
| Sin permiso de escritura | La columna muestra el número, sin botón | EstacionesView.vue:225 |
No hay forma de ver qué números tienen las cajas, su saldo, ni de desactivar o borrar una caja —alcance mínimo deliberado, no pedido— así que agregar una caja de más es irreversible desde la UI. Ver Hallazgos.
Activar modulo_caja en el país crea automáticamente la Caja 1 de cada sucursal (CashBoxService::ensureCashBoxesForCountry, :89-95), así que esta pantalla se usa solo para la segunda caja en adelante.
4.7 Admin - Ubicaciones - Países: ruta /admin/ubicaciones/paises
Ruta: /admin/ubicaciones/paises, meta.module = 'abm_paises' (router/index.ts:195-200). Es la pantalla que enciende o apaga el módulo Caja entero para un país, y desde el 2026-08-06 también su moneda de pago.
El formulario lateral expone tres campos relevantes, todos solo en modo edición — un país recién creado nace con modulo_caja apagado (PaisesView.vue:198-227):
| Campo | Control | Regla |
|---|---|---|
modulo_caja | Checkbox "Módulo Caja activo", con texto de ayuda: "Para países sin EPOS (ej. Guatemala) — habilita saldo de caja por sucursal en moneda local, apertura/cierre diario y tipo de cambio. Apagado = comportamiento actual sin cambios." | PaisesView.vue:198-207 |
local_currency | Input de 3 letras (ISO 4217), visible solo si modulo_caja está tildado | PaisesView.vue:208-216 |
pago_en_moneda_local | Checkbox "Pagar en moneda local", visible solo si modulo_caja está tildado. Default apagado (paga en USD) | PaisesView.vue:217-227 |
Validación en el cliente: con el módulo activo, la moneda local es obligatoria y tiene que ser exactamente 3 letras (PaisesView.vue:60-71). Al guardar, local_currency se manda null y pago_en_moneda_local se fuerza a false si el módulo está apagado (:76-82).
Desactivar un país usa ConfirmDialog con aviso de alcance (PaisesView.vue:252-263). Apagar modulo_caja o encender pago_en_moneda_local, en cambio, no piden ninguna confirmación pese a que el segundo cambia la unidad en la que se mueve plata real en todas las sucursales del país — ver Hallazgos.
El texto de ayuda de pago_en_moneda_local implementado es más corto que el propuesto en la versión anterior de este análisis, pero cubre lo mismo: "Apagado, las transacciones se pagan en dólares y no hace falta tipo de cambio. Encendelo solo cuando el país tenga autorización para hacer cambio de divisa — a partir de ahí cada pago se convierte al TC vigente." (PaisesView.vue:222-226).
4.8 Admin - Ubicaciones - Tipo de Cambio: ruta /admin/ubicaciones/tipo-cambio
Ruta: /admin/ubicaciones/tipo-cambio, meta.module = 'tipo_cambio' (router/index.ts:211-215). Módulo de permiso propio, distinto de abm_paises: el Supervisor edita el TC sin ser Admin (decisión #16 de E4.3). Por eso el redirect de la sección Ubicaciones es dinámico y no fijo (router/index.ts:186-193).
- Selector de país, alimentado por
GET /tipo-cambio/paises, que devuelve solo países conmodulo_cajaactivo (TipoCambioView.vue:28-38). - "Vigente" arriba a la derecha, e historial completo debajo: Valor / Vigente desde / Cargado por (
TipoCambioView.vue:114-141). - Con
tipo_cambio:write, panel de alta con la leyenda que explica la inmutabilidad (:144-149).
| Estado | Qué se ve | Cita |
|---|---|---|
| Ningún país con Caja | "Ningún país tiene el módulo Caja activo todavía — activalo desde la pestaña Países para gestionar su tipo de cambio." | TipoCambioView.vue:100-103 |
| Sin TC cargado | "Todavía no se cargó un tipo de cambio para este país." | TipoCambioView.vue:122-124 |
| Error de carga | "No se pudo cargar el tipo de cambio. Probá de nuevo." | TipoCambioView.vue:121 |
| Sin permiso de escritura | El panel de alta no se renderiza | TipoCambioView.vue:144 |
| Error de carga de países | ❌ Sin manejo: loadCountries() no tiene catch, una falla deja la pantalla en el estado "ningún país con Caja activo", que es un mensaje falso | TipoCambioView.vue:28-38 |
Con pago_en_moneda_local implementado, esta pantalla dejó de ser un prerrequisito para operar en un país que paga en dólares: NO_EXCHANGE_RATE ya no puede dispararse si el país no tiene la bandera encendida (2.8). El texto de la pantalla no lo aclara — sigue siendo la misma redacción de antes de E4b, así que un Admin que la mire hoy no tiene forma de saber si está en uso o es configuración latente para cuando Guatemala consiga la autorización.
Hallazgos para preguntas abiertas
Cosas encontradas escribiendo este documento que no resolví acá y conviene llevar a 06-preguntas-abiertas-caja:
- El estado vacío de
/cajacubre tres causas distintas con un solo mensaje. "No se pudo determinar una caja con módulo Caja activo para tu usuario" (CajaView.vue:363-365) se muestra igual si el país no tienemodulo_caja, si la sucursal no tiene cajas dadas de alta, o si el usuario no tiene sucursales asignadas. - Un error al cargar los movimientos es indistinguible de "no hay movimientos". El
catchdeja el array vacío sin marcar el error (CajaView.vue:130-138) — el mismo problema quePagoView.vueya corrigió explícitamente para "Mis transacciones" (PagoView.vue:96-115). Falta aplicar ahí el criterio ya adoptado. CASH_NOT_OPENsigue diciendo "esta sucursal" donde el objeto es "esta caja" (CashBoxController.php:236), mientras queCASH_ALREADY_OPENya se corrigió a "esta caja" (:140,206). Es la única inconsistencia entre los dos mensajes hoy.- El modal de apertura no muestra la procedencia del saldo heredado (fecha y usuario del cierre anterior) — el texto es genérico, a diferencia de lo que diseñaba 5.6.2. Tampoco distingue la primera apertura de una caja de una apertura normal.
- Agregar una caja de más es irreversible desde la UI. No hay baja ni desactivación de cajas (
EstacionesView.vue:221-234), y el botón "+ agregar caja" no pide confirmación. - El reporte de Cajas no tiene permiso propio. Comparte
reportescon Estado de Situación y Volumen (router/index.ts:107-109), así que cualquiera que vea reportes ve saldos de caja de todas las sucursales de su alcance. - Apagar
modulo_cajao encenderpago_en_moneda_localno piden confirmación (PaisesView.vue:198-227), a diferencia de desactivar el país entero, que sí (:252-263) — y el efecto del segundo sobre la operatoria diaria (cambia la moneda en la que se mueve plata real) es comparable o mayor. - El motivo en Reportes → Cajas sigue sin filtrarse ni mostrarse en pantalla, pese a que el catálogo cerrado de motivos (2.3/2.4) ya habilita ese filtro en el backend de
/caja. El export de detalle sí incluye la columna, la pantalla y el filtro no. Ver 4.5. - "Distribuido en cajas" y "Disponible para distribuir" ya tienen endpoint —
CountryPrefundingService::breakdown()— pero el bloque "Circuito de fondos" que conectaría visualmente las tres capas (5.7.4) no se implementó. Sigue sin existir ninguna pantalla que muestre Holding → País → Caja de una sola pasada. /pagono tiene ningún tratamiento de UI para los errores de Caja, que hoy son el único bloqueo posible de un pago en un país con módulo Caja activo (PagoView.vue:167-173). Ver 4.3.2.
← Anterior · Índice · Siguiente: UX de múltiples cajas y fondeo →

