From 8af3b11ac241a12a631b3b19db1776d25d5a3390 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 19 Oct 2026 18:59:56 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20el=20stock=20por=20almac=C3=A9n=20?= =?UTF-8?q?muestra=20el=20nombre=20del=20almac=C3=A9n=20y=20la=20lista=20d?= =?UTF-8?q?e=20art=C3=ADculos=20el=20nombre=20del=20grupo=20(columna=20nam?= =?UTF-8?q?eOf=20en=20las=20tablas=20de=20filas)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../masters/articulos/articulos.screens.ts | 4 +++- .../masters/master-collections.spec.ts | 2 +- apps/web/src/app/shared/grid/grid.types.ts | 5 +++++ apps/web/src/app/shared/grid/rows-table.ts | 21 ++++++++++++++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/features/masters/articulos/articulos.screens.ts b/apps/web/src/app/features/masters/articulos/articulos.screens.ts index 2861788..b66dc2c 100644 --- a/apps/web/src/app/features/masters/articulos/articulos.screens.ts +++ b/apps/web/src/app/features/masters/articulos/articulos.screens.ts @@ -26,10 +26,11 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [ createLabel: 'Nuevo artículo', orderby: 'ItemCode', extraFields: ['Frozen'], + listNames: [{ key: 'ItemsGroupCode', resource: 'ItemGroups', valueField: 'Number', labelField: 'GroupName' }], columns: [ { key: 'ItemCode', title: 'Nº de artículo', width: '170px', mono: true, searchable: true }, { key: 'ItemName', title: 'Descripción', searchable: true }, - { key: 'ItemsGroupCode', title: 'Grupo', type: 'number', align: 'left', width: '90px', format: (r) => String(r['ItemsGroupCode'] ?? '') }, + { key: 'ItemsGroupCode', title: 'Grupo', type: 'number', align: 'left', width: '200px', format: (r) => String(r['ItemsGroupCode'] ?? '') }, { key: 'QuantityOnStock', title: 'En stock', type: 'number', width: '110px' }, { key: 'Frozen', @@ -149,6 +150,7 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [ identity: ['WarehouseCode'], columns: [ { key: 'WarehouseCode', title: 'Almacén', type: 'text', width: '110px' }, + { key: 'WarehouseName', title: 'Nombre', type: 'text', width: '260px', nameOf: { codeKey: 'WarehouseCode', resource: 'Warehouses', valueField: 'WarehouseCode', labelField: 'WarehouseName' } }, { key: 'InStock', title: 'En stock', type: 'number', width: '110px' }, { key: 'Committed', title: 'Comprometido', type: 'number', width: '110px' }, { key: 'Ordered', title: 'Pedido', type: 'number', width: '110px' }, diff --git a/apps/web/src/app/features/masters/master-collections.spec.ts b/apps/web/src/app/features/masters/master-collections.spec.ts index 9c64c4d..2ee595c 100644 --- a/apps/web/src/app/features/masters/master-collections.spec.ts +++ b/apps/web/src/app/features/masters/master-collections.spec.ts @@ -252,7 +252,7 @@ describe('formulario con tablas', () => { await open('/inventario/articulos', 'A1', item()); const tab = el().querySelectorAll('sbo-tab')[5]; const inputs = [...tab.querySelectorAll('tbody input')]; - expect(inputs.map((i) => i.value)).toEqual(['01', '990', '2', '1', '0', '0']); + expect(inputs.map((i) => i.value)).toEqual(['01', '', '990', '2', '1', '0', '0']); // la segunda es el nombre del almacén (llega del catálogo) expect(inputs.every((i) => i.readOnly)).toBe(true); expect(tab.querySelector('.addrow')).toBeNull(); expect(page().dirty()).toBe(false); diff --git a/apps/web/src/app/shared/grid/grid.types.ts b/apps/web/src/app/shared/grid/grid.types.ts index 622b0b8..a5be222 100644 --- a/apps/web/src/app/shared/grid/grid.types.ts +++ b/apps/web/src/app/shared/grid/grid.types.ts @@ -70,6 +70,11 @@ export interface DetailColumn { lookupFor?: (row: Record) => VhLookup | undefined; /** La celda no se edita en esta fila. */ readonlyFor?: (row: Record) => boolean; + /** + * La columna muestra el nombre de lo que identifica el código de otra columna (`WarehouseCode` → nombre del almacén); + * el catálogo se consulta una sola vez. No es un campo que se guarde. + */ + nameOf?: { codeKey: string; resource: string; valueField: string; labelField: string }; /** Texto calculado de la fila (solo se muestra; no es un campo que se guarde). */ computed?: (row: Record) => string; /** Campos de la fila que cambian además del propio al elegir o escribir un valor de la lista (la cuenta elegida primero completa el «Cuenta / socio»). */ diff --git a/apps/web/src/app/shared/grid/rows-table.ts b/apps/web/src/app/shared/grid/rows-table.ts index 9ad7166..f0904e6 100644 --- a/apps/web/src/app/shared/grid/rows-table.ts +++ b/apps/web/src/app/shared/grid/rows-table.ts @@ -1,4 +1,6 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, computed, effect, inject, input, model, signal } from '@angular/core'; +import { API_URL } from '../../core/api'; import { formatNumber, parseDecimal } from '../../core/format'; import { SlLookupApi } from '../../core/sl/sl-lookup'; import type { ListRow } from '../list/list.types'; @@ -151,8 +153,24 @@ export class RowsTable { readonly emptyText = input('Sin filas.'); private readonly lookupApi = inject(SlLookupApi); + private readonly http = inject(HttpClient); + /** Nombres de los catálogos de las columnas `nameOf`, por recurso. */ + private readonly names = signal>>({}); + private readonly requested = new Set(); constructor() { + effect(() => { + for (const c of this.columns()) { + const n = c.nameOf; + if (!n || this.requested.has(n.resource)) continue; + this.requested.add(n.resource); + const params = new HttpParams().set('$select', `${n.valueField},${n.labelField}`).set('$top', 500); + this.http.get<{ value?: Record[] }>(`${API_URL}/sl/${n.resource}`, { params }).subscribe({ + next: (res) => this.names.update((m) => ({ ...m, [n.resource]: new Map((res.value ?? []).map((x) => [String(x[n.valueField]), String(x[n.labelField] ?? '')])) })), + error: () => undefined, // sin nombres, la columna queda vacía + }); + } + }); // las celdas de código con texto propio (la cuenta con su formato) piden los textos de las filas que se cargan effect(() => { const rows = this.rows(); @@ -210,11 +228,12 @@ export class RowsTable { } protected locked(c: DetailColumn, row?: Row): boolean { - return this.readonly() || !!c.readonly || !!c.computed || (!!row && !!c.readonlyFor?.(row)); + return this.readonly() || !!c.readonly || !!c.computed || !!c.nameOf || (!!row && !!c.readonlyFor?.(row)); } protected text(row: Row, c: DetailColumn): string { if (c.computed) return c.computed(row); + if (c.nameOf) return this.names()[c.nameOf.resource]?.get(String(row[c.nameOf.codeKey] ?? '')) ?? ''; const v = row[c.key]; if (c.type === 'date') return dateOnly(v); if (c.type === 'number') return v === null || v === undefined || v === '' ? '' : formatNumber(v, c.decimals);