feat(web): el stock por almacén muestra el nombre del almacén y la lista de artículos el nombre del grupo (columna nameOf en las tablas de filas)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5253ef4cb7
commit
8af3b11ac2
4 files changed
+29
-3
No files matched your search
@@ -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' },
|
||||
|
||||
@@ -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<HTMLInputElement>('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);
|
||||
|
||||
@@ -70,6 +70,11 @@ export interface DetailColumn {
|
||||
lookupFor?: (row: Record<string, unknown>) => VhLookup | undefined;
|
||||
/** La celda no se edita en esta fila. */
|
||||
readonlyFor?: (row: Record<string, unknown>) => 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, unknown>) => 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»). */
|
||||
|
||||
@@ -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<Record<string, Map<string, string>>>({});
|
||||
private readonly requested = new Set<string>();
|
||||
|
||||
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<string, unknown>[] }>(`${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);
|
||||
|
||||
Reference in new issue
Block a user