Reportes
El PDF que se entrega al cliente y qué nunca aparece en él.
Hay dos clases de reporte, con audiencias opuestas.
1. Reportes internos
Sólo para administración. Muestran todo, incluidos costos y márgenes.
| Pantalla | Qué responde |
|---|---|
Panel (/admin) | ¿Cómo va hoy? ¿Y el mes? |
| Detalle de servicio | ¿Qué se hizo, quién, cuánto tiempo, cuánto dejó? |
| Ficha de propiedad | ¿Es rentable esta propiedad? |
| Ficha de cliente | ¿Cuánto factura este cliente? |
| Ficha de empleado | ¿Cuántas horas y cuánto se le debe? |
Reportes (/admin/reportes) | ¿Qué propiedades y clientes dejan más margen? |
| Pagos | ¿Qué hay que pagar y qué ya se pagó? |
Rentabilidad
Ingresos = precio base + insumos cobrados (sólo aprobados)
Costos = costo laboral + costo interno de insumos
Margen = Ingresos − Costos
Margen % = Margen ÷ Ingresos × 100Todo el cálculo está en
src/modules/finance/calculations.ts,
con tests.
Cifras parciales. Cuando falta un costo interno, el resultado se marca como parcial y se explica por qué:
«2 insumos no tienen costo interno registrado.»
Nunca se asume que un costo desconocido vale cero. Presentar un margen inflado como si fuera exacto sería peor que no presentarlo.
El margen es null, no cero, cuando los ingresos son cero: un margen sobre cero
no está definido y mostrar «0 %» sería una afirmación falsa.
Línea de tiempo
El detalle de servicio muestra una línea de tiempo derivada del registro de auditoría, no de una tabla de eventos paralela. Duplicar el historial garantizaría que algún día las dos versiones se contradigan.
09:58 Trabajo iniciado María
10:17 Tarea completada María · Limpiar cocina
10:52 Incidente reportado María
11:40 Servicio completado María
12:15 Horario corregido admin
«La empleada olvidó finalizar el servicio.»2. Reporte PDF para el cliente
Documento profesional pensado para compartir por correo o WhatsApp.
Qué incluye
- Marca, dirección y contacto de Royal Clean
- Cliente, propiedad y dirección
- Fecha, hora programada, horario real y duración
- Personal que atendió
- Cada tarea realizada, con sus fotografías antes y después
- Notas del personal
- Incidentes relevantes
- Insumos repuestos, por nombre y cantidad, sin importes
- Sello «SERVICIO COMPLETADO»
Qué NO incluye
- Importes de cobro (precio del servicio ni de los insumos): el monto lo fija la proforma
- Tarifas por hora
- Salarios ni costo laboral
- Costos internos de insumos
- Márgenes ni rentabilidad
- Notas administrativas
- Cualquier dato de otros clientes
Esto no se deja a la disciplina de quien programe: hay un test que lo
comprueba. Se crea un servicio con una tarifa distintiva (₡7.777/h) y un costo
interno distintivo (₡1.804), se generan los datos del reporte y se verifica que
esos números no aparecen por ninguna parte
(tests/integration/service-report.test.ts).
Imágenes incrustadas
El servidor descarga las fotografías de R2 y las incrusta en el PDF como datos.
La alternativa —enlazar a URLs firmadas— produciría un documento que deja de funcionar en cuanto expira la firma. Un reporte que el cliente abre tres días después mostraría huecos. Incrustar hace el documento autosuficiente.
Se incrustan todas las fotografías de cada tarea (un servicio puede llevar unas 30 entre antes y después). Se descargan por tandas y se maquetan en filas de cuatro, partiendo la tarea entre páginas si hace falta. Una imagen que no se puede recuperar se omite en lugar de romper la generación: es preferible un reporte con una foto menos que ningún reporte.
Generación
GET /api/reportes/servicio/{id}Sólo ADMIN. Se genera bajo demanda y no se almacena (ver DECISIONS.md DEC-010).
Cada generación incrementa reportVersion y anota reportGeneratedAt, con una
entrada de auditoría, para poder trazar cuántas veces se emitió un reporte y
cuándo.
El PDF se sirve con Content-Disposition: inline, así que se abre en el
navegador: el administrador lo revisa antes de decidir si lo comparte.
Con ?descargar=1 se sirve como attachment, que es lo que usan los botones
Descargar y Compartir. Se hace igual que con la cotización: en el
teléfono el visor de PDF no deja sacar el archivo hacia WhatsApp, que es por
donde se le manda al cliente. Cada variante (con o sin fotografías) ofrece
Ver, Compartir y Descargar. Compartir abre la hoja de compartir del
sistema con el PDF adjunto y sólo aparece si el navegador sabe hacerlo; nada se
envía sin que la persona lo toque. Generar el reporte para compartirlo cuenta
como una emisión más (reportVersion y auditoría).
El nombre del archivo (Propiedad_dd-MM-aaaa.pdf) lo calcula
buildReportFilename, en un módulo sin dependencias del renderizador para que
la ficha del servicio pueda pasárselo al botón de compartir.
Paginación
Un servicio con muchas tareas ocupa varias páginas. Cada tarea se marca con
wrap={false} para que no se parta por la mitad, y el pie repite «Página X de
Y» en todas.
Hay un test con 25 tareas que verifica que el documento tiene más de una página, es decir, que el contenido se reparte en lugar de recortarse.
Tipografía
Se usan las fuentes estándar de PDF (Helvetica), disponibles en todos los lectores sin incrustar nada. Mantiene el archivo pequeño y elimina cualquier dependencia de red durante la generación.
Filtros por rango de fechas
Pagos y Reportes ofrecen atajos —hoy, esta semana, este mes, últimos 30 días— y un rango libre.
No se asume una quincena fija: Royal Clean todavía no ha confirmado su ciclo de pago (PENDING_BUSINESS_RULES.md #1).
Rendimiento
- Ningún listado descarga imágenes a resolución completa: las miniaturas se cargan de forma diferida y sólo las visibles.
- Todos los listados están paginados.
- Las agregaciones financieras se calculan en el servidor, dentro de un
Suspensepropio, para que la parte operativa de la página aparezca antes.