Merge branch 'main' into cliente/tagumedica
This commit is contained in:
commit
ac4fdea372
19 files changed
+168
-37
No files matched your search
@@ -58,6 +58,10 @@ Cada menú abre primero una **lista de los últimos documentos** (barra de filtr
|
||||
|
||||
Módulo genérico `apps/api/src/sapdb/` (en `main`, opcional como el Gateway): ejecuta consultas **registradas** de solo lectura directamente en la base de SAP, para lo que el Service Layer no da bien (por ejemplo, el stock de un lote por almacén o el almacén de origen de un traslado). Hoy el motor es **HANA** (`hdb`, JavaScript puro); la interfaz `SapDbAdapter` deja listo sumar **MSSQL** (SAP10 de pruebas) con otro adaptador, y cada consulta declara su SQL por motor. En `main` solo existe `ping`; **las consultas de negocio van en la rama de cada cliente** (`sapdb.extensions.ts`, vacío en `main`) porque dependen de sus tablas y campos de usuario. El frontend nunca manda SQL: pide la consulta por nombre con parámetros tipados (`POST /api/sapdb/queries/:name`); el SQL usa `:nombre` y el servidor lo cambia por `?` (nunca se concatena). Al arrancar se rechaza una consulta que no sea un único `SELECT`/`WITH`. Cola propia (`SAPDB_MAX_CONCURRENCY` 2), tope de filas y tiempo máximo; auditoría `db_query` (nombres, no valores). Variables `SAPDB_*` en el `.env`. Verificado contra `SBO_TAGUMEDICA_21` (conexión, esquema y parámetro). **Aviso:** el `.env` del mini-erp usa el usuario `SYSTEM` de HANA (superusuario): conviene un usuario de la base que solo pueda leer; el módulo igualmente solo ejecuta los `SELECT` registrados.
|
||||
|
||||
## 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).
|
||||
|
||||
## Cómo verificar cambios de interfaz
|
||||
|
||||
- Probar contra el SAP real con la extensión de Chrome (`list_connected_browsers` debe mostrar un navegador). Si no conecta, usar Chrome headless por CDP.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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';
|
||||
|
||||
@Component({ imports: [TextField], template: '<sbo-text-field label="Descripción" dbName="ItemName" [value]="\'x\'" />' })
|
||||
class Host {}
|
||||
|
||||
describe('Ver campos', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('el nombre del campo de SAP siempre sale como ayuda de la etiqueta, y debajo solo con «Ver campos»', () => {
|
||||
const names = TestBed.inject(FieldNames);
|
||||
const fixture = TestBed.createComponent(Host);
|
||||
fixture.detectChanges();
|
||||
const label = () => fixture.nativeElement.querySelector('label') as HTMLElement;
|
||||
expect(label().getAttribute('title')).toBe('ItemName');
|
||||
expect(label().querySelector('.db')).toBeNull();
|
||||
names.toggle();
|
||||
fixture.detectChanges();
|
||||
expect(label().querySelector('.db')?.textContent).toBe('ItemName');
|
||||
});
|
||||
|
||||
it('se recuerda en el navegador', () => {
|
||||
TestBed.inject(FieldNames).toggle();
|
||||
expect(localStorage.getItem('erp_fields')).toBe('1');
|
||||
TestBed.resetTestingModule();
|
||||
expect(TestBed.inject(FieldNames).show()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
const KEY = 'erp_fields';
|
||||
|
||||
/**
|
||||
* «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
|
||||
* pasar el cursor sobre la etiqueta. Se recuerda en el navegador.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FieldNames {
|
||||
readonly show = signal(this.read());
|
||||
|
||||
toggle(): void {
|
||||
const next = !this.show();
|
||||
this.show.set(next);
|
||||
try {
|
||||
localStorage.setItem(KEY, next ? '1' : '0');
|
||||
} catch {
|
||||
/* sin almacenamiento: vale para esta sesión */
|
||||
}
|
||||
}
|
||||
|
||||
private read(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@
|
||||
@switch (f.type) {
|
||||
@case ('partner') {
|
||||
<sbo-lookup-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[required]="!!f.required"
|
||||
[readonly]="readOnly() || !isNew() || locked(f)"
|
||||
[value]="str(header()[f.key])"
|
||||
@@ -145,7 +145,7 @@
|
||||
}
|
||||
@case ('checkbox') {
|
||||
<sbo-checkbox-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[readonly]="readOnly() || !!f.readonly || locked(f)"
|
||||
[value]="header()[f.key] === 'tYES'"
|
||||
(valueChange)="setHeader(f.key, $event ? 'tYES' : 'tNO')"
|
||||
@@ -153,7 +153,7 @@
|
||||
}
|
||||
@case ('lookup') {
|
||||
<sbo-lookup-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[required]="!!f.required"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="str(header()[f.key])"
|
||||
@@ -165,7 +165,7 @@
|
||||
}
|
||||
@case ('warehouse') {
|
||||
<sbo-lookup-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[required]="!!f.required"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="str(header()[f.key])"
|
||||
@@ -176,7 +176,7 @@
|
||||
}
|
||||
@case ('date') {
|
||||
<sbo-date-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[required]="!!f.required"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="str(header()[f.key])"
|
||||
@@ -185,7 +185,7 @@
|
||||
}
|
||||
@case ('select') {
|
||||
<sbo-select-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[required]="!!f.required"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[options]="f.options"
|
||||
@@ -196,7 +196,7 @@
|
||||
}
|
||||
@case ('number') {
|
||||
<sbo-number-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[decimals]="f.decimals"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="+str(header()[f.key])"
|
||||
@@ -205,14 +205,14 @@
|
||||
}
|
||||
@case ('textarea') {
|
||||
<sbo-textarea-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[value]="str(header()[f.key])"
|
||||
(valueChange)="setHeader(f.key, $event)"
|
||||
/>
|
||||
}
|
||||
@case ('folio') {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-field [label]="f.label" [dbName]="f.key">
|
||||
<sbo-input
|
||||
width="90px"
|
||||
[maxlength]="10"
|
||||
@@ -229,7 +229,7 @@
|
||||
</sbo-field>
|
||||
}
|
||||
@case ('group') {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-field [label]="f.label" [dbName]="f.key">
|
||||
@for (c of f.cells; track c.key) {
|
||||
<sbo-input
|
||||
[width]="c.width"
|
||||
@@ -246,7 +246,7 @@
|
||||
</sbo-field>
|
||||
}
|
||||
@case ('series') {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-field [label]="f.label" [dbName]="f.key">
|
||||
<sbo-select
|
||||
width="170px"
|
||||
ariaLabel="Serie de numeración"
|
||||
@@ -277,7 +277,7 @@
|
||||
}
|
||||
@case ('contact') {
|
||||
<sbo-select-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[options]="contactOptions()"
|
||||
placeholder=""
|
||||
@@ -287,7 +287,7 @@
|
||||
}
|
||||
@case ('address') {
|
||||
<sbo-select-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
[options]="f.side === 'ship' ? shipOptions() : billOptions()"
|
||||
placeholder=""
|
||||
@@ -302,7 +302,7 @@
|
||||
</sbo-field>
|
||||
}
|
||||
@case ('currency') {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-field [label]="f.label" [dbName]="f.key">
|
||||
<sbo-select
|
||||
width="170px"
|
||||
ariaLabel="Moneda del documento"
|
||||
@@ -322,7 +322,7 @@
|
||||
}
|
||||
@case ('rounding') {
|
||||
@if (roundingEnabled()) {
|
||||
<sbo-field [label]="f.label">
|
||||
<sbo-field [label]="f.label" [dbName]="f.key">
|
||||
<sbo-checkbox
|
||||
ariaLabel="Redondear el total del documento"
|
||||
[readonly]="readOnly() || locked(f)"
|
||||
@@ -340,11 +340,11 @@
|
||||
}
|
||||
}
|
||||
@case ('computed') {
|
||||
<sbo-text-field [label]="f.label" readonly [value]="computedValue(f.source)" />
|
||||
<sbo-text-field [label]="f.label" [dbName]="f.key" readonly [value]="computedValue(f.source)" />
|
||||
}
|
||||
@default {
|
||||
<sbo-text-field
|
||||
[label]="f.label"
|
||||
[label]="f.label" [dbName]="f.key"
|
||||
[readonly]="readOnly() || !!f.readonly || locked(f)"
|
||||
[value]="str(header()[f.key])"
|
||||
(valueChange)="setHeader(f.key, $event)"
|
||||
|
||||
@@ -18,7 +18,7 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
@switch (field().type) {
|
||||
@case ('checkbox') {
|
||||
<sbo-checkbox-field
|
||||
[label]="field().label"
|
||||
[label]="field().label" [dbName]="dbName()"
|
||||
[readonly]="readOnly() || !!ro()"
|
||||
[value]="value() === 'tYES'"
|
||||
(valueChange)="changed.emit($event ? 'tYES' : 'tNO')"
|
||||
@@ -26,7 +26,7 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
}
|
||||
@case ('lookup') {
|
||||
<sbo-lookup-field
|
||||
[label]="field().label"
|
||||
[label]="field().label" [dbName]="dbName()"
|
||||
[required]="required()"
|
||||
[readonly]="readOnly()"
|
||||
[value]="str()"
|
||||
@@ -37,7 +37,7 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
}
|
||||
@case ('warehouse') {
|
||||
<sbo-lookup-field
|
||||
[label]="field().label"
|
||||
[label]="field().label" [dbName]="dbName()"
|
||||
[required]="required()"
|
||||
[readonly]="readOnly()"
|
||||
[value]="str()"
|
||||
@@ -48,7 +48,7 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
}
|
||||
@case ('select') {
|
||||
<sbo-select-field
|
||||
[label]="field().label"
|
||||
[label]="field().label" [dbName]="dbName()"
|
||||
[required]="required()"
|
||||
[readonly]="readOnly()"
|
||||
[options]="options()"
|
||||
@@ -59,7 +59,7 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
}
|
||||
@case ('number') {
|
||||
<sbo-number-field
|
||||
[label]="field().label"
|
||||
[label]="field().label" [dbName]="dbName()"
|
||||
[decimals]="decimals()"
|
||||
[readonly]="readOnly() || !!ro()"
|
||||
[value]="num()"
|
||||
@@ -67,14 +67,14 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
/>
|
||||
}
|
||||
@case ('date') {
|
||||
<sbo-date-field [label]="field().label" [required]="required()" [readonly]="readOnly()" [value]="str()" (valueChange)="changed.emit($event)" />
|
||||
<sbo-date-field [label]="field().label" [dbName]="dbName()" [required]="required()" [readonly]="readOnly()" [value]="str()" (valueChange)="changed.emit($event)" />
|
||||
}
|
||||
@case ('textarea') {
|
||||
<sbo-textarea-field [label]="field().label" [readonly]="readOnly()" [value]="str()" (valueChange)="changed.emit($event)" />
|
||||
<sbo-textarea-field [label]="field().label" [dbName]="dbName()" [readonly]="readOnly()" [value]="str()" (valueChange)="changed.emit($event)" />
|
||||
}
|
||||
@default {
|
||||
<sbo-text-field
|
||||
[label]="field().label"
|
||||
[label]="field().label" [dbName]="dbName()"
|
||||
[required]="required()"
|
||||
[readonly]="readOnly() || !!ro()"
|
||||
[value]="str()"
|
||||
@@ -102,6 +102,10 @@ export class MasterField {
|
||||
return f.type === 'lookup' && f.lookup.valueType === 'number' && /^\d+$/.test(code.trim()) ? Number(code) : code;
|
||||
}
|
||||
/** El texto del campo; una fecha de SAP llega como `2031-10-31T00:00:00Z` y el selector de fecha pide `yyyy-MM-dd`. */
|
||||
protected readonly dbName = computed(() => {
|
||||
const f = this.field();
|
||||
return 'key' in f ? f.key : undefined;
|
||||
});
|
||||
protected readonly str = computed(() => (this.field().type === 'date' ? text(this.value()).slice(0, 10) : text(this.value())));
|
||||
protected readonly num = computed(() => (this.value() === '' || this.value() === null || this.value() === undefined ? null : Number(this.value())));
|
||||
protected readonly required = computed(() => {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
color: var(--shell-quiet);
|
||||
}
|
||||
.out,
|
||||
.fields,
|
||||
.burger {
|
||||
background: transparent;
|
||||
color: var(--shell-ink);
|
||||
@@ -42,7 +43,16 @@
|
||||
.burger {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
.fields {
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
padding: 1px 8px;
|
||||
}
|
||||
.fields.on {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.out:hover,
|
||||
.fields:hover,
|
||||
.burger:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</button>
|
||||
<a class="brand" routerLink="/">Basic ERP <small>{{ session.company() }}</small></a>
|
||||
<span class="sp"></span>
|
||||
<button class="fields" type="button" [class.on]="fieldNames.show()" [attr.aria-pressed]="fieldNames.show()" title="Mostrar el nombre del campo de SAP debajo de cada etiqueta" (click)="fieldNames.toggle()">Campos</button>
|
||||
<sbo-theme-switcher />
|
||||
<span class="user">{{ session.fullName() }}</span>
|
||||
<button class="out" type="button" (click)="logout()">Salir</button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FieldNames } from '../core/field-names';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
import { CompanySettingsService } from '../core/company/company-settings';
|
||||
@@ -31,6 +32,7 @@ export class MainLayout {
|
||||
private readonly auth = inject(AuthService);
|
||||
protected readonly session = inject(SessionStore);
|
||||
protected readonly menu = inject(MenuService);
|
||||
protected readonly fieldNames = inject(FieldNames);
|
||||
|
||||
/** En pantallas estrechas el menú se abre encima del contenido. */
|
||||
protected readonly sidebarOpen = signal(false);
|
||||
|
||||
@@ -8,4 +8,6 @@ export abstract class FieldBase {
|
||||
readonly readOnly = input(false, { alias: 'readonly', transform: booleanAttribute });
|
||||
readonly disabled = input(false, { transform: booleanAttribute });
|
||||
readonly error = input<string>();
|
||||
/** Nombre del campo de SAP (ayuda y «Ver campos»). */
|
||||
readonly dbName = input<string>();
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { SboField } from './sbo-field';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, TextInput],
|
||||
template: `
|
||||
<sbo-field [label]="label()" [required]="required()" [error]="error()">
|
||||
<sbo-field [label]="label()" [dbName]="dbName()" [required]="required()" [error]="error()">
|
||||
<sbo-input
|
||||
[(value)]="value"
|
||||
[readonly]="readOnly()"
|
||||
@@ -37,7 +37,7 @@ export class TextField extends FieldBase {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, CheckboxInput],
|
||||
template: `
|
||||
<sbo-field [label]="label()" [error]="error()">
|
||||
<sbo-field [label]="label()" [dbName]="dbName()" [error]="error()">
|
||||
<sbo-checkbox [(value)]="value" [readonly]="readOnly()" [disabled]="disabled()" />
|
||||
</sbo-field>
|
||||
`,
|
||||
@@ -52,7 +52,7 @@ export class CheckboxField extends FieldBase {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, TextareaInput],
|
||||
template: `
|
||||
<sbo-field [label]="label()" [required]="required()" [error]="error()">
|
||||
<sbo-field [label]="label()" [dbName]="dbName()" [required]="required()" [error]="error()">
|
||||
<sbo-textarea
|
||||
[(value)]="value"
|
||||
[readonly]="readOnly()"
|
||||
@@ -78,7 +78,7 @@ export class TextareaField extends FieldBase {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, NumberInput],
|
||||
template: `
|
||||
<sbo-field [label]="label()" [required]="required()" [error]="error()">
|
||||
<sbo-field [label]="label()" [dbName]="dbName()" [required]="required()" [error]="error()">
|
||||
<sbo-number
|
||||
[(value)]="value"
|
||||
[decimals]="decimals()"
|
||||
@@ -100,7 +100,7 @@ export class NumberField extends FieldBase {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, DateInput],
|
||||
template: `
|
||||
<sbo-field [label]="label()" [required]="required()" [error]="error()">
|
||||
<sbo-field [label]="label()" [dbName]="dbName()" [required]="required()" [error]="error()">
|
||||
<sbo-date [(value)]="value" [readonly]="readOnly()" [disabled]="disabled()" [required]="required()" />
|
||||
</sbo-field>
|
||||
`,
|
||||
@@ -115,7 +115,7 @@ export class DateField extends FieldBase {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SboField, SelectInput],
|
||||
template: `
|
||||
<sbo-field [label]="label()" [required]="required()" [error]="error()">
|
||||
<sbo-field [label]="label()" [dbName]="dbName()" [required]="required()" [error]="error()">
|
||||
<sbo-select
|
||||
[(value)]="value"
|
||||
[options]="options()"
|
||||
|
||||
@@ -33,6 +33,7 @@ const NAMES = new Map<string, string>();
|
||||
template: `
|
||||
<sbo-field
|
||||
[label]="label()"
|
||||
[dbName]="dbName()"
|
||||
[required]="required()"
|
||||
[error]="error() || missingText()"
|
||||
action
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, booleanAttribute, inject, input, output } from '@angular/core';
|
||||
import { FieldNames } from '../../core/field-names';
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
@@ -19,8 +20,11 @@ let nextId = 0;
|
||||
[class.req]="required()"
|
||||
[class.ga]="action()"
|
||||
[class.link]="linkable()"
|
||||
[attr.title]="dbName() ?? null"
|
||||
(click)="labelClick.emit()"
|
||||
>{{ label() }}</label
|
||||
>{{ label() }}@if (dbName() && names.show()) {
|
||||
<span class="db">{{ dbName() }}</span>
|
||||
}</label
|
||||
>
|
||||
<div class="ctl">
|
||||
<div class="sbo-group"><ng-content /></div>
|
||||
@@ -65,6 +69,12 @@ let nextId = 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
}
|
||||
.db {
|
||||
display: block;
|
||||
margin-top: -2px;
|
||||
font: 10.5px/1.2 var(--mono, monospace);
|
||||
color: var(--quiet);
|
||||
}
|
||||
label.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -83,7 +93,10 @@ let nextId = 0;
|
||||
`,
|
||||
})
|
||||
export class SboField {
|
||||
protected readonly names = inject(FieldNames);
|
||||
readonly label = input.required<string>();
|
||||
/** Nombre del campo de SAP; sale como ayuda del cursor y, con «Ver campos», debajo de la etiqueta. */
|
||||
readonly dbName = input<string>();
|
||||
readonly required = input(false, { transform: booleanAttribute });
|
||||
readonly action = input(false, { transform: booleanAttribute });
|
||||
/** La etiqueta es un enlace (ej. abrir el socio de negocio). */
|
||||
|
||||
@@ -151,3 +151,10 @@ tr.none td {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
th .db {
|
||||
display: block;
|
||||
font: 10.5px/1.2 var(--mono, monospace);
|
||||
font-weight: 400;
|
||||
color: var(--quiet);
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
<tr>
|
||||
<th class="rn">#</th>
|
||||
@for (c of columns(); track c.key; let j = $index) {
|
||||
<th sboColResize [style.width]="c.width" [style.text-align]="alignOf(c)" [class.req]="c.required">{{ c.title }}</th>
|
||||
<th sboColResize [style.width]="c.width" [style.text-align]="alignOf(c)" [class.req]="c.required" [attr.title]="c.key">{{ c.title }}@if (names.show()) {
|
||||
<span class="db">{{ c.key }}</span>
|
||||
}</th>
|
||||
@if (hasDetails() && j === detailAt()) {
|
||||
<th class="det">Lotes / Series</th>
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { FieldNames } from '../../core/field-names';
|
||||
import { formatNumber, parseDecimal } from '../../core/format';
|
||||
import { SlLookupApi } from '../../core/sl/sl-lookup';
|
||||
import type { ListRow } from '../list/list.types';
|
||||
@@ -57,6 +58,7 @@ export class LinesGrid {
|
||||
|
||||
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
private readonly injector = inject(Injector);
|
||||
protected readonly names = inject(FieldNames);
|
||||
|
||||
constructor() {
|
||||
// las celdas de código con texto propio (la cuenta con su formato) piden los textos de las líneas que se cargan
|
||||
|
||||
@@ -1,6 +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 { formatNumber, parseDecimal } from '../../core/format';
|
||||
import { SlLookupApi } from '../../core/sl/sl-lookup';
|
||||
import type { ListRow } from '../list/list.types';
|
||||
@@ -31,7 +32,9 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
|
||||
<tr>
|
||||
<th class="rn">#</th>
|
||||
@for (c of columns(); track c.key) {
|
||||
<th [style.width]="c.width" [class.req]="c.required">{{ c.title }}</th>
|
||||
<th [style.width]="c.width" [class.req]="c.required" [attr.title]="c.key">{{ c.title }}@if (fieldNames.show()) {
|
||||
<span class="db">{{ c.key }}</span>
|
||||
}</th>
|
||||
}
|
||||
@if (canRemove()) {
|
||||
<th class="del"></th>
|
||||
@@ -145,6 +148,7 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
|
||||
.menu button:hover { background: var(--btn-h); }
|
||||
table { width: 100%; min-width: max-content; border-collapse: collapse; }
|
||||
th { padding: 4px 6px; background: var(--grid-head); border: 1px solid var(--line); font-size: 12.5px; font-weight: 600; text-align: left; white-space: nowrap; }
|
||||
th .db { display: block; font: 10.5px/1.2 var(--mono, monospace); font-weight: 400; color: var(--quiet); }
|
||||
th.req::after { content: ' *'; color: var(--err); }
|
||||
td { padding: 0 2px; height: 27px; border: 1px solid var(--line-soft); white-space: nowrap; }
|
||||
td.ro { background: var(--field-ro); }
|
||||
@@ -181,6 +185,7 @@ export class RowsTable {
|
||||
return rows.filter((r) => keys.some((k) => Number(r[k]) !== 0 && Number.isFinite(Number(r[k]))));
|
||||
});
|
||||
|
||||
protected readonly fieldNames = inject(FieldNames);
|
||||
private readonly lookupApi = inject(SlLookupApi);
|
||||
private readonly http = inject(HttpClient);
|
||||
/** Nombres de los catálogos de las columnas `nameOf`, por recurso. */
|
||||
|
||||
@@ -75,3 +75,10 @@ tr.none td {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th .db {
|
||||
display: block;
|
||||
font: 10.5px/1.2 var(--mono, monospace);
|
||||
font-weight: 400;
|
||||
color: var(--quiet);
|
||||
}
|
||||
@@ -19,10 +19,13 @@
|
||||
[style.text-align]="align(c)"
|
||||
[class.pick]="searchable(c)"
|
||||
[class.active]="c.key === activeColumnKey()"
|
||||
[attr.title]="searchable(c) ? 'Buscar en esta columna' : null"
|
||||
[attr.title]="(searchable(c) ? 'Buscar en esta columna · ' : '') + c.key"
|
||||
(click)="columnPicked.emit(c)"
|
||||
>
|
||||
{{ c.title }}
|
||||
@if (names.show()) {
|
||||
<span class="db">{{ c.key }}</span>
|
||||
}
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core';
|
||||
import { FieldNames } 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';
|
||||
@@ -15,6 +16,8 @@ import { type ListColumn, type ListRow, alignOf, cellText, isSearchable, type To
|
||||
styleUrl: './data-table.css',
|
||||
})
|
||||
export class DataTable {
|
||||
protected readonly names = inject(FieldNames);
|
||||
|
||||
readonly columns = input.required<readonly ListColumn[]>();
|
||||
readonly rows = input.required<readonly ListRow[]>();
|
||||
/** Campo único de cada fila. */
|
||||
|
||||
Reference in new issue
Block a user