Los radios estaban escritos en cada componente. Ahora salen de --radius (campos), --radius-lg (botones), --radius-pill (insignias) y --radius-panel (tablas y ventanas, 0 por defecto). Sin cambio visual: un tema nuevo puede redondear la interfaz sin tocar componentes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8.5 KiB
web
Frontend Angular (standalone, zoneless, señales) del ERP base. Interfaz densa y ligera estilo SAP Business One.
npm run dev:web # desde la raíz; http://localhost:4200 (proxy /api -> localhost:3000)
npm run build -w apps/web
npm test -w apps/web
Criterios de rendimiento
Es un ERP, no un sitio publicitario: rápido y ligero.
- Sin librerías de UI (nada de Material/PrimeNG): componentes propios, densos (13 px).
- Fuentes del sistema, sin webfonts; sin animaciones.
- Zoneless +
OnPush; rutas conloadComponentpara cargar solo lo que se usa. - Presupuesto del bundle inicial: aviso a 260 kB, error a 400 kB (
angular.json). Hoy: ~200 kB (56 kB comprimido). - Listas largas con paginación del servidor (
$top/$skip) o virtualización.
Temas
El color sale solo de variables CSS (src/styles/themes/*.css). Tema por defecto: claro, con la paleta del mockup de Oferta de ventas.
Agregar un tema:
src/styles/themes/<id>.csscon un bloque:root[data-theme='<id>'](copiar los tokens delight.css).@importensrc/styles.css.- Una entrada en
src/app/core/theme/themes.ts.
Forma: --radius (campos, 2 px), --radius-lg (botones, 3 px), --radius-pill (insignias) y --radius-panel (tablas, ventanas y diálogos; 0 = esquinas rectas como SBO). Un tema puede redondear sin tocar componentes.
Tokens de color: --app --shell --shell-ink --shell-quiet --win --panel --line --line-soft --ink --quiet --field --field-ro --field-b --focus --gold --grid-head --row-sel --row-alt --ok --ok-bg --warn --warn-bg --err --err-bg --btn --btn-b --btn-h --overlay --shadow-pop.
Tipos compartidos
@basic-erp/sap-types (libs/sap-types): usuario, respuestas de la API y el evaluador de permisos de menú, los mismos que usa el backend.
Autenticación y rutas
core/auth:SessionStore(sesión en localStorage, compartida entre pestañas, con timeout deslizante según el servidor),AuthService(login/logout/expire), interceptor (Bearer en/api, renueva actividad y atiende los 401) y guards (authGuard,guestGuard,permissionGuard).core/menu:MenuServicecargaGET /api/user/menuuna vez por sesión; el menú visible sale deMENU(@basic-erp/sap-types) filtrado por permisos.app.routes.tsgenera una ruta por pantalla del menú; las que aún no existen muestran "no disponible" (ComingSoonPage). Para construir una pantalla, se reemplaza suloadComponent.- Un 401 en cualquier llamada cierra la sesión local y lleva al login recordando la ruta (
?returnUrl=, solo rutas internas).
Componentes compartidos (src/app/shared)
sbo-value-help — lista de valores ("elegir de la lista")
Búsqueda por columna sobre un recurso del proxy /api/sl, selección única o múltiple y paginación del servidor.
<sbo-value-help
[open]="open()"
title="Socios de negocio"
resource="BusinessPartners"
[columns]="columns"
baseFilter="CardType eq 'cCustomer'"
orderby="CardCode"
selection="single"
(selected)="onPick($event)"
(closed)="open.set(false)" />
columns: VhColumn[] = [
{ key: 'CardCode', title: 'Código', width: '130px', mono: true },
{ key: 'CardName', title: 'Nombre' },
{ key: 'CurrentAccountBalance', title: 'Saldo', type: 'currency' },
];
- Agregar una columna = una entrada en
columns; el$selectse deriva de ahí. - Columna activa de búsqueda: clic en el encabezado (solo columnas de texto, o
searchable: true). Se aplica al pulsar Buscar/Enter concontains(), combinada conbaseFilterentre paréntesis y con las comillas escapadas. selection="single": un clic en la fila confirma y cierra.selection="multiple": casillas, "seleccionar página" y Confirmar (n); lo marcado se conserva al cambiar de página ([initialSelection]precarga filas).type:text(defecto),number,currency(2 decimales),date(dd/MM/yyyy);format: (row) => stringpara algo a medida.- Teclado: Enter en la búsqueda, Enter sobre una fila, Esc para cerrar.
- Cada apertura reinicia búsqueda, página y selección. Las peticiones obsoletas se cancelan solas (
httpResource).
Campos de formulario (shared/form)
Una fila (sbo-field) es: etiqueta + uno o varios controles en la misma línea. La cabecera de un formulario tiene como máximo 2 columnas (.sbo-hdr con dos .sbo-col). Las combinaciones del mockup (Cliente = input + "…", Canal / Zona = dos mitades, Serie + Número = 72 px + resto, select + número + botón) salen de combinar controles:
<div class="sbo-hdr">
<div class="sbo-col">
<sbo-lookup-field label="Cliente" required [(value)]="cardCode" [lookup]="bp" (picked)="onBp($event)" />
<sbo-text-field label="Nombre" readonly [value]="cardName()" />
<sbo-field label="Canal / Zona">
<sbo-input readonly ariaLabel="Canal" [value]="canal()" />
<sbo-input readonly ariaLabel="Zona" [value]="zona()" />
</sbo-field>
</div>
<div class="sbo-col">
<sbo-field label="Número">
<sbo-input width="72px" readonly ariaLabel="Serie" value="COT" />
<sbo-input readonly ariaLabel="Número correlativo" [value]="numero()" />
</sbo-field>
<sbo-field label="Moneda / Importe">
<sbo-select width="90px" ariaLabel="Moneda" [options]="monedas" [(value)]="moneda" />
<sbo-number [decimals]="2" ariaLabel="Importe" [(value)]="importe" />
<button type="button" class="sbo-btn">…</button>
</sbo-field>
</div>
</div>
Controles combinables (sin etiqueta, dentro de sbo-field; ocupan el espacio sobrante o un width fijo): sbo-input, sbo-textarea, sbo-number, sbo-date, sbo-select y el botón nativo <button class="sbo-btn">. Todos con enlace por señales [(value)], readonly, disabled, required, width y ariaLabel (obligatorio en los que no son el primero de la fila: el primero toma el id de la etiqueta). El error de la fila marca todos sus controles (borde rojo + mensaje, aria-invalid/aria-describedby).
Atajos con etiqueta (un control por fila): sbo-text-field, sbo-textarea-field, sbo-number-field, sbo-date-field, sbo-select-field y sbo-lookup-field (flecha dorada + botón "…" / F4 que abre sbo-value-help según [lookup] (VhLookup), emite (picked) con la fila; linkable + (linkClick)). Comunes: label, required, readonly, disabled, error.
Detalles: sbo-number muestra 1,234.50 fuera de foco y acepta 12,5 o 1,234.50 al escribir (decimals redondea); sbo-date usa texto ISO yyyy-MM-dd; sbo-select devuelve el valor con su tipo original.
Estilos globales en styles/forms.css (.sbo-in, .sbo-sel, .sbo-ta, .sbo-btn, .sbo-group, .sbo-hdr, .sbo-col). El ancho de etiqueta se ajusta con --label-w (165 px por defecto).
Pantallas de documento: lista, nuevo y ver/editar
Cada pantalla de documento del menú abre primero la lista de los últimos documentos (más recientes primero, 50 por página); desde ahí se crea uno nuevo o se abre uno existente:
| Ruta | Muestra |
|---|---|
/ventas/ordenes |
Lista (sbo-list-view) |
/ventas/ordenes/nuevo |
Formulario vacío |
/ventas/ordenes/:docEntry |
Formulario con datos (ver / editar) |
Las tres rutas las gobierna el permiso de menú de la ruta padre. Se generan solas desde features/documents/document-screens.ts (DOCUMENT_SCREENS): por documento define recurso, columnas, campos extra, filtros y el texto del botón de alta. Agregar un documento = una entrada ahí (más su ruta en MENU y su recurso en el proxy del backend).
sbo-list-view y sbo-filter-bar
Lista con barra de filtros y búsqueda por columna, igual que el mini-erp:
- Búsqueda: caja que busca en la columna activa (clic en el encabezado; solo columnas buscables). Texto →
contains; columna numérica → igualdad (DocNum eq 314, avisa si no es un número). - Filtros declarativos (
ListFilter):text,select(condefaultValue, ej. Estado = Abierto),multi(chips + lista de valores) ydate(siempre un rango [desde – hasta], ambos inclusive y opcionales). - Ir aplica búsqueda y filtros en una sola consulta; Limpiar vuelve a los valores por defecto. Editar un filtro no consulta hasta pulsar Ir.
- Una columna con
format(texto calculado) no es buscable salvosearchable: true;tonepinta una insignia (estado). - Entradas:
title,resource,columns,filters,extraFields,baseFilter,orderby,pageSize(50),createLabel; salidas(open)y(create).
El estado de búsqueda/paginación (core/sl/sl-list.ts) y la tabla (sbo-data-table) son compartidos con sbo-value-help.