feat(web): las extensiones de cliente pueden ocultar campos y columnas y dejarlos de solo lectura (documentos y maestros)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 11:14:52 -05:00
1 parent f077a61d08
commit 8dc4a8eaab
20 files changed
+374 -38

No files matched your search

@@ -6,6 +6,7 @@ import { of } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CLIENT_EXTENSIONS } from './client-extensions';
import { headerKeysOf } from './document-layouts';
import { lineFieldsOf } from './line-columns';
import { DocumentFormPage } from './document-form.page';
import { BASE_SCREENS, DOCUMENT_SCREENS } from './document-screens';
import { type ScreenExtensions, applyExtensions } from './screen-extensions';
@@ -224,3 +225,122 @@ describe('columnas de las listas de valores por documento', () => {
for (const c of cols) expect(c.lookup!.config.columns.at(-1)!.key).toBe('U_Sucursal');
});
});
describe('ocultar y dejar de solo lectura (extensiones de cliente)', () => {
const layoutOf = (out: readonly { path: string }[], path: string) => by(out, path).layout;
const apply = (e: ScreenExtensions) => applyExtensions(BASE_SCREENS, e);
const DIMS_2_5 = ['CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5'];
it('un cliente que solo quiere CostingCode oculta las otras dimensiones: no se muestran ni se envían', () => {
const l = layoutOf(apply({ '/ventas/facturas': { hideLineColumns: DIMS_2_5 } }), '/ventas/facturas');
const keys = l.columns.map((c) => c.key);
expect(keys).toContain('CostingCode');
for (const k of DIMS_2_5) {
expect(keys).not.toContain(k);
expect(l.lines.fields).not.toContain(k);
}
expect(l.lines.fields).toContain('CostingCode');
});
it('oculta también las columnas de lectura y de lista, sin tocar las demás pantallas', () => {
const out = apply({
'/ventas/facturas': { hideLineColumns: ['BaseType', 'BaseEntry', 'BaseLine', 'AccountCode'], hideListColumns: ['FolioPrefixString'] },
});
expect(layoutOf(out, '/ventas/facturas').columns.map((c) => c.key)).not.toEqual(expect.arrayContaining(['BaseType']));
expect(by(out, '/ventas/facturas').columns.map((c) => c.key)).not.toContain('FolioPrefixString');
expect(by(out, '/ventas/facturas').columns.map((c) => c.key)).toContain('FolioNumber');
expect(layoutOf(out, '/ventas/ordenes').columns.map((c) => c.key)).toEqual(layoutOf(BASE_SCREENS, '/ventas/ordenes').columns.map((c) => c.key));
});
it('oculta campos de cabecera: dejan de mostrarse y de enviarse', () => {
const l = layoutOf(apply({ '/ventas/ordenes': { hideHeaderFields: ['Indicator', 'Project', 'partnerTaxId'] } }), '/ventas/ordenes');
const keys = headerKeysOf(l);
expect(keys).not.toEqual(expect.arrayContaining(['Indicator']));
expect(keys).not.toContain('Project');
expect(l.header.left.map((f) => ('label' in f ? f.label : ''))).not.toContain('RUT');
expect(keys).toContain('NumAtCard');
});
it('el cliente puede ocultar una columna base y agregar la suya', () => {
const l = layoutOf(apply({ '/ventas/ordenes': { hideLineColumns: ['AccountCode'], lineColumns: [{ key: 'U_Lote', title: 'Lote' }] } }), '/ventas/ordenes');
const keys = l.columns.map((c) => c.key);
expect(keys).not.toContain('AccountCode');
expect(keys.at(-1)).toBe('U_Lote');
});
it('una columna de solo lectura se ve y no se edita, pero su valor sigue enviándose', () => {
const l = layoutOf(apply({ '/ventas/facturas': { readOnlyLineColumns: ['CostingCode', 'ProjectCode'] } }), '/ventas/facturas');
const col = (k: string) => l.columns.find((c) => c.key === k)!;
expect(col('CostingCode').locked).toBe(true);
expect(col('ProjectCode').locked).toBe(true);
expect(col('CostingCode2').locked).toBeUndefined();
expect(l.lines.fields).toEqual(expect.arrayContaining(['CostingCode', 'ProjectCode']));
expect(lineFieldsOf(l.columns)).toEqual(expect.arrayContaining(['CostingCode', 'ProjectCode']));
});
it('un campo de cabecera de solo lectura queda marcado y se sigue enviando', () => {
const l = layoutOf(apply({ '/ventas/ordenes': { readOnlyHeaderFields: ['Indicator', 'NumAtCard'] } }), '/ventas/ordenes');
expect(l.lockedKeys).toEqual(expect.arrayContaining(['Indicator', 'NumAtCard']));
expect(headerKeysOf(l)).toEqual(expect.arrayContaining(['Indicator', 'NumAtCard']));
});
it('no se puede ocultar lo obligatorio, y un nombre que no existe es un error', () => {
expect(() => apply({ '/ventas/ordenes': { hideLineColumns: ['ItemCode'] } })).toThrow(/obligatoria/);
expect(() => apply({ '/ventas/ordenes': { hideLineColumns: ['Quantity'] } })).toThrow(/obligatoria/);
expect(() => apply({ '/ventas/ordenes': { hideHeaderFields: ['CardCode'] } })).toThrow(/obligatorio/);
expect(() => apply({ '/inventario/entradas': { hideLineColumns: ['WarehouseCode'] } })).toThrow(/obligatoria/);
expect(() => apply({ '/ventas/ordenes': { hideLineColumns: ['CostingCode9'] } })).toThrow(/no tiene la columna de línea/);
expect(() => apply({ '/ventas/ordenes': { hideHeaderFields: ['Nada'] } })).toThrow(/no tiene el campo de cabecera/);
expect(() => apply({ '/ventas/ordenes': { readOnlyLineColumns: ['Nada'] } })).toThrow(/no tiene la columna de línea/);
expect(() => apply({ '/ventas/ordenes': { hideListColumns: ['Nada'] } })).toThrow(/no tiene la columna de lista/);
});
});
describe('formulario con campos ocultos o de solo lectura', () => {
let fixture: ComponentFixture<DocumentFormPage>;
const el = () => fixture.nativeElement as HTMLElement;
const tick = async () => {
fixture.detectChanges();
await new Promise((r) => setTimeout(r));
fixture.detectChanges();
};
async function openWith(ext: ScreenExtensions) {
TestBed.resetTestingModule();
const screen = by(applyExtensions(BASE_SCREENS, ext), '/ventas/ordenes');
const route = {
snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap({}) },
paramMap: of(convertToParamMap({})),
};
await TestBed.configureTestingModule({
imports: [DocumentFormPage],
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
}).compileComponents();
fixture = TestBed.createComponent(DocumentFormPage);
await tick();
}
const input = (label: string) => {
const f = [...el().querySelectorAll('sbo-field, sbo-text-field, sbo-lookup-field')].find((x) => x.querySelector('label')?.textContent?.trim() === label);
return f?.querySelector<HTMLInputElement>('input') ?? null;
};
it('un campo oculto no aparece en el formulario', async () => {
await openWith({ '/ventas/ordenes': { hideHeaderFields: ['Indicator'] } });
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).not.toContain('Indicador');
expect([...el().querySelectorAll('label')].map((l) => l.textContent?.trim())).toContain('Proyecto');
});
it('un campo de solo lectura se ve pero no se puede escribir', async () => {
await openWith({ '/ventas/ordenes': { readOnlyHeaderFields: ['Indicator', 'Project'] } });
expect(input('Indicador')!.readOnly).toBe(true);
expect(input('Proyecto')!.readOnly).toBe(true);
expect(input('Nº referencia')!.readOnly).toBe(false);
const buttons = [...el().querySelectorAll('sbo-lookup-field')].find((f) => f.querySelector('label')?.textContent?.trim() === 'Proyecto')!.querySelector('button.sbo-btn') as HTMLButtonElement;
expect(buttons.disabled).toBe(true);
});
it('sin extensiones los campos se editan como siempre', async () => {
await openWith({});
expect(input('Indicador')!.readOnly).toBe(false);
});
});