From 58eafa72ec0940b75a93c103298c5f5cccbaccf1 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 20 Oct 2026 08:42:43 -0500 Subject: [PATCH] =?UTF-8?q?feat(tagumedica):=20campos=20de=20usuario=20de?= =?UTF-8?q?=20los=20lotes=20(calidad,=20liberaci=C3=B3n,=20registro=20sani?= =?UTF-8?q?tario=20y=20ensayos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../masters/lotes/lotes.extensions.spec.ts | 26 +++++++ .../masters/lotes/lotes.extensions.ts | 78 ++++++++++++++++--- .../masters/master-extensions.spec.ts | 7 +- 3 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/app/features/masters/lotes/lotes.extensions.spec.ts diff --git a/apps/web/src/app/features/masters/lotes/lotes.extensions.spec.ts b/apps/web/src/app/features/masters/lotes/lotes.extensions.spec.ts new file mode 100644 index 0000000..a593233 --- /dev/null +++ b/apps/web/src/app/features/masters/lotes/lotes.extensions.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { masterKeysOf } from '../master-model'; +import { MASTER_SCREENS } from '../master-screens'; + +describe('Lotes de TAGUMEDICA', () => { + const screen = MASTER_SCREENS.find((s) => s.path === '/inventario/lotes')!; + + it('suma sus campos de usuario en dos pestañas nuevas, después de las del base', () => { + expect(screen.layout.tabs.map((t) => t.id)).toEqual(['general', 'stock', 'detalles', 'calidad', 'registro']); + const keys = masterKeysOf(screen.layout); + for (const k of ['U_SIS_ESTLOT', 'U_SIS_ESTLIB', 'U_SIS_FECAPROB', 'U_SIS_FECLIB', 'U_SIS_RESPAPROB', 'U_SIS_OBSER', 'U_SIS_REGSAN', 'U_INT_NUMPRO', 'U_SIS_ACTMUE', 'U_SIS_INFENS', 'U_SIS_PROCED', 'U_INT_UBIAPT']) { + expect(keys, k).toContain(k); + } + }); + + it('la hora de aprobación se ve pero no se edita (la pone el proceso de aprobación)', () => { + const hora = screen.layout.tabs.flatMap((t) => [...t.left, ...t.right]).find((f) => 'key' in f && f.key === 'U_SIS_HORAPROB'); + expect(hora && 'readonly' in hora && hora.readonly).toBe(true); + }); + + it('la lista muestra el estado de calidad y de liberación con su texto', () => { + const cols = Object.fromEntries(screen.columns.map((c) => [c.key, c])); + expect(cols['U_SIS_ESTLOT'].format?.({ U_SIS_ESTLOT: '102' })).toBe('Aprobado'); + expect(cols['U_SIS_ESTLIB'].format?.({ U_SIS_ESTLIB: '102' })).toBe('No liberado'); + }); +}); diff --git a/apps/web/src/app/features/masters/lotes/lotes.extensions.ts b/apps/web/src/app/features/masters/lotes/lotes.extensions.ts index 78c9f32..2c6c45f 100644 --- a/apps/web/src/app/features/masters/lotes/lotes.extensions.ts +++ b/apps/web/src/app/features/masters/lotes/lotes.extensions.ts @@ -1,17 +1,71 @@ import type { MasterExtensions } from '../master-extensions'; +/** Estado del lote en calidad (`U_SIS_ESTLOT`, valores válidos de la tabla de usuario del lote en SAP). */ +const ESTADO_LOTE = [ + { value: '101', label: 'Cuarentena' }, + { value: '102', label: 'Aprobado' }, + { value: '103', label: 'Rechazado' }, + { value: '104', label: 'Inmovilizado' }, +]; +/** Estado de liberación (`U_SIS_ESTLIB`). */ +const ESTADO_LIBERACION = [ + { value: '101', label: 'Liberado' }, + { value: '102', label: 'No liberado' }, +]; + +const label = (options: readonly { value: string; label: string }[], value: unknown): string => options.find((o) => o.value === String(value))?.label ?? String(value ?? ''); + /** - * Campos que este cliente agrega a la pantalla de lotes. En `main` va vacío: el proyecto base solo lleva campos - * estándar de SAP. La rama `cliente/` lo completa, por ruta de pantalla, con campos estándar adicionales o de - * usuario (`U_*`) y no toca ningún otro archivo. + * Campos de usuario de TAGUMEDICA en los lotes (`OBTN`, verificados en `CUFD`/`UFD1` de `SBO_TAGUMEDICA_21`). Solo + * están en esta rama; `main` lleva solo campos estándar. * - * Además de agregar, puede ocultar campos (`hideFields`, `hideListColumns`) y dejarlos de solo lectura - * (`readOnlyFields`). Un nombre que no exista, o ocultar un campo obligatorio, lanza un error al arrancar. - * - * Ejemplo (los campos son de ejemplo): - * - * '/inventario/lotes': { - * newTabs: [{ id: 'calidad', label: 'Calidad', left: [{ type: 'text', key: 'U_Ubicacion', label: 'Ubicación' }], right: [] }], - * }, + * Notas verificadas en la base: `U_SIS_HORAPROB` guarda la hora como entero `hhmm` (1223 = 12:23) y la pone el proceso de + * aprobación, por eso va de solo lectura; `U_SIS_REGSAN` es el código de la tabla de usuario `@SIS_REGSAN` (357 + * registros; hoy se escribe el código, la ventana de selección llega con el registro de tablas de usuario). Los + * campos `U_SIS_MOTEST`, `U_SIS_COMEST` y `U_SIS_ACCTOM` no son del lote sino de la solicitud de traslado (`OWTQ`). */ -export const LOTES_EXTENSIONS: MasterExtensions = {}; +export const LOTES_EXTENSIONS: MasterExtensions = { + '/inventario/lotes': { + newTabs: [ + { + id: 'calidad', + label: 'Calidad', + left: [ + { type: 'select', key: 'U_SIS_ESTLOT', label: 'Estado del lote', options: ESTADO_LOTE }, + { type: 'date', key: 'U_SIS_FECAPROB', label: 'Fecha de aprobación' }, + { type: 'text', key: 'U_SIS_HORAPROB', label: 'Hora de aprobación (hhmm)', readonly: true }, + { type: 'text', key: 'U_SIS_RESPAPROB', label: 'Responsable de aprobación' }, + ], + right: [ + { type: 'select', key: 'U_SIS_ESTLIB', label: 'Estado de liberación', options: ESTADO_LIBERACION }, + { type: 'date', key: 'U_SIS_FECLIB', label: 'Fecha de liberación' }, + { type: 'text', key: 'U_SIS_OBSER', label: 'Observaciones' }, + ], + }, + { + id: 'registro', + label: 'Registro y ensayos', + left: [ + { type: 'text', key: 'U_SIS_REGSAN', label: 'Registro sanitario' }, + { type: 'text', key: 'U_INT_NUMPRO', label: 'Número de protocolo' }, + { type: 'text', key: 'U_SIS_PROCED', label: 'Procedencia' }, + ], + right: [ + { type: 'text', key: 'U_SIS_ACTMUE', label: 'Acta de muestreo' }, + { type: 'text', key: 'U_SIS_INFENS', label: 'Informe de ensayo' }, + { type: 'text', key: 'U_INT_UBIAPT', label: 'Ubicación APT' }, + ], + }, + ], + listColumns: [ + { + key: 'U_SIS_ESTLOT', + title: 'Calidad', + width: '110px', + format: (r) => label(ESTADO_LOTE, r['U_SIS_ESTLOT']), + tone: (r) => (r['U_SIS_ESTLOT'] === '102' ? 'ok' : r['U_SIS_ESTLOT'] === '101' ? 'info' : r['U_SIS_ESTLOT'] === '103' ? 'err' : 'warn'), + }, + { key: 'U_SIS_ESTLIB', title: 'Liberación', width: '110px', format: (r) => label(ESTADO_LIBERACION, r['U_SIS_ESTLIB']), tone: (r) => (r['U_SIS_ESTLIB'] === '101' ? 'ok' : 'muted') }, + ], + }, +}; diff --git a/apps/web/src/app/features/masters/master-extensions.spec.ts b/apps/web/src/app/features/masters/master-extensions.spec.ts index 504e48a..73c1949 100644 --- a/apps/web/src/app/features/masters/master-extensions.spec.ts +++ b/apps/web/src/app/features/masters/master-extensions.spec.ts @@ -15,9 +15,10 @@ const EXT: MasterExtensions = { }; describe('extensiones por cliente de los maestros', () => { - it('en main no agregan nada', () => { - expect(MASTER_EXTENSIONS).toEqual({}); - expect(MASTER_SCREENS).toEqual(BASE_MASTER_SCREENS); + // En `main` esta prueba exige `{}` (el base no lleva campos de cliente); en la rama del cliente se lista lo que agrega. + it('en esta rama solo se extienden las pantallas del cliente', () => { + expect(Object.keys(MASTER_EXTENSIONS)).toEqual(['/inventario/lotes']); + expect(MASTER_SCREENS.filter((s) => s.path !== '/inventario/lotes')).toEqual(BASE_MASTER_SCREENS.filter((s) => s.path !== '/inventario/lotes')); }); it('agregan campos al final de la cabecera y de las pestañas, una pestaña nueva y columnas de lista', () => {