Merge branch 'main' into cliente/tagumedica

This commit is contained in:
George committed 2026-10-20 16:57:33 -05:00
commit b7c684d39e
6 files changed
+27 -7

No files matched your search

@@ -24,7 +24,8 @@ export function parseHtmlValues(def: HtmlReportDef, raw: Readonly<Record<string,
const missing: string[] = []; const missing: string[] = [];
for (const p of def.params) { for (const p of def.params) {
const v = raw[p.name]; const v = raw[p.name];
let text = typeof v === 'string' ? v.trim() : typeof v === 'number' ? String(v) : ''; // un parámetro de solo lectura vale siempre su valor por defecto, escriba lo que escriba el cliente
let text = p.readOnly ? '' : typeof v === 'string' ? v.trim() : typeof v === 'number' ? String(v) : '';
if (text === '') text = p.defaultValue ?? ''; if (text === '') text = p.defaultValue ?? '';
if (text === '') { if (text === '') {
if (p.required) missing.push(p.label); if (p.required) missing.push(p.label);
@@ -48,11 +48,18 @@ describe('registro de reportes HTML', () => {
expect(() => validateHtmlReports([{ ...def, params: [P({ name: 'a' }), P({ name: 'a' })] }])).toThrow(/repite el parámetro/); expect(() => validateHtmlReports([{ ...def, params: [P({ name: 'a' }), P({ name: 'a' })] }])).toThrow(/repite el parámetro/);
expect(() => validateHtmlReports([{ ...def, template: '../fuera.njk' }])).toThrow(/dentro de la carpeta/); expect(() => validateHtmlReports([{ ...def, template: '../fuera.njk' }])).toThrow(/dentro de la carpeta/);
expect(() => validateHtmlReports([{ ...def, template: '/etc/passwd' }])).toThrow(/dentro de la carpeta/); expect(() => validateHtmlReports([{ ...def, template: '/etc/passwd' }])).toThrow(/dentro de la carpeta/);
expect(() => validateHtmlReports([{ ...def, params: [P({ name: 'papel', readOnly: true })] }])).toThrow(/solo lectura y necesita un valor por defecto/);
expect(validateHtmlReports([def])).toHaveLength(1); expect(validateHtmlReports([def])).toHaveLength(1);
}); });
}); });
describe('valores de los parámetros', () => { describe('valores de los parámetros', () => {
it('un parámetro de solo lectura vale siempre su valor por defecto, aunque el cliente mande otro', () => {
const fijo = { ...def, params: [P({ name: 'papel', label: 'Papel', kind: 'select', options: [{ value: 'Carta', label: 'Carta' }, { value: 'A4', label: 'A4' }], defaultValue: 'Carta', readOnly: true })] };
expect(parseHtmlValues(fijo, {})).toEqual({ papel: 'Carta' });
expect(parseHtmlValues(fijo, { papel: 'A4' })).toEqual({ papel: 'Carta' });
});
it('exige los obligatorios, aplica el valor por defecto y comprueba tipos', () => { it('exige los obligatorios, aplica el valor por defecto y comprueba tipos', () => {
expect(parseHtmlValues(def, { doc: '144153', cantidad: 6 })).toEqual({ doc: '144153', cantidad: '6', papel: 'Carta', fecha: '' }); expect(parseHtmlValues(def, { doc: '144153', cantidad: 6 })).toEqual({ doc: '144153', cantidad: '6', papel: 'Carta', fecha: '' });
expect(() => parseHtmlValues(def, { cantidad: '6' })).toThrow(/Falta indicar: Entrega/); expect(() => parseHtmlValues(def, { cantidad: '6' })).toThrow(/Falta indicar: Entrega/);
+1
View File
@@ -16,6 +16,7 @@ export function validateHtmlReports(reports: readonly HtmlReportDef[]): readonly
for (const p of r.params) { for (const p of r.params) {
if (names.has(p.name)) throw new Error(`El reporte ${r.code} repite el parámetro ${p.name}`); if (names.has(p.name)) throw new Error(`El reporte ${r.code} repite el parámetro ${p.name}`);
names.add(p.name); names.add(p.name);
if (p.readOnly && !p.defaultValue) throw new Error(`El parámetro ${p.name} de ${r.code} es de solo lectura y necesita un valor por defecto`);
} }
} }
return reports; return reports;
@@ -60,6 +60,15 @@ describe('ReportParamField', () => {
expect(select.value).toBe('2'); expect(select.value).toBe('2');
}); });
it('un parámetro de solo lectura se ve con su valor pero no se puede cambiar', async () => {
await show(param({ kind: 'select', label: 'Papel', options: [{ value: 'Carta', label: 'Carta' }], readOnly: true }), 'Carta');
const select = el().querySelector('select') as HTMLSelectElement;
expect(select.value).toBe('Carta');
expect(select.disabled || select.hasAttribute('readonly') || select.closest('[readonly]') !== null).toBe(true);
await show(param({ label: 'Nombre', readOnly: true }), 'fijo');
expect(inputs()[0].readOnly).toBe(true);
});
it('un rango pide desde y hasta y los emite como lista', async () => { it('un rango pide desde y hasta y los emite como lista', async () => {
await show(param({ kind: 'date', range: true }), ['2026-01-01', '2026-01-31']); await show(param({ kind: 'date', range: true }), ['2026-01-01', '2026-01-31']);
expect(labels().join(' ')).toContain('desde / hasta'); expect(labels().join(' ')).toContain('desde / hasta');
@@ -24,23 +24,23 @@ const asList = (v: Value): readonly string[] => (typeof v === 'string' ? (v ===
@case ('single') { @case ('single') {
@switch (p.kind) { @switch (p.kind) {
@case ('number') { @case ('number') {
<sbo-number-field [label]="p.label" [required]="p.required" [value]="num(single())" (valueChange)="value.set($event === null ? '' : '' + $event)" /> <sbo-number-field [label]="p.label" [required]="p.required" [readonly]="!!p.readOnly" [value]="num(single())" (valueChange)="value.set($event === null ? '' : '' + $event)" />
} }
@case ('date') { @case ('date') {
<sbo-date-field [label]="p.label" [required]="p.required" [value]="single()" (valueChange)="value.set($event)" /> <sbo-date-field [label]="p.label" [required]="p.required" [readonly]="!!p.readOnly" [value]="single()" (valueChange)="value.set($event)" />
} }
@case ('select') { @case ('select') {
<sbo-select-field [label]="p.label" [required]="p.required" [options]="p.options ?? []" placeholder="" [value]="single() || null" (valueChange)="value.set($event === null ? '' : '' + $event)" /> <sbo-select-field [label]="p.label" [required]="p.required" [readonly]="!!p.readOnly" [options]="p.options ?? []" placeholder="" [value]="single() || null" (valueChange)="value.set($event === null ? '' : '' + $event)" />
} }
@case ('lookup') { @case ('lookup') {
@if (catalog(); as c) { @if (catalog(); as c) {
<sbo-lookup-field [label]="p.label" [required]="p.required" [lookup]="c.lookup" resolveOnLeave [value]="single()" (valueChange)="value.set($event)" /> <sbo-lookup-field [label]="p.label" [required]="p.required" [readonly]="!!p.readOnly" [lookup]="c.lookup" resolveOnLeave [value]="single()" (valueChange)="value.set($event)" />
} @else { } @else {
<sbo-text-field [label]="p.label" [required]="p.required" [value]="single()" (valueChange)="value.set($event)" /> <sbo-text-field [label]="p.label" [required]="p.required" [readonly]="!!p.readOnly" [value]="single()" (valueChange)="value.set($event)" />
} }
} }
@default { @default {
<sbo-text-field [label]="p.label" [required]="p.required" [placeholder]="p.placeholder ?? ''" [value]="single()" (valueChange)="value.set($event)" /> <sbo-text-field [label]="p.label" [required]="p.required" [readonly]="!!p.readOnly" [placeholder]="p.placeholder ?? ''" [value]="single()" (valueChange)="value.set($event)" />
} }
} }
} }
+2
View File
@@ -20,6 +20,8 @@ export interface GatewayParam {
/** Se admiten varios valores. */ /** Se admiten varios valores. */
multiple?: boolean; multiple?: boolean;
defaultValue?: string; defaultValue?: string;
/** Se muestra pero no se edita: vale siempre su valor por defecto (por ejemplo el papel de unas etiquetas precortadas). */
readOnly?: boolean;
placeholder?: string; placeholder?: string;
/** Opciones de una lista fija (`kind = select`). */ /** Opciones de una lista fija (`kind = select`). */
options?: { value: string; label: string }[]; options?: { value: string; label: string }[];