From 8ba845cc23bc9a428cf5c9e6dc50db0d68cedd4a Mon Sep 17 00:00:00 2001 From: George Date: Tue, 20 Oct 2026 09:09:45 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20=C2=ABCampos=C2=BB=20tambi=C3=A9n?= =?UTF-8?q?=20en=20pagos,=20asientos=20y=20filas=20con=20varios=20controle?= =?UTF-8?q?s;=20no=20se=20muestran=20las=20columnas=20internas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 2 +- apps/web/src/app/core/field-names.spec.ts | 29 +++++++++++++- apps/web/src/app/core/field-names.ts | 3 ++ .../features/banking/payment-form.page.html | 32 +++++++-------- .../documents/document-form.page.html | 36 ++++++++--------- .../features/documents/document-form.page.ts | 4 +- .../features/documents/document-layouts.ts | 39 +++++++++++++++++++ .../features/finance/journal-form.page.html | 20 +++++----- apps/web/src/app/shared/form/sbo-field.ts | 6 ++- apps/web/src/app/shared/grid/lines-grid.html | 2 +- apps/web/src/app/shared/grid/lines-grid.ts | 3 +- apps/web/src/app/shared/grid/rows-table.ts | 5 ++- apps/web/src/app/shared/list/data-table.html | 4 +- apps/web/src/app/shared/list/data-table.ts | 3 +- 14 files changed, 132 insertions(+), 56 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3315f84..001b009 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Módulo genérico `apps/api/src/sapdb/` (en `main`, opcional como el Gateway): e ## Ver el nombre del campo de SAP («Campos») -El mini-erp mostraba el nombre del campo de la base junto a cada casillero (`dbField`). Aquí es general: el botón **Campos** de la barra superior (`FieldNames`, se recuerda en `localStorage` `erp_fields`) muestra, debajo de cada etiqueta y en la cabecera de las tablas (listas, líneas de documento, tablas de filas), el nombre con el que el Service Layer lee y guarda el campo (`ItemCode`, `U_SIS_ESTLOT`); sin activarlo, el nombre sale siempre como ayuda al pasar el cursor. Los campos lo reciben con `dbName` (`FieldBase`/`SboField`); los formularios de maestros y documentos lo pasan solos desde su configuración declarativa. Pendiente: los formularios de pagos y de asientos (sus campos están escritos a mano) y las filas con varios controles (folio, grupos). +El mini-erp mostraba el nombre del campo de la base junto a cada casillero (`dbField`). Aquí es general: el botón **Campos** de la barra superior (`FieldNames`, se recuerda en `localStorage` `erp_fields`) muestra, debajo de cada etiqueta y en la cabecera de las tablas (listas, líneas de documento, tablas de filas), el nombre con el que el Service Layer lee y guarda el campo (`ItemCode`, `U_SIS_ESTLOT`); sin activarlo, el nombre sale siempre como ayuda al pasar el cursor. Los campos lo reciben con `dbName` (`FieldBase`/`SboField`); los formularios de maestros y documentos lo pasan solos desde su configuración declarativa. Cubre los formularios de maestros, de documentos (incluidas las filas con varios controles: `FolioPrefixString · FolioNumber`, `DocCurrency · DocRate`…, con `fieldDbName` en `document-layouts.ts`), de pagos y de asientos (sus campos van escritos a mano en el HTML con `dbName="…"`: un campo nuevo debe llevarlo) y las cabeceras de tablas. Solo se muestran como campos de SAP los nombres que empiezan con mayúscula (`isSapField`): las columnas internas de la interfaz (`docNum`, `_Name`) no. ## Cómo verificar cambios de interfaz diff --git a/apps/web/src/app/core/field-names.spec.ts b/apps/web/src/app/core/field-names.spec.ts index 084e7c3..e40fd97 100644 --- a/apps/web/src/app/core/field-names.spec.ts +++ b/apps/web/src/app/core/field-names.spec.ts @@ -2,7 +2,8 @@ import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { beforeEach, describe, expect, it } from 'vitest'; import { TextField } from '../shared/form'; -import { FieldNames } from './field-names'; +import { fieldDbName, type HeaderField } from '../features/documents/document-layouts'; +import { FieldNames, isSapField } from './field-names'; @Component({ imports: [TextField], template: '' }) class Host {} @@ -32,3 +33,29 @@ describe('Ver campos', () => { expect(TestBed.inject(FieldNames).show()).toBe(true); }); }); + +describe('nombre del campo de una fila del formulario', () => { + const name = (f: HeaderField) => fieldDbName(f); + + it('un campo sencillo muestra su campo y una fila con varios controles los muestra todos', () => { + expect(name({ type: 'text', key: 'NumAtCard', label: 'Nº de referencia' })).toBe('NumAtCard'); + expect(name({ type: 'folio', label: 'Folio', prefixKey: 'FolioPrefixString', numberKey: 'FolioNumber' })).toBe('FolioPrefixString · FolioNumber'); + expect(name({ type: 'group', label: 'Indicador', cells: [{ kind: 'text', key: 'Indicator', label: 'I' }, { kind: 'text', key: 'FolioPrefixString', label: 'P' }] })).toBe('Indicator · FolioPrefixString'); + expect(name({ type: 'series', key: 'Series', label: 'Número' })).toBe('Series · DocNum'); + expect(name({ type: 'currency', label: 'Moneda' })).toBe('DocCurrency · DocRate'); + expect(name({ type: 'rounding', label: 'Redondeo' })).toBe('Rounding · RoundingDiffAmount'); + }); + + it('los valores calculados muestran de dónde salen', () => { + expect(name({ type: 'computed', label: 'Total', source: 'total' })).toBe('DocTotal'); + expect(name({ type: 'computed', label: 'Saldo', source: 'balance' })).toBe('DocTotal − PaidToDate'); + }); +}); + +describe('qué columnas son campos de SAP', () => { + it('los de SAP empiezan con mayúscula; los internos de la interfaz, no', () => { + for (const k of ['ItemCode', 'U_SIS_ESTLOT', 'AccounttNum']) expect(isSapField(k), k).toBe(true); + for (const k of ['docNum', 'balance', '_Name', '_manageBatches']) expect(isSapField(k), k).toBe(false); + }); +}); + diff --git a/apps/web/src/app/core/field-names.ts b/apps/web/src/app/core/field-names.ts index 514613b..2d27b4d 100644 --- a/apps/web/src/app/core/field-names.ts +++ b/apps/web/src/app/core/field-names.ts @@ -2,6 +2,9 @@ import { Injectable, signal } from '@angular/core'; const KEY = 'erp_fields'; +/** Un nombre de campo de SAP empieza con mayúscula (`ItemCode`, `U_SIS_X`); los de columnas internas de la interfaz (`docNum`, `_Name`) no. */ +export const isSapField = (key: string): boolean => /^[A-Z]/.test(key); + /** * «Ver campos»: muestra, junto a cada etiqueta, el nombre del campo de SAP (`ItemCode`, `U_SIS_ESTLOT`) con el que se lee * y se guarda. Útil para quien configura el sistema o arma una consulta; el nombre también sale siempre como ayuda al diff --git a/apps/web/src/app/features/banking/payment-form.page.html b/apps/web/src/app/features/banking/payment-form.page.html index c1c0b17..b9b2f1e 100644 --- a/apps/web/src/app/features/banking/payment-form.page.html +++ b/apps/web/src/app/features/banking/payment-form.page.html @@ -11,10 +11,10 @@
- + @if (!toAccounts()) { - + }
@if (isNew()) { - + } @else { - + } - - + +
@@ -66,15 +66,15 @@

Medios de pago

- - - - + + + +
- - - + + +
@@ -94,8 +94,8 @@
- - + +
diff --git a/apps/web/src/app/features/documents/document-form.page.html b/apps/web/src/app/features/documents/document-form.page.html index 4e42c9f..3fc1acd 100644 --- a/apps/web/src/app/features/documents/document-form.page.html +++ b/apps/web/src/app/features/documents/document-form.page.html @@ -133,7 +133,7 @@ @switch (f.type) { @case ('partner') { } @case ('folio') { - + } @case ('group') { - + @for (c of f.cells; track c.key) { } @case ('series') { - + -

{{ f.label }}

+

{{ f.label }}

} @case ('currency') { - + + + } @default { allFields(lay export const hasCurrency = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'currency'); export const hasFolio = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'folio' || (f.type === 'group' && f.cells.some((c) => c.key === 'FolioNumber'))); + +/** + * Nombre(s) del campo de SAP de una fila del formulario, para «Campos» y la ayuda del cursor: el de un campo sencillo, o + * todos los de una fila con varios controles (`FolioPrefixString · FolioNumber`). + */ +export function fieldDbName(f: HeaderField): string | undefined { + switch (f.type) { + case 'folio': + return `${f.prefixKey} · ${f.numberKey}`; + case 'group': + return f.cells.map((c) => c.key).join(' · '); + case 'series': + return 'Series · DocNum'; + case 'currency': + return 'DocCurrency · DocRate'; + case 'rounding': + return 'Rounding · RoundingDiffAmount'; + case 'expenses': + return 'DocumentAdditionalExpenses'; + case 'computed': + return COMPUTED_DB_NAMES[f.source]; + default: + return 'key' in f ? f.key : undefined; + } +} + +const COMPUTED_DB_NAMES: Readonly> = { + docNum: 'DocNum', + partnerName: 'CardName', + partnerTaxId: 'FederalTaxID', + subtotal: 'Σ líneas', + expenses: 'Σ DocumentAdditionalExpenses', + tax: 'VatSum', + total: 'DocTotal', + downPayment: 'DownPaymentAmount', + applied: 'PaidToDate', + balance: 'DocTotal − PaidToDate', +}; + diff --git a/apps/web/src/app/features/finance/journal-form.page.html b/apps/web/src/app/features/finance/journal-form.page.html index ff383a6..486a81e 100644 --- a/apps/web/src/app/features/finance/journal-form.page.html +++ b/apps/web/src/app/features/finance/journal-form.page.html @@ -12,21 +12,21 @@
@if (isNew()) { - + } @else { - + } - +
- - - + + +
- + @if (!saved()) { @@ -36,9 +36,9 @@
- - - + + +
diff --git a/apps/web/src/app/shared/form/sbo-field.ts b/apps/web/src/app/shared/form/sbo-field.ts index 781e9a5..f17117b 100644 --- a/apps/web/src/app/shared/form/sbo-field.ts +++ b/apps/web/src/app/shared/form/sbo-field.ts @@ -22,7 +22,7 @@ let nextId = 0; [class.link]="linkable()" [attr.title]="dbName() ?? null" (click)="labelClick.emit()" - >{{ label() }}@if (dbName() && names.show()) { + >{{ label() }}@if (dbName() && names.show()) { {{ dbName() }} } @@ -51,7 +51,7 @@ let nextId = 0; text-overflow: ellipsis; white-space: nowrap; } - label.req::after { + label.req .t::after { content: ' *'; color: var(--err); } @@ -74,6 +74,8 @@ let nextId = 0; margin-top: -2px; font: 10.5px/1.2 var(--mono, monospace); color: var(--quiet); + white-space: normal; + overflow-wrap: anywhere; } label.link { cursor: pointer; diff --git a/apps/web/src/app/shared/grid/lines-grid.html b/apps/web/src/app/shared/grid/lines-grid.html index e2e1b81..ac2312c 100644 --- a/apps/web/src/app/shared/grid/lines-grid.html +++ b/apps/web/src/app/shared/grid/lines-grid.html @@ -15,7 +15,7 @@
@for (c of columns(); track c.key; let j = $index) { - @if (hasDetails() && j === detailAt()) { diff --git a/apps/web/src/app/shared/grid/lines-grid.ts b/apps/web/src/app/shared/grid/lines-grid.ts index f5cd035..53cd636 100644 --- a/apps/web/src/app/shared/grid/lines-grid.ts +++ b/apps/web/src/app/shared/grid/lines-grid.ts @@ -12,7 +12,7 @@ import { output, signal, } from '@angular/core'; -import { FieldNames } from '../../core/field-names'; +import { FieldNames, isSapField } from '../../core/field-names'; import { formatNumber, parseDecimal } from '../../core/format'; import { SlLookupApi } from '../../core/sl/sl-lookup'; import type { ListRow } from '../list/list.types'; @@ -59,6 +59,7 @@ export class LinesGrid { private readonly host = inject>(ElementRef); private readonly injector = inject(Injector); protected readonly names = inject(FieldNames); + protected readonly isSap = isSapField; constructor() { // las celdas de código con texto propio (la cuenta con su formato) piden los textos de las líneas que se cargan diff --git a/apps/web/src/app/shared/grid/rows-table.ts b/apps/web/src/app/shared/grid/rows-table.ts index af0b95b..ecb37e7 100644 --- a/apps/web/src/app/shared/grid/rows-table.ts +++ b/apps/web/src/app/shared/grid/rows-table.ts @@ -1,7 +1,7 @@ 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 { FieldNames } from '../../core/field-names'; +import { FieldNames, isSapField } from '../../core/field-names'; import { formatNumber, parseDecimal } from '../../core/format'; import { SlLookupApi } from '../../core/sl/sl-lookup'; import type { ListRow } from '../list/list.types'; @@ -32,7 +32,7 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10) @for (c of columns(); track c.key) { - } @@ -186,6 +186,7 @@ export class RowsTable { }); protected readonly fieldNames = inject(FieldNames); + protected readonly isSap = isSapField; private readonly lookupApi = inject(SlLookupApi); private readonly http = inject(HttpClient); /** Nombres de los catálogos de las columnas `nameOf`, por recurso. */ diff --git a/apps/web/src/app/shared/list/data-table.html b/apps/web/src/app/shared/list/data-table.html index 85423c0..2e09881 100644 --- a/apps/web/src/app/shared/list/data-table.html +++ b/apps/web/src/app/shared/list/data-table.html @@ -19,11 +19,11 @@ [style.text-align]="align(c)" [class.pick]="searchable(c)" [class.active]="c.key === activeColumnKey()" - [attr.title]="(searchable(c) ? 'Buscar en esta columna · ' : '') + c.key" + [attr.title]="(searchable(c) ? 'Buscar en esta columna' : '') + (searchable(c) && isSap(c.key) ? ' · ' : '') + (isSap(c.key) ? c.key : '') || null" (click)="columnPicked.emit(c)" > {{ c.title }} - @if (names.show()) { + @if (names.show() && isSap(c.key)) { {{ c.key }} } diff --git a/apps/web/src/app/shared/list/data-table.ts b/apps/web/src/app/shared/list/data-table.ts index 6dbfa78..5c84889 100644 --- a/apps/web/src/app/shared/list/data-table.ts +++ b/apps/web/src/app/shared/list/data-table.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core'; -import { FieldNames } from '../../core/field-names'; +import { FieldNames, isSapField } from '../../core/field-names'; import { ColResize } from '../table/col-resize'; import { Badge } from '../window/badge'; import { type ListColumn, type ListRow, alignOf, cellText, isSearchable, type Tone } from './list.types'; @@ -17,6 +17,7 @@ import { type ListColumn, type ListRow, alignOf, cellText, isSearchable, type To }) export class DataTable { protected readonly names = inject(FieldNames); + protected readonly isSap = isSapField; readonly columns = input.required(); readonly rows = input.required();
#{{ c.title }}@if (names.show()) { + {{ c.title }}@if (names.show() && isSap(c.key)) { {{ c.key }} }
#{{ c.title }}@if (fieldNames.show()) { + {{ c.title }}@if (fieldNames.show() && isSap(c.key)) { {{ c.key }} }