Skip to main content

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 localStorage y validación contra AdministracionService.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).
  • View (ConsultaResultadosComponent + HTML)

    • Maneja el FormGroup y eventos UI.
    • Conecta MatTableDataSource con MatPaginator.
    • Renderiza/destruye instancias de Chart.js.
    • Muestra loading/empty/snackbar.

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, llama auth.ensureValidToken() y agrega:

      • Authorization: Bearer <token>
      • ALP_AUTH: Bearer <token>
    • Asegura Content-Type: application/json en POST/PUT/PATCH si no existe.

  • Manejo de 401:

    • Hace auth.clearToken()
    • Vuelve a pedir ensureValidToken()
    • Reintenta una sola vez la misma request.

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 devolver true para 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.ts
  • src/app/view/consulta-resultados/consulta-resultados.component.html
  • src/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)UIEventoAcción
tipoEstudiomat-select(selectionChange)vm.onTipoEstudioChanged(value)
ranchoIdmat-select(selectionChange)vm.onRanchoChanged(value)
tanqueIdmat-select(selectionChange)vm.onTanqueChanged(value)
aniomat-select(selectionChange)onAnioChange(year)vm.setYear() + vm.reloadForYear() + aplicar
mesmat-select(selectionChange)onMesChange(month | null) → habilita/deshabilita día + vm.setMes() + aplicar
diamat-datepicker (solo día)(dateInput)onDiaChange(Date | null)vm.setDia(day) + aplicar
SubmitBotón “Aplicar”(ngSubmit)aplicar('post') (recarga año y aplica)
ResetBotón “Limpiar”(click)refrescar() (resetea filtros y recarga)

Paginación

  • Se usa MatTableDataSource<DataResult>.

  • Se enlaza a MatPaginator con un @ViewChild(MatPaginator) set paginator(...).

    • Esto es útil cuando el paginator aparece/desaparece por *ngIf y evita fallos de sincronización.

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())
  • Empty state:

    • Si !loading y registros$ está vacío, muestra “Sin datos en el periodo seleccionado”.
  • 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)

StreamTipoPropó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)

  1. loading$ = true
  2. Obtiene metadata del estudio actual (METADATA_ESTUDIO[currentStudyName]).
  3. Llama api.loadAllDataForCurrentUser(template, year).
  4. Guarda dataset anual en memoria: allRows.
  5. Expone combos (ranchos$, tanques$).
  6. Valida selección de rancho con ensureValidRanchoSelection().
  7. 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 agenda runApplyIfNeeded().
  • runApplyIfNeeded()

    • evita repetición por lastAppliedKey.
    • soporta applyInFlight y applyPending.

La “firma” (buildApplyKey()) incluye: estudio, year/mes/día, rancho/tanque, dateStart/dateEnd.

Aplicación real de filtros: aplicarFiltrosCore()

Secuencia:

  1. filterTableInMemory() → publica registros$.
  2. loadChartsDataFromService() → publica barData$, pieData$, stackedData$.

Filtrado en memoria (tabla): filterTableInMemory()

Orden actual:

  1. Si falta dateStart o dateEnd:

    • limpia registros$ y los datasets de gráficas.
  2. Parte de allRows (dataset anual).

  3. Filtra por:

    • Tipo de estudio: r.tipo_estudio === currentStudy (match exacto)
    • Rancho si no es ALL
    • Fechas usando fecha_registro (helper isYmdBetween que compara YYYYMMDD local)
  4. Reconstruye índice de tanques (para el combo) en base a los registros visibles.

  5. Asegura que el tanque actual sea válido.

  6. Filtra por tanque si no es ALL.

  7. Mapea a DataResult y publica registros$.

Importante: el filtro de fechas se hace sobre fecha_registro (no fecha_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:

EscenarioLlamadasfocusData
Año completo (sin mes)1annualData
Mes seleccionado (sin día)1annualData.filter(item => item.mes === mesBackend)
Día seleccionado2Llama 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ía null.
  • mes y dia suelen ir null para 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 meta desde METADATA_ESTUDIO[tipoEstudioName].

  • Arma payload y llama a REPORT_URL.

  • Devuelve resp.data.registros como ReporteEstudiosItem[] o [].

  • Manejo de errores:

    • catchconsole.error y regresa [].

loadAllDataForCurrentUser(template, year) (tabla)

  1. Obtiene ranchos permitidos:
  • getRanchoIds():

    • Si hay mocks: usa TEST_RANCH_IDS.

    • Si no: lee CurrentUser del localStorage, toma o_rfc y llama:

      • AdministracionService.getSocios(rfc)
    • Mapea numeroSocioLql a number[].

  1. Carga datos:
  • Si hay TEST_ROWS (mock): filtra y construye respuesta.

  • Si no hay mocks:

    • Llama a I_CATALOGS_URL con [TEMPLATE_TO_ID[template], year, null, null, null, null].
    • Fallback mensual: si viene vacío, intenta 12 llamadas (mes=1..12) y concatena.
  1. Normaliza + deduplica:
  • normalizeReporteRow() asegura shape IcatalogsRow y homogeniza resultado/tx_resultado.

  • Deduplica por clave:

    • rancho|tanque|fecha_muestra|fecha_registro|resultado
  1. 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_IDS
  • ICATALOGS_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

Metadata y constantes

  • METADATA_ESTUDIO / TEMPLATE_TO_ID

    • Mapeo: nombre visible → { id, template }.
  • 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().
  • CategoricalStrategy

    • Para resultados tipo '-', '1+', '2+', '3+', '4+'.
  • 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).

9) Casos de uso / flujos típicos

9.1 Carga inicial

  1. ConsultaResultadosComponent.ngOnInit()

  2. Configura bandera de negocio:

    • vm.setAllowAllRanchos(ALLOW_ALL_RANCHOS)
  3. Crea FormGroup y suscribe loading → disable/enable.

  4. await vm.initFromLocalStorage(currentYear).

  5. Sincroniza selección inicial de rancho (por defecto: primer rancho) y tanque ALL.

  6. Construye títulos reactivos (tituloRancho$, tituloGrafica$).

  7. Llama aplicar('local') para ejecutar 1 sola aplicación y pintar tabla + gráficas.

9.2 Aplicar filtros (botón “Aplicar”)

  • ngSubmit ejecuta aplicar('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.
  • onDiaChange(date):

    • actualiza vm.setDia(day)
    • aplica.

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$ y dateEnd$).

    • 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 pone true durante:

    • initFromLocalStorage
    • reloadForYear
    • aplicarFiltrosCore (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/catch y 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_CRM debe 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 con numeroSocioLql.

11.4 Interceptor

  • KeycloudAuthInterceptor debe 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 MatPaginatorIntl como provider donde corresponda.


12) Notas de mantenimiento y puntos de extensión

Agregar un nuevo tipo de estudio

Checklist:

  1. Agregar entrada en METADATA_ESTUDIO (id + template).

  2. Agregar a estudiosDisponibles$ (o cargar dinámicamente si lo migras).

  3. Ajustar StudyStrategyFactory.getStrategy():

    • devolver la estrategia correcta para el nuevo estudio.
  4. 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() y filterTableInMemory().
  • Si cambia el índice del mes (0–11 vs 1–12):

    • impacta loadChartsDataFromService() (conversión m + 1).
  • 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.