# Plan: Grupos de parámetros en `campos_resultado`

Permitir que el catálogo de parámetros de resultado de un análisis se organice
en **grupos** (p. ej. *Inspección visual* → *color*, *consistencia*, *textura*),
mostrando un encabezado de sección en captura y resultados (HTML y PDF), sin
romper la compatibilidad con análisis existentes.

## Decisiones de diseño (confirmadas 2026-08-28)

1. **Modelo**: catálogo central por tenant + FK. Tabla nueva
   `grupo_parametro`; cada entrada de `analisis.campos_resultado` lleva una
   clave opcional `idgrupo` (NULL permitido).
2. **Alcance visual**: aparecen encabezados de grupo en
   formulario de captura del lab, vista HTML del resultado (`ver.php` +
   `hoja.php`), PDF de resultados, portal público, reportes y vista de empleado.
3. **Legacy**: análisis existentes sin grupos se renderizan bajo un encabezado
   sintético **"General"** (sin migración de datos).
4. **Reorden**: solo campos dentro del mismo grupo. Las flechas ↑/↓ del editor
   no cruzan entre grupos; el admin debe reasignar para mover de grupo.

---

## 1. Migración `app/Database/Migrations/2026-08-XX-100000_CreateGrupoParametro.php`

Idempotente, con `down()` simétrico. Patrón de
[`2026-08-12-100000_CreateMetodologia.php`](../app/Database/Migrations/2026-08-12-100000_CreateMetodologia.php).

**Tabla `grupo_parametro`:**

| Columna | Tipo | Notas |
|---|---|---|
| `idgrupo` | INT UNSIGNED AUTO_INCREMENT PK | |
| `nombre` | VARCHAR(120) NOT NULL | |
| `descripcion` | VARCHAR(255) NULL | |
| `activo` | TINYINT(1) NOT NULL DEFAULT 1 | baja lógica |
| `tenant_id` | INT UNSIGNED NOT NULL | FK a `tenant` con `ON UPDATE CASCADE ON DELETE RESTRICT` |
| `created_at` / `updated_at` / `deleted_at` | DATETIME NULL | |
| `created_by` / `updated_by` / `deleted_by` | INT NULL | BaseAuditModel |

- Key `(tenant_id, nombre)`.
- Helper privado `colExists()` (mismo patrón que `CreateMetodologia`).
- `down()`: drop FK → drop table.

## 2. Modelo `app/Models/GrupoParametroModel.php`

Extiende `BaseTenantModel` (soft delete + tenant scoping + auditoría gratis):

- `$table = 'grupo_parametro'`, `$primaryKey = 'idgrupo'`, `$returnType = 'array'`.
- `$allowedFields = ['nombre', 'descripcion', 'activo']`.
- Métodos:
  - `listarActivas(): array` — `where('activo', 1)`, `orderBy('nombre')`.
  - `listarTodas(): array` — para gestión en Config.
  - `contarAnalisis(int $id): int` — nº de análisis que referencian el grupo
    (para aviso al dar de baja).

## 3. CRUD de catálogo (en `Admin/Config.php` + `Admin/GrupoParametro.php`)

Patrón idéntico a `TomaMuestra` / `Metodologia` (ver `Admin/Metodologia.php`):

- `Admin/Config.php::index()`: añade carga de `$grupos = $grupoModel->listarTodas()`
  con `n_analisis`, se pasa a la vista.
- `Admin/GrupoParametro.php` (nuevo): endpoints `POST` para alta / edición /
  baja lógica; `redirect()->to('/config#grupos')`.
- Vista nueva `app/Views/admin/config/_grupos.php`: tabla con nombre,
  descripción, nº análisis, acciones. Si está en uso, baja lógica (`activo=0`).

## 4. Editor de análisis

### 4.1 `app/Controllers/Admin/Analisis.php`

- `parseCamposResultadoPost()`: lee `campo[i][idgrupo]`, valida que exista y
  esté activo en el tenant actual; si no, error 422.
- Inyecta `'idgrupo' => int|null` en la entrada de `campos_resultado`.
- `analisisPayload()`: añade `idgrupo` por campo en el JSON del modal.
- `index()`, `nuevo()`, `editar()`: pasan `$grupos` a la vista.

### 4.2 `app/Views/admin/analisis/_form_fields.php`

En el bloque "Parámetros de resultado y rangos":

- Barra superior con botón **"Agregar grupo"** (modal pequeño para crear grupo
  al vuelo vía AJAX; devuelve `{idgrupo, nombre}`).
- Cada card de campo gana un `<select>` **"Grupo"** con las activas del
  catálogo + opción "— Sin grupo —".

### 4.3 `app/Views/admin/analisis/_campos_scripts.php`

- En `campoCardHtml()`: nuevo `<select data-name-tpl="campo[__I__][idgrupo]">`.
- En `reindexCamposIn()`: respetar el nuevo tpl.
- **↑/↓**: bloquear visualmente las flechas que cruzarían entre grupos
  (`disabled` + tooltip "Reasigna el grupo primero").

## 5. Helper `agrupar_campos()` en `app/Helpers/lab_helper.php`

```php
/**
 * Agrupa campos de resultado por idgrupo, conservando el orden original.
 *
 * @param list<array> $campos           Entradas de campos_resultado
 * @param array<int,string> $nombres    Mapa opcional idgrupo => nombre (para evitar N+1)
 *
 * @return list<array{
 *     grupo: array{idgrupo:int,nombre:string}|null,
 *     campos: list<array>
 * }>
 */
function agrupar_campos(array $campos, array $nombres = []): array
```

- Recorre `$campos` en orden; arma buckets por `idgrupo`.
- Campos sin `idgrupo` válido → bucket sintético `{grupo: null}` (la vista lo
  pinta como "General").
- Si se pasa `$nombres`, los usa para resolver el nombre sin tocar BD.

## 6. Render con encabezados de grupo

### 6.1 HTML (4 vistas)

Patrón común:

```php
$bloques = agrupar_campos($campos, $nombresGrupos);
foreach ($bloques as $b):
    $tituloGrupo = $b['grupo']['nombre'] ?? 'General'; ?>
    <h6 class="fw-semibold text-uppercase small text-muted mt-3 mb-2">
        <?= esc($tituloGrupo) ?>
    </h6>
    <table class="table table-sm"> ... filas de $b['campos'] ... </table>
<?php endforeach; ?>
```

Archivos a tocar:

| Archivo | Cambio |
|---|---|
| `app/Views/lab/captura/_form_body.php` | Captura: agrupar con `<fieldset>` o `<section>` |
| `app/Views/lab/resultado/ver.php` | Tabla por grupo (cards o apilada) |
| `app/Views/lab/resultado/hoja.php` | Hoja imprimible, optimizada para `@media print` |
| `app/Views/empleado/resultados/_detalle_body.php` | Detalle desde sucursal |
| `app/Views/empleado/resultados/hoja.php` | Hoja imprimible del empleado |
| `app/Views/public/portal_resultados.php` | Portal público por link |

### 6.2 PDF — `app/Libraries/PdfResultado.php`

- Refactor menor: extraer `dibujarCampos(array $campos, array $valores, ?string $sexo, int $edad): void`.
- Iterar `agrupar_campos()`; por cada bloque emitir fila de encabezado
  (`Cell(186, 6, 'INSPECCIÓN VISUAL', 1, 1, 'L', true)`, fill 239/244/250,
  Arial B 8), luego filas cuantitativas o cualitativas como hasta ahora.
- Si un análisis no tiene grupos → bucket "General" → mismo aspecto que hoy.

## 7. Compatibilidad con plantillas y comandos CLI

- `CatalogoPlantilla::normalizarCamposResultado()`: añadir `idgrupo` (int|null)
  al shape normalizado para que el `CHECK json_valid` siga tolerante y las
  plantillas importadas/exportadas lo conserven.
- `CatalogoExportPlantilla` / `CatalogoImportPlantilla`: propagar el campo.
- `CatalogoSanearPlantilla`: no tocar `idgrupo` (viene del JSON origen).

## 8. Tests (`tests/unit/`)

- `GrupoParametroModelTest.php`: soft delete, scoping por tenant, unicidad de
  nombre dentro del tenant.
- `AgruparCamposTest.php` (helper): casos
  (todos con grupo, todos sin grupo, mezcla, vacío, `idgrupo` inválido).
- `ResultadoPdfServiceTest`: verificar que un campo con `idgrupo=1` produce un
  bloque con título (smoke test del PDF).

## 9. Documentación

- Bitácora al final de `PLAN_ACTUALIZACION.md`: feature "Grupos de parámetros",
  con resumen del modelo, archivos tocados y comandos.
- `ANALISIS_CONTEXTO.md`: añadir fila en la tabla de catálogos
  (`grupo_parametro`).
- `docs/README.md`: añadir entrada de este plan en el índice.

## 10. Orden de ejecución propuesto

1. Migración + modelo + tests del modelo.
2. Helper `agrupar_campos()` + tests del helper.
3. CRUD del catálogo en `Admin/Config.php` + `Admin/GrupoParametro.php` + vista `_grupos.php`.
4. Editor de análisis (`_form_fields`, `_campos_scripts`, `Admin/Analisis::parseCamposResultadoPost`, payload).
5. Vistas HTML de resultados y captura (iteración con UI visual para validar UX).
6. PDF (`PdfResultado::dibujarTablaCuantitativa` + `dibujarCualitativo`).
7. Portal público + reportes + empleado.
8. Compatibilidad con `CatalogoPlantilla` + export/import.
9. Bitácora en `PLAN_ACTUALIZACION.md` + entrada en `docs/README.md`.

## 11. Riesgos y puntos abiertos

- **Bloqueo de ↑/↓ entre grupos**: si prefieres permitir cruzar libremente,
  dilo y se simplifica.
- **"General" sintético vs seed real**: hoy es solo del helper. Si quieres un
  grupo real `idgrupo=NULL` (renombrable, con nº de análisis), lo metemos en
  la migración. Más queries con join pero es editable.
- **Renombrar un grupo cambia la apariencia de TODOS los análisis que lo
  usan**. Esperado; los informes ya impresos no se regeneran.
- **Auditoría**: el renombrar/borrar grupo audita automáticamente vía
  `BaseAuditModel` (no requiere trabajo extra).

## 12. Archivos estimados a tocar

**Nuevos (5):**
- `app/Database/Migrations/2026-08-XX-100000_CreateGrupoParametro.php`
- `app/Models/GrupoParametroModel.php`
- `app/Controllers/Admin/GrupoParametro.php`
- `app/Views/admin/config/_grupos.php`
- `tests/unit/GrupoParametroModelTest.php` + `tests/unit/AgruparCamposTest.php`

**Modificados (~12):**
- `app/Controllers/Admin/Config.php` (cargar + sección grupos)
- `app/Controllers/Admin/Analisis.php` (parse + payload + listar)
- `app/Views/admin/analisis/_form_fields.php` (selector de grupo)
- `app/Views/admin/analisis/_campos_scripts.php` (editor JS)
- `app/Helpers/lab_helper.php` (`agrupar_campos`)
- `app/Views/lab/captura/_form_body.php`
- `app/Views/lab/resultado/ver.php` + `hoja.php`
- `app/Views/empleado/resultados/_detalle_body.php` + `hoja.php`
- `app/Views/public/portal_resultados.php`
- `app/Libraries/PdfResultado.php`
- `app/Services/CatalogoPlantilla.php` (compat JSON)
- `docs/PLAN_ACTUALIZACION.md` + `docs/README.md` + `docs/ANALISIS_CONTEXTO.md`
