Consulta de Resultados (Angular • MVVM)
1) ¿Qué es este módulo y qué hace?
El módulo Consulta de Resultados permite al usuario consultar resultados de distintos tipos de estudio (ej. Veterinario anillo en leche, Aflatoxina M1 Quimico) aplicando filtros por:
- Tipo de estudio
- Rancho (con regla de negocio opcional para permitir o no “Todos”)
- Tanque
- Fecha (a través de una combinación: Año + (Mes opcional) + (Día opcional))
Y presenta los resultados en:
-
Tabla (Angular Material
mat-table) con paginación (mat-paginator). -
3 gráficas (Chart.js) basadas en un resumen del backend:
- Barras (conteo por categoría/rango)
- Pastel (porcentaje por categoría/rango)
- Barras apiladas (porcentaje por categoría/rango mes a mes)
Alcance (lo que SÍ cubre)
- Carga inicial del dataset anual para el usuario (según RFC en
localStoragey validación contraAdministracionService.getSocios). - Filtrado en memoria para la tabla.
- Consulta al backend para el resumen de gráficas (con optimización: reusa datos anuales para no llamar de más).
- UX de loading, empty states y notificación (snackbar) cuando no hay datos.
- Modo mocks para pruebas locales sin backend.
Fuera de alcance (por diseño actual)
- No hay componentes “hijos” tipo
list/form/filter. Todo está orquestado por un único componente de página. - No existe un canal tipado
error$hacia UI: el service atrapa errores y devuelve arreglos vacíos.
2) Rutas reales y estructura del módulo
En tu proyecto, la raíz es
src/app/y dentro viven carpetas transversales (Interceptors,model,service,view) que contienen archivos por módulo.
src/app/
Interceptors/
keycloudAuth.interceptor.ts
model/
ConsultaResultados/
api-envelope.model.ts
chart.model.ts
constantes.model.ts
current-user.model.ts
data-result.model.ts
icatalogs.mock.ts
rancho.model.ts
tanque.model.ts
strategy/
study-strategy.model.ts
categorical.model.ts
numeric-range.model.ts
service/
consultaResultados.service.ts
consulta-resultados/
study-strategy.service.ts
view/
consulta-resultados/
consulta-resultados.component.ts
consulta-resultados.component.html
consulta-resultados.component.scss
paginator/
paginator-es.intl.ts
viewmodels/
consulta-resultados.viewmodel.ts
3) Arquitectura MVVM (flujo obligatorio)
Este feature respeta el flujo:
Service → ViewModel → Component.ts → Component.html
-
Service (
ConsultaResultadosService)- Llama endpoints HTTP.
- Normaliza/deduplica filas.
- Construye catálogos derivados de ranchos/tanques.
- Soporta mocks.
-
ViewModel (
ConsultaResultadosViewModel)- Mantiene el estado del módulo en
BehaviorSubjects. - Aplica filtros (tabla) en memoria.
- Orquesta la carga de datos para gráficas.
- Aplica estrategias (Strategy Pattern) según el tipo de estudio.
- Incluye un “scheduler” para evitar m últiples llamadas (debounce + cola).
- Mantiene el estado del módulo en
-
View (
ConsultaResultadosComponent+ HTML)- Maneja el
FormGroupy eventos UI. - Conecta
MatTableDataSourceconMatPaginator. - Renderiza/destruye instancias de Chart.js.
- Muestra loading/empty/snackbar.
- Maneja el
Diagrama de flujo (simplificado)
[HTML] (selectionChange / submit / limpiar / dateInput)
↓
[Component.ts] (onTipoEstudioChange, onRanchoChange, aplicar, refrescar...)
↓
[ViewModel] (setX + beginBatch/endBatch + requestApply)
├─ filterTableInMemory() // Tabla: IN-MEMORY
└─ loadChartsDataFromService() // Gráficas: HTTP + Strategy
↓
[BehaviorSubjects] registros$ / barData$ / pieData$ / stackedData$
↓
[Component subscriptions] → MatTableDataSource + render charts
4) Interceptor de autenticación (dependencia transversal)
KeycloudAuthInterceptor
Ruta: src/app/Interceptors/keycloudAuth.interceptor.ts
Por su ubicación y dependencias (AuthService, shouldAttachAlpHeaders, KC_TOKEN_ENDPOINT) se considera transversal/global (no específico del feature).
Qué hace
-
No intercepta la llamada al endpoint de token (
KC_TOKEN_ENDPOINT). -
Evalúa
needsAlpHeaders = shouldAttachAlpHeaders(req.url).-
Si
false, no agrega nada y deja pasar la request. -
Si
true, llamaauth.ensureValidToken()y agrega:Authorization: Bearer <token>ALP_AUTH: Bearer <token>
-
Asegura
Content-Type: application/jsonenPOST/PUT/PATCHsi no existe.
-
-
Manejo de 401:
- Hace
auth.clearToken() - Vuelve a pedir
ensureValidToken() - Reintenta una sola vez la misma request.
- Hace
Cómo afecta a este módulo
Los métodos HTTP de ConsultaResultadosService no adjuntan token manualmente. Para que el módulo funcione:
-
shouldAttachAlpHeaders()debe devolvertruepara URLs como:.../gc/v1/api/v1/catalogo/funcion/fn_reporte_estudios.../gc/v1/api/v1/catalogo/funcion/fn_reporte_estudios_totales
Si esas rutas no entran en la regla, verás errores 401 o respuestas vacías.
Sugerencia de soporte: cuando falle auth, revisar consola (el interceptor loguea
needsAlpHeaders?y un prefijo del token).
5) View (Componentes)
5.1 ConsultaResultadosComponent
Rutas:
src/app/view/consulta-resultados/consulta-resultados.component.tssrc/app/view/consulta-resultados/consulta-resultados.component.htmlsrc/app/view/consulta-resultados/consulta-resultados.component.scss
Selector: app-consulta-resultados
Provider local: providers: [ConsultaResultadosViewModel] (VM por instancia)
Responsabilidad
- Declarar y validar el Reactive Form (
filtrosForm). - Traducir eventos de UI → métodos del
ViewModel. - Sincronizar UI con el estado del VM (cuando el VM corrige rancho/tanque).
- Cargar y refrescar
MatTableDataSource. - Renderizar/destruir gráficas Chart.js.
- Controlar UX de loading/empty/notificaciones.
Formulario (controles y eventos)
| Control (formControlName) | UI | Evento | Acción |
|---|---|---|---|
tipoEstudio | mat-select | (selectionChange) | vm.onTipoEstudioChanged(value) |
ranchoId | mat-select | (selectionChange) | vm.onRanchoChanged(value) |
tanqueId | mat-select | (selectionChange) | vm.onTanqueChanged(value) |
anio | mat-select | (selectionChange) | onAnioChange(year) → vm.setYear() + vm.reloadForYear() + aplicar |
mes | mat-select | (selectionChange) | onMesChange(month | null) → habilita/deshabilita día + vm.setMes() + aplicar |
dia | mat-datepicker (solo día) | (dateInput) | onDiaChange(Date | null) → vm.setDia(day) + aplicar |
| Submit | Botón “Aplicar” | (ngSubmit) | aplicar('post') (recarga año y aplica) |
| Reset | Botón “Limpiar” | (click) | refrescar() (resetea filtros y recarga) |
Paginación
-
Se usa
MatTableDataSource<DataResult>. -
Se enlaza a
MatPaginatorcon un@ViewChild(MatPaginator) set paginator(...).- Esto es útil cuando el paginator aparece/desaparece por
*ngIfy evita fallos de sincronización.
- Esto es útil cuando el paginator aparece/desaparece por
Gráficas (Chart.js)
-
Hay 3
<canvas>:#barCanvas,#pieCanvas,#stackedCanvas. -
El componente se subscribe a:
vm.barData$→renderBar()vm.pieData$→renderPie()vm.stackedData$→renderStacked()
-
Si no hay datos, destruye la instancia previa (
chart.destroy()). -
Se usa
cdr.detectChanges()antes de renderizar para asegurar que el<canvas>exista (especialmente con*ngIf).
Loading / Empty / Notificaciones
-
vm.loading$controla:- spinner (
mat-progress-spinner) - skeleton de tabla
- deshabilitar controles del formulario (método
setFiltersDisabled())
- spinner (
-
Empty state:
- Si
!loadingyregistros$está vacío, muestra “Sin datos en el periodo seleccionado”.
- Si
-
Notificación (snackbar):
-
Cuando una acción de usuario tipo “Aplicar” resulta en 0 registros, se muestra:
Estimado accionista no cuenta con información
-
Tiene “gate” para evitar notificaciones fantasma:
- solo si estás en la ruta del módulo
- solo si ya terminó la carga inicial
- solo si la última acción fue “apply”
- solo una vez por “firma” de filtros (para no repetir).
-
6) ViewModel (estado, streams, funciones y lifecycle)
6.1 ConsultaResultadosViewModel
Ruta: src/app/view/consulta-resultados/viewmodels/consulta-resultados.viewmodel.ts
Streams principales (API pública para la vista)
| Stream | Tipo | Propósito |
|---|---|---|
loading$ | BehaviorSubject<boolean> | UX de carga; también dispara disable/enable de filtros en el componente. |
registros$ | BehaviorSubject<DataResult[]> | Datos visibles en tabla (ya filtrados). |
ranchos$ | BehaviorSubject<RanchoItem[]> | Catálogo de ranchos para combo. |
tanquesUi$ | BehaviorSubject<number[]> | Lista de tanques visible según rancho actual. |
ranchoId$ / tanqueId$ | BehaviorSubject<number | 'ALL'> | Filtros vivos. |
dateStart$ / dateEnd$ | BehaviorSubject<Date | null> | Rango vivo para filtrar tabla. |
year$ / mes$ / dia$ | BehaviorSubject<number | null> | Estado de fecha (para gráficas + key anti-repetición). |
estudiosDisponibles$ | BehaviorSubject<string[]> | Lista dura de estudios (actualmente 2). |
tipoEstudioSelected$ | BehaviorSubject<string> | Estudio seleccionado. |
barData$ / pieData$ / stackedData$ | BehaviorSubject<... | null> | Salidas para gráficas. |
Regla de negocio: permitir “Todos los ranchos”
-
allowAllRanchos(privado) se controla desde el componente vía:vm.setAllowAllRanchos(flag)
-
Si NO se permite
ALL, el VM fuerza el primer rancho disponible.
Lifecycle (carga inicial)
Método: initFromLocalStorage(year)
loading$ = true- Obtiene metadata del estudio actual (
METADATA_ESTUDIO[currentStudyName]). - Llama
api.loadAllDataForCurrentUser(template, year). - Guarda dataset anual en memoria:
allRows. - Expone combos (
ranchos$,tanques$). - Valida selección de rancho con
ensureValidRanchoSelection(). loading$ = false
Nota: el “apply” real no lo dispara aquí; lo dispara el componente al final de
ngOnInit().
Scheduler anti-múltiples llamadas (debounce + cola)
Para evitar “tormenta” de peticiones cuando se actualizan varios filtros:
-
beginBatch()/endBatch(true)- Agrupa cambios y dispara 1 solo apply al final.
-
requestApply()- aplica debounce (
APPLY_DEBOUNCE_MS = 150) y agendarunApplyIfNeeded().
- aplica debounce (
-
runApplyIfNeeded()- evita repetición por
lastAppliedKey. - soporta
applyInFlightyapplyPending.
- evita repetición por
La “firma” (buildApplyKey()) incluye: estudio, year/mes/día, rancho/tanque, dateStart/dateEnd.
Aplicación real de filtros: aplicarFiltrosCore()
Secuencia:
filterTableInMemory()→ publicaregistros$.loadChartsDataFromService()→ publicabarData$,pieData$,stackedData$.
Filtrado en memoria (tabla): filterTableInMemory()
Orden actual:
-
Si falta
dateStartodateEnd:- limpia
registros$y los datasets de gráficas.
- limpia
-
Parte de
allRows(dataset anual). -
Filtra por:
- Tipo de estudio:
r.tipo_estudio === currentStudy(match exacto) - Rancho si no es
ALL - Fechas usando
fecha_registro(helperisYmdBetweenque comparaYYYYMMDDlocal)
- Tipo de estudio:
-
Reconstruye índice de tanques (para el combo) en base a los registros visibles.
-
Asegura que el tanque actual sea válido.
-
Filtra por tanque si no es
ALL. -
Mapea a
DataResulty publicaregistros$.
Importante: el filtro de fechas se hace sobre
fecha_registro(nofecha_muestra).
Carga de gráficas: loadChartsDataFromService()
Obtiene valores vivos del VM: tipoEstudio, y, m, d, r, t.
-
mesBackend = (m !== null) ? m + 1 : null(UI 0–11 → backend 1–12) -
Hace una llamada base anual:
annualData = api.getReporteEstudios(tipo, y, null, null, r, t)- Se usa como
contextData(para la apilada).
Define focusData para barras/pie:
| Escenario | Llamadas | focusData |
|---|---|---|
| Año completo (sin mes) | 1 | annualData |
| Mes seleccionado (sin día) | 1 | annualData.filter(item => item.mes === mesBackend) |
| Día seleccionado | 2 | Llama al backend con (mesBackend, d) para detalle diario. |
Finalmente:
strategy.processStatsFromSummary(contextData, focusData)- Publica
barData$,pieData$,stackedData$.
7) Services (endpoints, payloads, responses, errores)
7.1 ConsultaResultadosService
Ruta: src/app/service/consultaResultados.service.ts
Scope: @Injectable({ providedIn: 'root' })
Endpoints
-
Base URL:
URL_BASE = AppSettings.ENDPOINT_CRM
-
Dataset para tabla (anual):
POST {URL_BASE}/gc/v1/api/v1/catalogo/funcion/fn_reporte_estudios- Constante:
I_CATALOGS_URL
-
Resumen para gráficas:
POST {URL_BASE}/gc/v1/api/v1/catalogo/funcion/fn_reporte_estudios_totales- Constante:
REPORT_URL
Headers
El service envía explícitamente:
Content-Type: application/json
Además, el interceptor puede agregar:
Authorization: Bearer <token>ALP_AUTH: Bearer <token>
Payload (ambos endpoints)
El backend espera un arreglo con 6 posiciones:
[
idEstudio, // number
year, // number
mes, // number | null // backend 1..12 (cuando aplica)
dia, // number | null
ranchoId, // number | null // null = sin filtro / “todos”
tanqueId // number | null
]
Reglas vigentes:
- Si el filtro es
ALL→ se envíanull. mesydiasuelen irnullpara el “anual”.- Conversión de mes: en este módulo la conversión UI→backend se hace en el VM (
m + 1).
getReporteEstudios(...) (gráficas)
-
Resuelve
metadesdeMETADATA_ESTUDIO[tipoEstudioName]. -
Arma payload y llama a
REPORT_URL. -
Devuelve
resp.data.registroscomoReporteEstudiosItem[]o[]. -
Manejo de errores:
catch→console.errory regresa[].
loadAllDataForCurrentUser(template, year) (tabla)
- Obtiene ranchos permitidos:
-
getRanchoIds():-
Si hay mocks: usa
TEST_RANCH_IDS. -
Si no: lee
CurrentUserdellocalStorage, tomao_rfcy llama:AdministracionService.getSocios(rfc)
-
Mapea
numeroSocioLqlanumber[].
-
- Carga datos:
-
Si hay
TEST_ROWS(mock): filtra y construye respuesta. -
Si no hay mocks:
- Llama a
I_CATALOGS_URLcon[TEMPLATE_TO_ID[template], year, null, null, null, null]. - Fallback mensual: si viene vacío, intenta 12 llamadas (mes=1..12) y concatena.
- Llama a
- Normaliza + deduplica:
-
normalizeReporteRow()asegura shapeIcatalogsRowy homogenizaresultado/tx_resultado. -
Deduplica por clave:
rancho|tanque|fecha_muestra|fecha_registro|resultado
- Filtra por ranchos permitidos y construye:
ranchos: RanchoItem[]tanques: TanqueItem[]rows: IcatalogsRow[]
Mocks
Se activan cuando:
!environment.production && environment?.mocks?.consultaResultados === true
Y usan:
ICATALOGS_TEST_RANCH_IDSICATALOGS_TEST_ROWS
8) Modelos/Interfaces (contratos importantes)
Contratos de tabla
-
IcatalogsRow(src/app/model/ConsultaResultados/api-envelope.model.ts)- Representa una fila cruda del backend para el dataset anual.
-
DataResult(src/app/model/ConsultaResultados/data-result.model.ts)- Representa la fila ya mapeada para UI.
Contratos de gráficas
-
ReporteEstudiosItem(api-envelope.model.ts){ mes: 1..12, resultado: string, total: number, promedio: number }
-
ChartBarData / ChartPieData / ChartStackedData- En
src/app/model/ConsultaResultados/chart.model.ts
- En
Metadata y constantes
-
METADATA_ESTUDIO/TEMPLATE_TO_ID- Mapeo: nombre visible →
{ id, template }.
- Mapeo: nombre visible →
-
RESULTADO_ORDER/RESULTADO_COLORS_HEX- Orden y colores (principalmente para el estudio categórico).
Estrategias (Strategy Pattern)
-
IStudyStrategy(strategy/study-strategy.model.ts)- Contrato:
getConfig(),classify(),processStatsFromSummary().
- Contrato:
-
CategoricalStrategy- Para resultados tipo
'-', '1+', '2+', '3+', '4+'.
- Para resultados tipo
-
NumericRangeStrategy- Para resultados numéricos en rangos (R1..R4).
-
StudyStrategyFactory(src/app/service/consulta-resultados/study-strategy.service.ts)- Selección por heurística del nombre (
anillo,aflatoxina,quimico).
- Selección por heurística del nombre (
9) Casos de uso / flujos típicos
9.1 Carga inicial
-
ConsultaResultadosComponent.ngOnInit() -
Configura bandera de negocio:
vm.setAllowAllRanchos(ALLOW_ALL_RANCHOS)
-
Crea
FormGroupy suscribe loading → disable/enable. -
await vm.initFromLocalStorage(currentYear). -
Sincroniza selección inicial de rancho (por defecto: primer rancho) y tanque
ALL. -
Construye títulos reactivos (
tituloRancho$,tituloGrafica$). -
Llama
aplicar('local')para ejecutar 1 sola aplicación y pintar tabla + gráficas.
9.2 Aplicar filtros (botón “Aplicar”)
-
ngSubmitejecutaaplicar('post'). -
Si
valid === 'post':await vm.reloadForYear(y)(recarga dataset anual del año)
-
Construye rango (
buildRange()) según:- Año → año completo
- Año+Mes → mes completo
- Año+Mes+Día → día exacto
-
Sincroniza VM de forma consolidada:
vm.beginBatch()- set de year/mes/día/rango/rancho/tanque (silencioso)
vm.endBatch(true)→ dispara 1 apply.
9.3 Cambiar año
-
onAnioChange(y):- Limpia mes/día
- Deshabilita día
await vm.reloadForYear(y)- aplica.
9.4 Cambiar mes / día
-
onMesChange(m):- si
m == null: deshabilita día, rango = año - si
m != null: habilita día, fija límites min/max, rango = mes - aplica.
- si
-
onDiaChange(date):- actualiza
vm.setDia(day) - aplica.
- actualiza
9.5 Cambiar tipo de estudio
-
onTipoEstudioChange()→vm.onTipoEstudioChanged(val):- cambia estrategia
- recarga dataset anual según
template - aplica al final.
9.6 Limpiar (refrescar)
-
refrescar():-
resetea form a:
- estudio por defecto
- año actual
- mes/día null
- rancho/tanque =
ALL - rango: todo el año
-
sincroniza VM (incluye
dateStart$ydateEnd$). -
recarga vía
vm.onTipoEstudioChanged(defaultStudy).
-
9.7) Diagramas de secuencia (Mermaid)
Estos diagramas reflejan el flujo real Service → ViewModel → Component → HTML y muestran el interceptor de autenticación como dependencia transversal.
9.7.1) Carga inicial del módulo
9.7.2) Aplicar filtros (rancho/tanque/fecha)
9.7.3) Cambiar tipo de estudio (recarga dataset + re-render)
9.7.4) Manejo de 401 (reintento automático del interceptor)
10) Manejo de loading, errores y empty states
Loading
-
vm.loading$se ponetruedurante:initFromLocalStoragereloadForYearaplicarFiltrosCore(filtrado + carga de gráficas)
El componente responde:
- deshabilita filtros (
setFiltersDisabled(true)). - muestra spinner y skeleton.
Errores
- En el service, los errores HTTP se capturan con
try/catchy se retorna[]. - Consecuencia: en UI, un error puede verse igual que “no hay datos”.
Empty states
-
Si
registros$está vacío y no hay loading:- muestra empty state en tabla y gráficas.
-
Si la última acción fue “Aplicar” y quedó vacío:
- se muestra snackbar (una vez por firma de filtros).
11) Configuración necesaria / dependencias
11.1 Endpoints
AppSettings.ENDPOINT_CRMdebe estar configurado al backend correcto.
11.2 Sesión / localStorage
-
Debe existir
localStorage['CurrentUser']con al menos:o_rfc
11.3 Dependencia de AdministracionService
AdministracionService.getSocios(rfc)debe estar disponible y retornar elementos connumeroSocioLql.
11.4 Interceptor
KeycloudAuthInterceptordebe estar registrado globalmente (providers del core/app/host).shouldAttachAlpHeaders(url)debe reconocer las rutas del módulo.
11.5 UI / libs
- Angular Material (select, form-field, datepicker, table, paginator, snackbar, spinner, card, icon).
- Chart.js.
11.6 Paginador en español
-
getEsPaginatorIntl()está en:src/app/view/consulta-resultados/paginator/paginator-es.intl.ts
-
Para activarlo, se suele registrar
MatPaginatorIntlcomo provider donde corresponda.
12) Notas de mantenimiento y puntos de extensión
Agregar un nuevo tipo de estudio
Checklist:
-
Agregar entrada en
METADATA_ESTUDIO(id + template). -
Agregar a
estudiosDisponibles$(o cargar dinámicamente si lo migras). -
Ajustar
StudyStrategyFactory.getStrategy():- devolver la estrategia correcta para el nuevo estudio.
-
Si es necesario, crear una nueva implementación de
IStudyStrategy.
Cambios en backend
-
Si cambia el campo o formato de fechas (ej.
fecha_registro):- impacta
isYmdBetween()yfilterTableInMemory().
- impacta
-
Si cambia el índice del mes (0–11 vs 1–12):
- impacta
loadChartsDataFromService()(conversiónm + 1).
- impacta
-
Si cambian nombres de estudio en
tipo_estudio:- el filtro es exact match, puede quedar todo vacío.
Performance
- Tabla: filtrado en memoria sobre
allRows. - Gráficas: 1 o 2 llamadas (anual y opcional día).
Si el volumen de allRows crece demasiado:
- considerar indexado por rancho/mes.
- mover parte del filtrado al backend.