# Plan de Modernización — labSoft
# 20 días · CodeIgniter 4 · PHP 8

> **Progreso:** Días 1–20 completados (2026-07-03). Plan de 20 días terminado.
> **Bug encontrado el Día 20:** `database/campos_resultado.sql` (generado el Día 12 por `migrar_plantillas.php`)
> tenía un escape incorrecto en un JSON (plantilla 75, `INMUNOGLOBULINA "E"(LGE)`): `\"` dentro de un string
> SQL de comillas simples se interpreta como `"` literal, rompiendo el JSON. Se detectó ejecutando
> `schema.sql` + `seed.sql` contra MySQL real (no solo `php -l`). Corregido en `campos_resultado.sql`,
> `seed.sql` y en el generador `migrar_plantillas.php` (para que no se repita si se vuelve a correr).
> **Hueco encontrado el Día 18:** el Día 10 (Abonos + Monedero) nunca se había implementado — solo existía
> `AbonoModel`; las rutas apuntaban a controladores inexistentes. Se completó: `Empleado\Abono` y
> `Empleado\Monedero` + vistas + `VentaModel::buscarConSaldo()`.
> **Bug de seguridad grave encontrado el Día 19:** el filtro `csrf` estaba registrado pero comentado en
> `app/Config/Filters.php` (`$globals['before']`), configuración de stock de CI4 nunca activada. Todos los
> `csrf_field()`/`csrf_hash()` del proyecto eran decorativos — el servidor nunca los validaba. Activado.
> Al activarlo se descubrió que `auth/login.php` era el único formulario POST sin token CSRF (se habría roto
> el login); corregido. También se limpió `Auth.php`, que usaba `$_POST`/`$_SERVER` crudos en vez de
> `$this->request`.
> **Bug encontrado y corregido el Día 16:** `app/ThirdParty/fpdf/fpdf.php` (FPDF 1.6, de 2011) usa tres
> construcciones eliminadas en PHP 8: constructor estilo PHP4 (`function FPDF(...)` en vez de `__construct`),
> `get_magic_quotes_runtime()` y `each()`. Sin el fix, cualquier generación de PDF fallaba. Corregido en el
> vendor copiado (no en el original de `BL_ant`).
> Contexto del sistema legacy en `C:\DonDisco\BL_ant\php\CLAUDE.md`.
> **Bug encontrado y corregido el Día 12:** `AuthFilter.php` hacía `explode(',', $arguments[0])`
> pero CI4 ya entrega `$arguments` como array separado (`'auth:0,1'` → `['0','1']`), así que
> el segundo rol de CUALQUIER grupo multi-rol (`lab` niveles 0,1 y `empleado` niveles 0,2)
> quedaba expulsado a su propio módulo en loop. Corregido a `array_map('intval', $arguments)`.

---

## Framework elegido: CodeIgniter 4

**Por qué CI4 y no Laravel:**
- Ya lo usas en Cháchara Infernal — mismos patrones, mismo entorno de hosting
- Ligero, rápido, sin magia innecesaria
- El mismo servidor (Hostinger/sistematlan.com) ya corre CI4
- Curva de aprendizaje prácticamente cero para ti
- MVC limpio sin overhead de Eloquent ni Blade

**Qué reemplazamos con CI4:**
| Viejo | Nuevo |
|-------|-------|
| `mysql_*` | Query Builder de CI4 + PDO |
| Framesets Dreamweaver | Vistas CI4 con layout único |
| Adobe Spry validation | HTML5 validation + Bootstrap |
| 84 archivos de plantilla | Un solo controlador dinámico |
| `HTTP_REFERER` como seguridad | CI4 Sessions + filtros de auth |
| Código espagueti | Controladores + Modelos + Servicios |
| jQuery 4 versiones simultáneas | Una sola versión + Bootstrap 5 |

**DB:** Reconstruida desde `BD/laboratorios_myAdmin.sql` (la más completa, marzo 2010)
+ columnas adicionales detectadas en el código que el SQL no tenía.

---

## Estructura del nuevo proyecto

```
lab-bello/                   ← nuevo directorio del proyecto CI4
├── app/
│   ├── Config/
│   │   ├── Routes.php
│   │   └── Auth.php
│   ├── Controllers/
│   │   ├── Auth.php         ← login/logout
│   │   ├── Admin/
│   │   │   ├── Dashboard.php
│   │   │   ├── Analisis.php
│   │   │   ├── Cliente.php
│   │   │   ├── Empleado.php
│   │   │   ├── Sucursal.php
│   │   │   ├── Precios.php
│   │   │   └── Reportes.php
│   │   ├── Empleado/
│   │   │   ├── Catalogo.php
│   │   │   ├── Carrito.php
│   │   │   ├── Venta.php
│   │   │   ├── Monedero.php
│   │   │   └── Abono.php
│   │   └── Lab/
│   │       ├── Pendientes.php
│   │       ├── Captura.php   ← reemplaza los 84 archivos
│   │       └── Resultado.php
│   ├── Models/
│   │   ├── AnalisisModel.php
│   │   ├── ClienteModel.php
│   │   ├── EmpleadoModel.php
│   │   ├── SucursalModel.php
│   │   ├── PrecioModel.php
│   │   ├── MuestraModel.php
│   │   ├── VentaModel.php
│   │   ├── AbonoModel.php
│   │   └── UsuarioModel.php
│   ├── Filters/
│   │   └── AuthFilter.php   ← reemplaza HTTP_REFERER
│   ├── Libraries/
│   │   └── Cdb.php          ← generador de código CDB
│   └── Views/
│       ├── layouts/
│       │   ├── admin.php
│       │   ├── empleado.php
│       │   └── lab.php
│       ├── auth/login.php
│       ├── admin/...
│       ├── empleado/...
│       └── lab/...
├── public/
│   ├── css/
│   ├── js/
│   └── img/
└── writable/
    └── resultados/          ← reemplaza expedientesXml/ (ahora en BD)
```

---

## Base de datos reconstruida

**Fuente:** `BD/laboratorios_myAdmin.sql` (2010-03-27) + columnas del código

**Cambios respecto al SQL original:**

| Tabla | Qué cambia |
|-------|-----------|
| `usuario` | Renombrada de `usuarios`, `pass` → `password_hash` (VARCHAR 255) |
| `cliente` | Se agrega `tipo_cliente` (del código), se limpia `huella` (longblob → pendiente) |
| `muestra` | Se agrega `idcliente` (faltaba en SQL pero estaba en código), `fechaCaptura` ya existe |
| `ventas` | Se limpian llaves foráneas duplicadas, se agrega `abonar` acumulado |
| `analisis` | Se agrega `campos_resultado` JSON — para el generador dinámico de plantillas |
| `resultado_detalle` | **NUEVA** — reemplaza los archivos XML: `(idmuestra, campo, valor)` |
| `consecutivo` | Se simplifica: solo `(idsucursal, consecutivo)` sin duplicados |

---

## Instrucciones diarias

Cada bloque es el prompt que pegarás al inicio de la sesión del día.
Empieza cada sesión con: **"Continuamos la modernización de labSoft. Hoy es el Día N."**
Luego pega el bloque del día.

---

### DÍA 1 — Setup del proyecto + Base de datos

```
Modernización labSoft — Día 1.

Contexto: Sistema LIMS en PHP 5 (mysql_*) que vamos a reescribir en CodeIgniter 4.
El proyecto viejo está en C:\BL\php. El nuevo irá en C:\BL\lab-bello.

Tarea de hoy:
1. Crear la estructura de carpetas del proyecto CI4 en C:\BL\lab-bello
   (solo directorios y archivos base, sin instalar composer — yo lo haré)
2. Escribir el script SQL completo de la base de datos reconstruida en C:\BL\lab-bello\database\schema.sql
   Fuente: C:\BL\php\BD\laboratorios_myAdmin.sql + correcciones del código original
   Incluye: todas las tablas limpias, nueva tabla resultado_detalle, campo password_hash en usuario,
   campo campos_resultado JSON en analisis, campo idcliente en muestra.
3. Escribir C:\BL\lab-bello\app\Config\Database.php con la configuración de conexión (con placeholders para credenciales)
4. Escribir C:\BL\lab-bello\app\Config\Routes.php con TODAS las rutas del sistema definidas (sin implementar los controladores aún)

El sistema tiene 3 roles: admin (0), laboratorista (1), empleado (2).
Rutas principales: /login, /admin/*, /empleado/*, /lab/*
```

---

### DÍA 2 — Autenticación + Sesiones + Filtro de acceso

```
Modernización labSoft — Día 2.

Ayer: Creamos la estructura del proyecto CI4 y el schema SQL en C:\BL\lab-bello.

Tarea de hoy:
1. app/Controllers/Auth.php — login (GET muestra form, POST procesa)
   - Busca usuario por login en tabla `usuario`
   - Verifica password con password_verify() contra password_hash
   - Guarda en sesión: id, login, nombre, accesslevel, idsucursal
   - Redirige según rol: 0→/admin, 1→/lab, 2→/empleado
   - En error: regresa al form con mensaje, SIN revelar si el usuario existe o no
2. app/Filters/AuthFilter.php — verifica sesión activa y nivel de acceso
   - Se aplica a rutas /admin/* (nivel 0), /lab/* (nivel 1), /empleado/* (niveles 0 y 2)
   - Sin sesión: redirige a /login
   - Con sesión pero nivel incorrecto: redirige a su módulo correspondiente
3. app/Controllers/Auth.php — logout (destruye sesión, redirige a /login)
4. app/Views/auth/login.php — formulario de login limpio con Bootstrap 5
5. app/Views/layouts/base.php — layout compartido con navbar que muestra nombre de usuario y rol

NO crear datos de prueba todavía. La vista de login debe tener el nombre "labSoft".
```

---

### DÍA 3 — Modelos base + Helpers

```
Modernización labSoft — Día 3.

Ayer: Auth completo — login, logout, filtro de sesión por rol.

Tarea de hoy:
1. Escribir los modelos base (solo propiedades $table, $primaryKey, $allowedFields y métodos simples):
   - app/Models/UsuarioModel.php
   - app/Models/ClienteModel.php  (incluir búsqueda por nombre con LIKE para autocomplete)
   - app/Models/EmpleadoModel.php
   - app/Models/SucursalModel.php
   - app/Models/AnalisisModel.php (incluir método getPorCategoria())
   - app/Models/PrecioModel.php   (incluir método getPrecioParaTipo($idanalisis, $tipo_cliente))
   - app/Models/MuestraModel.php
   - app/Models/VentaModel.php
   - app/Models/AbonoModel.php

2. app/Libraries/Cdb.php — clase que genera el código CDB
   Formato: {consecutivo}-{idusuario}-{idsucursal}-{idcliente}-{idmuestra}
   Método: generar($consecutivo, $idusuario, $idsucursal, $idcliente, $idmuestra): string
   Método: parsear($cdb): array con los 5 componentes

3. app/Helpers/lab_helper.php — funciones de uso frecuente:
   - calcularEdad($fechaNacimiento): int
   - formatearPrecio($precio): string
   - tiposCliente(): array  → ['General','GeneralD','Especial','EspecialD']
```

---

### DÍA 4 — Admin: Análisis CRUD

```
Modernización labSoft — Día 4.

Ayer: Modelos base + CDB library + helpers.

Tarea de hoy — Módulo Admin, gestión de análisis clínicos:

1. app/Controllers/Admin/Analisis.php
   - index(): lista todos los análisis paginada, con buscador por nombre/categoría
   - nuevo(): GET→form, POST→inserta y redirige
   - editar($id): GET→form con datos, POST→actualiza
   - eliminar($id): POST→soft delete o eliminar si no tiene muestras asociadas

2. app/Models/AnalisisModel.php — completar con:
   - tieneMuestras($id): bool — para saber si se puede eliminar
   - Las categorías son: HEMATOLOGIA, QUIMICA CLINICA, COPROLOGICOS, INMUNOLOGIA,
     UROANALISIS, P. GINECOLOGICO, PERFILES, ANTI Y AG, OTROS ESTUDIOS

3. app/Views/admin/analisis/index.php — tabla con: nombre, categoría, precio base, puntos, acciones
4. app/Views/admin/analisis/form.php — formulario compartido para crear y editar
   Campos: nombre, descripcion (categoría), precio, puntos, descuento, tipo_muestra (s/c/o/vacío)
   El campo campos_resultado (JSON) se edita en Día 13.

El layout admin debe tener sidebar con: Análisis, Clientes, Empleados, Sucursales, Precios, Reportes.
```

---

### DÍA 5 — Admin: Clientes + Empleados + Sucursales

```
Modernización labSoft — Día 5.

Ayer: Admin Análisis CRUD completo.

Tarea de hoy — Admin: las otras 3 entidades del catálogo:

1. app/Controllers/Admin/Cliente.php — CRUD completo
   Campos: nombre, fnacimiento, dir, mail, tel, cel, sexo, monedero, tipo_cliente, puntos, status
   - index(): tabla con buscador por nombre
   - La búsqueda por nombre también debe responder JSON para autocomplete (header Accept: application/json)

2. app/Controllers/Admin/Empleado.php — CRUD completo
   Campos: nombre, dir, correo, tel, idsucursal
   Al crear empleado, también crear usuario asociado (login, password temporal, accesslevel)

3. app/Controllers/Admin/Sucursal.php — CRUD completo
   Campos: nombre, direccion, telefono
   Al crear sucursal, inicializar su consecutivo en tabla `consecutivo` con valor 1

4. Vistas correspondientes para los 3 módulos (index + form para cada uno)
   Reutilizar el layout admin del Día 4.

IMPORTANTE para Cliente: el campo tipo_cliente determina qué columna de `precios` se usa.
Los valores válidos son exactamente: General, GeneralD, Especial, EspecialD
```

---

### DÍA 6 — Admin: Precios

```
Modernización labSoft — Día 6.

Ayer: Admin CRUD de Clientes, Empleados y Sucursales.

Tarea de hoy — Gestión de precios por tipo de cliente:

Contexto: La tabla `precios` tiene columnas General, GeneralD, Especial, EspecialD.
Cada análisis tiene una fila en precios con los 4 valores.
El `tipo_cliente` del cliente determina cuál columna se usa al vender.

1. app/Controllers/Admin/Precios.php
   - index(): tabla que muestra todos los análisis con sus 4 precios lado a lado
     Edición inline (un form por fila, submit por análisis) O modal de edición
   - actualizar($id): POST — actualiza los 4 precios de un análisis
   - actualizarMasivo(): POST — recibe array y actualiza todos en una sola operación
     (para cuando quieran subir precios en porcentaje)

2. app/Models/PrecioModel.php — completar:
   - getTabla(): todos los análisis con sus precios en una sola query (JOIN analisis)
   - actualizar($idanalisis, array $precios): update de los 4 valores
   - getPrecioParaCliente($idanalisis, $tipo_cliente): retorna el precio correcto

3. app/Views/admin/precios/index.php
   Tabla: Nombre análisis | General | GeneralD | Especial | EspecialD | Acción
   Los precios son editables directamente en la tabla.
```

---

### DÍA 7 — Empleado: Catálogo de análisis

```
Modernización labSoft — Día 7.

Ayer: Admin Precios completo.

Tarea de hoy — Módulo Empleado, primera pantalla operativa:

Contexto original: El empleado seleccionaba análisis de un catálogo dividido en 9 categorías
(tabs). El precio mostrado dependía del tipo de cliente (`tc` en GET). Los análisis
seleccionados se guardaban en un carrito de sesión.

1. app/Controllers/Empleado/Catalogo.php
   - index(): muestra el catálogo con el tipo_cliente del cliente en sesión (o 'General' por default)
   - El tipo de cliente puede pasarse como ?tc=Especial para iniciar una venta especial

2. app/Views/empleado/catalogo/index.php
   - Tabs Bootstrap 5 con las 9 categorías
   - Cada tab: tabla con nombre del análisis, precio según tipo, botón agregar/quitar
   - Estado del botón refleja si ya está en el carrito (sesión)
   - Contador de items en carrito visible en todo momento
   - Sin recargar página al agregar (AJAX simple, respuesta JSON)

3. app/Controllers/Empleado/Carrito.php
   - agregar(): POST JSON → agrega análisis a session['carrito']
   - quitar(): POST JSON → quita análisis de session['carrito']
   - ver(): GET → devuelve JSON con el carrito actual (para el contador)

Estructura del carrito en sesión:
session['carrito'] = [
  md5($id) => ['id'=>int, 'nombre'=>str, 'precio'=>float, 'descuento'=>int, 'puntos'=>int]
]
session['tipo_cliente'] = 'General' | 'GeneralD' | 'Especial' | 'EspecialD'
```

---

### DÍA 8 — Empleado: Cotización (vista del carrito)

```
Modernización labSoft — Día 8.

Ayer: Catálogo con carrito funcional en sesión.

Tarea de hoy — Vista de cotización y búsqueda de cliente:

Contexto original: cotizar.php mostraba el carrito + un formulario para ingresar datos
del cliente. El cliente se buscaba por nombre con autocomplete AJAX.
La venta podía hacerse como cotización (sin cobrar) o como venta directa.

1. app/Controllers/Empleado/Cotizacion.php
   - index(): muestra el carrito actual + formulario de cliente
   - buscarCliente(): GET ?q=nombre → JSON con resultados (para autocomplete)
     Busca en tabla cliente por nombre LIKE
   - Si el cliente no existe, permite capturar sus datos en el mismo form
   - Calcula subtotal, descuento por análisis, total a pagar

2. app/Views/empleado/cotizacion/index.php
   - Tabla del carrito: nombre análisis | precio unitario | descuento % | precio final
   - Totales: suma bruta, descuento total, precio con descuento
   - Formulario de cliente: nombre (con autocomplete), fecha de nacimiento, sexo,
     número de monedero (opcional), teléfono, correo
   - Cuando se selecciona un cliente existente: auto-llenar sus datos
   - Botón "Proceder a venta"
   - Botón "Solo cotizar" (guarda sin cobrar, status=0)

3. El autocomplete usa fetch() nativo (sin jQuery) → llama a buscarCliente()
   Muestra lista desplegable con los resultados, al seleccionar llena el form.
```

---

### DÍA 9 — Empleado: Venta (lógica principal)

```
Modernización labSoft — Día 9.

Ayer: Vista de cotización con búsqueda de cliente.

Tarea de hoy — La operación más compleja del sistema: procesar una venta.

Contexto original (insert.php tenía estos bugs que debemos CORREGIR):
- idsucursal hardcodeada como 3 → debe venir de session['idsucursal']
- idusuario hardcodeado como 1 → debe venir de session['id']
- $carro usado sin estar definido como variable local
- Sistema de puntos con lógica inconsistente

Lógica correcta de una venta:
1. Validar que el carrito no esté vacío
2. Buscar o crear el cliente (si es nuevo, insertar primero)
3. Actualizar datos del cliente (fnacimiento, monedero, tel, mail si cambiaron)
4. Obtener y BLOQUEAR el consecutivo de la sucursal del empleado logueado
5. Incrementar consecutivo en +1
6. Para cada análisis en el carrito:
   a. INSERT en `muestra` (status=0, idanalisis, tipo, cdb vacío aún)
   b. Generar CDB: {consecutivo}-{session.id}-{session.idsucursal}-{idcliente}-{idmuestra}
   c. UPDATE muestra SET cdb = CDB generado
7. Calcular precio final (con descuentos de análisis, sin descuento de puntos aún)
8. INSERT en `ventas` (idcliente, idusuario=session.id, idsucursal=session.idsucursal, etc.)
9. INSERT en `abonar` con el abono inicial
10. Calcular y actualizar puntos del cliente
11. Si puntos >= maximos.puntos_max: aplicar descuento y resetear puntos
12. Limpiar carrito de sesión
13. Retornar datos para ticket

1. app/Controllers/Empleado/Venta.php — método procesar()
2. app/Services/VentaService.php — toda la lógica de negocio (separada del controlador)
3. app/Views/empleado/venta/ticket.php — ticket imprimible (sin window.print automático,
   que el usuario elija cuándo imprimir)
```

---

### DÍA 10 — Empleado: Abonos + Monedero

```
Modernización labSoft — Día 10.

Ayer: Venta completa con VentaService.

Tarea de hoy — Registro de abonos y gestión de monedero:

1. app/Controllers/Empleado/Abono.php
   - index(): lista ventas con saldo pendiente (precio - suma de abonos)
   - buscarVenta(): GET ?q=nombre_cliente → JSON con ventas pendientes del cliente
   - registrar(): POST → INSERT en abonar, recalcula saldo en ventas
   - historial($id_venta): GET → todos los abonos de una venta

2. app/Controllers/Empleado/Monedero.php
   - index(): buscar cliente + mostrar saldo de monedero y puntos
   - actualizar(): POST → asignar/cambiar número de monedero a un cliente
   - consultarPuntos($idcliente): GET → devuelve puntos actuales y descuento disponible

3. app/Views/empleado/abono/index.php
   - Buscador de cliente por nombre (autocomplete)
   - Al seleccionar: muestra sus ventas con saldo pendiente
   - Al seleccionar venta: muestra historial de abonos + form para nuevo abono

4. app/Views/empleado/monedero/index.php
   - Buscador de cliente
   - Muestra: número de monedero actual, puntos acumulados, descuento disponible

NOTA: El monedero es solo un número de tarjeta de identificación (varchar).
Los puntos son los que generan descuento al llegar al máximo de tabla `maximos`.
```

---

### DÍA 11 — Laboratorista: Lista de pendientes

```
Modernización labSoft — Día 11.

Ayer: Abonos y Monedero en módulo Empleado.

Tarea de hoy — Primer día del módulo Laboratorista:

Contexto: El laboratorista ve las muestras pendientes de captura (status=0).
Originalmente en Laboratorista/Resultados/index.php, listaba muestras por CDB.

1. app/Controllers/Lab/Pendientes.php
   - index(): lista muestras con status=0, ordenadas por fecha de recepción
     JOIN con analisis para mostrar nombre del análisis
     JOIN con ventas→cliente para mostrar nombre del paciente
   - filtrar(): GET ?categoria=HEMATOLOGIA → filtra por categoría de análisis
   - La lista muestra: CDB, paciente, análisis, categoría, fecha, tipo_muestra

2. app/Views/lab/pendientes/index.php
   - Tabla con columnas: CDB | Paciente | Análisis | Categoría | Fecha | Tipo | Capturar
   - Botón "Capturar" lleva a /lab/captura/{idmuestra}
   - Filtros por categoría (los mismos 9 del catálogo)
   - Badge con total de pendientes

3. app/Controllers/Lab/Resultado.php — solo el método index por ahora:
   - index(): lista muestras con status=1 (ya capturadas), con link para ver/reimprimir

NOTA: tipo_muestra original: 's'=sangre, 'c'=centrifugado, 'o'=orina, ''=otro
Mostrar descripción legible en la tabla.
```

---

### DÍA 12 — Laboratorista: Generador dinámico de plantillas

```
Modernización labSoft — Día 12.

Ayer: Lista de pendientes del laboratorista.

Tarea de hoy — El cambio más importante del módulo Lab:
Reemplazar los 84 archivos de plantilla por un generador dinámico.

Contexto: Los archivos plantillas/1.php a plantillas/88.php son formularios
con campos element_1, element_2, ..., element_N. Cada uno corresponde a
un análisis. El campo `analisis.plantilla` indica el número de plantilla.
El campo que AGREGAREMOS: `analisis.campos_resultado` (JSON) define los campos.

Ejemplo de campos_resultado para BIOMETRÍA HEMÁTICA (plantilla 1):
[
  {"nombre": "element_1", "etiqueta": "NO. DE ERITROCITOS", "tipo": "text"},
  {"nombre": "element_2", "etiqueta": "HEMOGLOBINA", "tipo": "text"},
  ...19 campos...
]

Tarea:
1. Crear script de migración que convierta los 84 archivos a JSON en campos_resultado
   Leer cada plantillas/N.php y extraer las etiquetas de los <label> para generar el JSON
   Guardar en C:\BL\lab-bello\database\campos_resultado.sql (UPDATE por análisis)

2. app/Controllers/Lab/Captura.php
   - mostrar($idmuestra): GET → carga la muestra, el análisis, lee campos_resultado JSON,
     renderiza formulario dinámico
   - guardar($idmuestra): POST → itera element_1..N, INSERT en `resultado_detalle`,
     UPDATE muestra SET status=1, fechaCaptura=hoy
   - Si la muestra ya tiene status=1: mostrar modo lectura (no editable)

3. app/Models/ResultadoDetalleModel.php
   - guardarCampos($idmuestra, array $campos): INSERT múltiple
   - getCampos($idmuestra): array con campo=>valor para mostrar resultados

4. app/Views/lab/captura/form.php
   - Genera los campos dinámicamente desde el JSON
   - Muestra nombre del paciente, CDB, análisis, tipo de muestra
   - Botón guardar

NOTA: La tabla nueva es resultado_detalle(id, idmuestra, campo VARCHAR(50), valor TEXT)
```

---

### DÍA 13 — Resultados: Hoja de resultados

```
Modernización labSoft — Día 13.

Ayer: Generador dinámico de capturas de resultados.

Tarea de hoy — Vista de resultados para el empleado y para impresión:

Contexto original: Los resultados se guardaban como XML en disco (expedientesXml/).
Ahora están en la tabla resultado_detalle. La vista pública leía el XML.

1. app/Controllers/Empleado/Resultados.php (para empleado que atiende al cliente)
   - buscar(): GET ?q=nombre_cliente → JSON con ventas del cliente que tienen resultados
   - ver($idventa): muestra todos los resultados de una venta (puede tener múltiples análisis)
   - imprimir($idventa): versión imprimible sin navbar

2. app/Controllers/Lab/Resultado.php — completar:
   - ver($idmuestra): muestra resultado de UNA muestra con todos sus campos
   - imprimir($idmuestra): versión para imprimir hoja de resultados

3. app/Views/empleado/resultados/ver.php
   - Lista los análisis de la venta con sus resultados
   - Para cada análisis: tabla de campo/valor con los resultados
   - Encabezado: datos del paciente, fecha, CDB, doctor (si hay)

4. app/Views/lab/resultado/hoja.php (versión imprimible)
   - Encabezado: Logo + "labSoft"
   - Datos del paciente: nombre, edad calculada, sexo, fecha
   - CDB del análisis
   - Tabla de resultados con etiquetas y valores
   - Sin botón de impresión automática (el usuario decide)

NOTA: La edad se calcula de fnacimiento a fecha actual (usar calcularEdad() del helper).
```

---

### DÍA 14 — Admin: Reportes y Corte de caja

```
Modernización labSoft — Día 14.

Ayer: Vista de resultados completa.

Tarea de hoy — Módulo de reportes del administrador:

Contexto original (cortecaja.php tenía varios bugs de lógica condicional con isset/empty):
- Reporte por sucursal + rango de fechas: total vendido, total abonado, saldo pendiente
- La lógica condicional original era incorrecta (comparaciones isset vs empty mezcladas)

Lógica correcta del corte de caja:
- Si solo sucursal (sin fechas): total de esa sucursal desde siempre
- Si solo fechas (sin sucursal): total de TODAS las sucursales en ese rango
- Si sucursal + fechas: total de esa sucursal en ese rango
- Totales: suma(ventas.precio), suma(abonar.abonar), diferencia = saldo por cobrar

1. app/Controllers/Admin/Reportes.php
   - index(): form de filtros (sucursal, fecha_inicio, fecha_fin)
   - corteCaja(): POST → ejecuta la consulta según filtros, retorna vista con resultados
   - detalle($idsucursal): lista ventas individuales de una sucursal (para ver el desglose)

2. app/Models/ReporteModel.php
   - getCorteCaja($idsucursal=null, $fechaIni=null, $fechaFin=null): array con totales por sucursal
   - getVentasSucursal($idsucursal, $fechaIni=null, $fechaFin=null): ventas individuales

3. app/Views/admin/reportes/index.php
   - Formulario de filtros: select de sucursal (o "Todas"), date pickers de fecha
   - Tabla de resultados: Sucursal | Vendido | Abonado | Por cobrar
   - Botón de impresión (usa window.print(), no genera PDF)

4. app/Views/admin/reportes/detalle.php
   - Lista de ventas individuales: fecha, cliente, análisis, precio, abonado, saldo
```

---

### DÍA 15 — Admin: Gestión de usuarios del sistema

```
Modernización labSoft — Día 15.

Ayer: Reportes y corte de caja.

Tarea de hoy — Gestión de cuentas de acceso al sistema:

Contexto: La tabla `usuario` tiene: login, password_hash, idempleado, accesslevel.
El admin puede crear, editar y desactivar usuarios. Los accesslevel son 0, 1, 2.

1. app/Controllers/Admin/Usuario.php
   - index(): lista usuarios con nombre del empleado vinculado y su nivel
   - nuevo(): GET→form, POST→crea usuario con password_hash(password, PASSWORD_DEFAULT)
   - editar($id): GET→form (sin mostrar password), POST→actualiza (si password vacío, no cambia)
   - cambiarPassword($id): POST→actualiza solo el password
   - desactivar($id): POST→soft disable (no borrar)

2. app/Views/admin/usuario/index.php
   - Tabla: login, empleado, nivel de acceso, estado, acciones
   - Botones: editar, cambiar contraseña, desactivar

3. app/Views/admin/usuario/form.php
   - Campos: login, password (con confirmación), empleado (select), nivel de acceso (select)
   - Los niveles: 0=Administrador, 1=Laboratorista, 2=Empleado

4. Agregar al sidebar admin el enlace a Usuarios
5. Al crear un empleado (Día 5), debe poder crear el usuario en el mismo paso o desde la lista de empleados

IMPORTANTE: No permitir eliminar el propio usuario logueado.
No permitir que haya 0 administradores activos.
```

---

### DÍA 16 — Generación de PDFs

```
Modernización labSoft — Día 16.

Ayer: Gestión de usuarios del sistema.

Tarea de hoy — Generación de PDFs con FPDF (ya incluido en el proyecto viejo):

El proyecto original tenía FPDF en C:\BL\php\fpdf\ y html2pdf.
Usaremos FPDF directamente — es simple y no tiene dependencias.

1. Copiar C:\BL\php\fpdf\ a C:\BL\lab-bello\app\ThirdParty\fpdf\

2. app/Libraries/PdfTicket.php — genera el ticket de venta en PDF
   Contenido del ticket:
   - Encabezado: "labSoft" + fecha
   - Paciente: nombre, edad, sexo
   - Tabla de análisis: nombre | precio
   - Total, abono inicial, saldo
   - CDB de las muestras
   - "Resultados listos en N horas" (texto fijo)

3. app/Libraries/PdfResultado.php — genera la hoja de resultados en PDF
   Contenido:
   - Encabezado con logo (si existe) + nombre del laboratorio
   - Datos del paciente: nombre, edad, sexo, fecha de muestra, fecha de resultado
   - Doctor que solicitó (si hay)
   - Por cada análisis de la venta:
     - Nombre del análisis como subtítulo
     - Tabla de campo | valor
   - CDB en footer

4. Agregar a app/Controllers/Empleado/Venta.php:
   - ticket($idventa): descarga PDF del ticket
5. Agregar a app/Controllers/Lab/Resultado.php:
   - pdf($idmuestra): descarga PDF de la hoja de resultados
```

---

### DÍA 17 — Sistema de puntos correcto + Fix completo de hardcoded

```
Modernización labSoft — Día 17.

Ayer: Generación de PDFs.

Tarea de hoy — Corrección completa de todos los valores hardcodeados y el sistema de puntos:

HARDCODED a corregir (del código original):
1. idsucursal=3 en insert.php → session('idsucursal') — VERIFICAR que en TODOS los lugares
   donde se registra una venta o muestra, la sucursal viene de la sesión
2. idusuario=1 en insert.php → session('id') — mismo caso
3. La query SELECT donde idsucursal=3 en consecutivo → usar session('idsucursal')
4. El array de categorías hardcodeado en catalogo.php → vienen de DB (campo `descripcion` de analisis)

SISTEMA DE PUNTOS (lógica corregida):
La lógica original era inconsistente. La correcta:
- Cada análisis suma sus `puntos` al cliente al concretar la venta
- Si el cliente tiene número de monedero válido, acumula puntos
- Si puntos_acumulados >= maximos.puntos_max: se aplica maximos.descuento_max% sobre el total
  y los puntos se restan (puntos - puntos_max), no se ponen en cero
- El descuento de puntos se aplica ANTES de guardar el precio en ventas

1. Revisar y corregir app/Services/VentaService.php (del Día 9) con la lógica de puntos correcta
2. Verificar en TODOS los controladores que idsucursal e idusuario vienen de sesión
3. Agregar método en app/Models/AnalisisModel.php: getCategorias() → lista única de descripciones
   (para que el catálogo no tenga las 9 categorías hardcodeadas)
4. Agregar tabla `maximos` en la UI: app/Controllers/Admin/Config.php + vista simple
   para que el admin pueda cambiar puntos_max y descuento_max sin tocar la BD

Entregar: lista de TODOS los cambios hechos y en qué archivo.
```

---

### DÍA 18 — Pruebas: Flujo completo de venta

```
Modernización labSoft — Día 18.

Ayer: Sistema de puntos corregido + hardcoded eliminados.

Tarea de hoy — Revisión y corrección del flujo completo más crítico del sistema:

El flujo a revisar de punta a punta:
Empleado logueado → Catálogo → Agrega análisis → Cotización → Ingresa cliente →
Procesa venta → Ticket → Lab ve pendiente → Captura resultado → Empleado entrega resultado

Para cada paso, revisar:
1. ¿Los datos del cliente nuevo se guardan correctamente?
2. ¿El consecutivo se incrementa y no se repite entre ventas simultáneas? (usar transacción DB)
3. ¿El CDB generado es correcto? Verificar formato
4. ¿La suma de puntos es correcta?
5. ¿El abono inicial se guarda en `abonar`?
6. ¿El status de la muestra cambia de 0 a 1 al capturar resultado?
7. ¿Los campos del resultado se guardan en resultado_detalle correctamente?
8. ¿La vista de resultados muestra los campos en el orden del JSON?

Acciones:
- Identificar y corregir los bugs que encuentres en la revisión
- Agregar validaciones faltantes en los formularios
- Asegurarse de que las transacciones de DB estén envueltas en try/catch
- Verificar que session_destroy() en logout limpia TODO

Entregar: lista de bugs encontrados y cómo se corrigieron.
```

---

### DÍA 19 — Pruebas: Módulo Admin + Seguridad

```
Modernización labSoft — Día 19.

Ayer: Revisión del flujo de venta completo.

Tarea de hoy — Revisión del módulo admin y seguridad general:

ADMIN a revisar:
1. ¿El CRUD de análisis valida que nombre y categoría no estén vacíos?
2. ¿Al eliminar un análisis con muestras asociadas muestra error correcto?
3. ¿El corte de caja con sucursal vacía y sin fechas muestra todos los registros?
4. ¿El reporte con rango de fechas funciona con ambas fechas iguales?
5. ¿Los precios se guardan con 2 decimales correctamente?

SEGURIDAD a verificar:
1. AuthFilter está aplicado a TODAS las rutas protegidas (revisar Routes.php)
2. Ningún controlador usa $_GET o $_POST directamente sin pasar por $this->request->getGet()
3. Todas las queries usan Query Builder o prepared statements (ningún string concatenado)
4. La contraseña nunca se loguea, imprime o retorna en respuestas
5. El CSRF token está habilitado en el form de login y en todos los POST
6. No hay archivos .env, config con credenciales accesibles desde web

PENDIENTES a cerrar:
- Revisar que el layout de empleado muestre nombre del empleado y su sucursal
- Verificar que el logout redirige a /login y no al dashboard

Entregar: checklist con cada punto marcado como OK o PENDIENTE + descripción de los fixes.
```

---

### DÍA 20 — Preparación para producción

```
Modernización labSoft — Día 20.

Ayer: Revisión de seguridad y módulo admin.

Tarea de hoy — Checklist de producción y entregables finales:

1. Archivo C:\BL\lab-bello\database\schema.sql — versión final limpia y completa
   (sin datos de prueba, lista para ejecutar en producción)

2. Archivo C:\BL\lab-bello\database\seed.sql — datos iniciales obligatorios:
   - Un usuario admin (login: admin, password temporal que el admin cambia al entrar)
   - La sucursal principal con su consecutivo
   - Los 89 análisis con sus categorías
   - Los precios base en cero (el admin los configura)
   - Los maximos (puntos_max=30, descuento_max=10)
   - Los campos_resultado JSON para los 89 análisis

3. Archivo C:\BL\lab-bello\.env.example — plantilla de configuración sin credenciales reales

4. Archivo C:\BL\lab-bello\INSTALACION.md — instrucciones paso a paso:
   a. Requerimientos (PHP 8.1+, MySQL 5.7+)
   b. Cómo configurar el .env
   c. Cómo ejecutar schema.sql y seed.sql
   d. Cómo configurar el VirtualHost/subdomain
   e. Primer login y qué configurar primero (sucursales, empleados, usuarios, precios)

5. Revisar que .htaccess de CI4 funcione correctamente para el hosting

6. Verificar: ¿qué pasa si la sesión expira a mitad de una venta? 
   El carrito debe preservarse o alertar al usuario.

Entregar: todos los archivos de este día + lista final de TODO lo que quedó pendiente
(incluyendo el módulo de huella digital para una fase futura).
```

---

## Resumen del plan

| Semana | Días | Módulos |
|--------|------|---------|
| 1 | 1–3 | Cimientos: CI4 setup, BD, Auth, Modelos, Helpers |
| 2 | 4–6 | Admin: Análisis, Clientes, Empleados, Sucursales, Precios |
| 3 | 7–10 | Empleado: Catálogo, Carrito, Cotización, Venta |
| 4 | 11–13 | Empleado: Abonos, Monedero / Lab: Pendientes, Plantillas dinámicas |
| 5 | 14–16 | Resultados / Admin: Reportes, Usuarios / PDFs |
| 6 | 17–20 | Hardcoded fix, Puntos, Pruebas, Producción |

## Lo que NO entra en estos 20 días

- Lector de huella digital (requiere hardware específico)
- App móvil para resultados
- Notificaciones por WhatsApp/email
- Dashboard con gráficas
- Backup automático

## Antes de empezar el Día 1, tú necesitas

- [ ] Tener CI4 instalable (composer o descarga manual)
- [ ] Tener PHP 8.1+ disponible localmente o en el servidor
- [ ] Decidir si desarrollamos en local y subimos, o directo en servidor
- [ ] Confirmar las credenciales de la BD en producción (si es que el sistema volverá a operar)
