Files
basic-erp/apps/web

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 con loadComponent para cargar solo lo que se usa.
  • Presupuesto del bundle inicial en angular.json (aviso a 300 kB). Hoy: ~289 kB (82 kB comprimido). La configuración de las pantallas (documentos, maestros, pagos) se carga en diferido con un resolve; el enrutador solo necesita sus listas de rutas (*-paths.ts).
  • 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:

  1. src/styles/themes/<id>.css con un bloque :root[data-theme='<id>'] (copiar los tokens de light.css).
  2. @import en src/styles.css.
  3. 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: MenuService carga GET /api/user/menu una vez por sesión; el menú visible sale de MENU (@basic-erp/sap-types) filtrado por permisos.
  • app.routes.ts genera una ruta por pantalla del menú (documentos, maestros, pagos, asientos, administración); una ruta del menú sin pantalla muestra "no disponible" (ComingSoonPage). Un test comprueba que cada ruta del menú tiene su pantalla y su permiso.
  • 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 $select se 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 con contains(), combinada con baseFilter entre 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) => string para 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).

Formulario de documentos de ventas y compras (document-form.page)

Lo usan todas las pantallas de documento: ventas y compras (con socio, líneas con precios e impuestos) y las de inventario y solicitudes (sin socio ni totales). Cada pantalla declara su DocumentLayout (document-layouts.ts): campos de cabecera y pie (máximo 2 columnas), columnas de línea y colección que se guarda; el formulario no sabe de qué documento se trata.

  • Cabecera: socio de negocio (lista de valores filtrada por CardType; no se cambia en un documento ya creado), Nº de referencia, fechas y descuento %; líneas con sbo-lines-grid; totales informativos (SAP recalcula al guardar) con las tasas de SalesTaxCodes (también en compras: el Service Layer no tiene un maestro propio; partner.taxResource permite cambiarlo en una rama de cliente).
  • Códigos escritos a mano: al salir del campo (socio con resolveOnLeave en sbo-lookup-field; artículo, almacén e impuesto en la grilla) se buscan con SlLookupApi (core/sl/sl-lookup.ts) en la misma lista que abre "…": se normaliza el código y se completa nombre/descripción; si no existe se avisa (en el campo o en la barra de estado) y la celda vuelve a su valor.
  • Crear: valida sin SAP (socio, fecha, al menos una línea con artículo, cantidad > 0), quita campos vacíos y filas en blanco, y abre el documento creado.
  • Actualizar: envía solo la cabecera que cambió y, si cambiaron las líneas, todas con B1S-ReplaceCollectionsOnPatch: true. Lleva If-Match con el odata.etag leído: si otro usuario cambió el documento, SAP responde 412, no se guarda nada y se ofrece "Recargar".
  • Copiar a / Copiar de (document-copy.ts): los flujos estándar (oferta → orden → entrega → factura → nota de crédito, y los de compras) están en COPY_FLOWS. Copiar a (documento guardado, abierto y sin cambios) abre el documento de destino nuevo con el origen en el estado de la navegación; Copiar de (documento nuevo) lista los abiertos del origen elegido, del mismo socio si ya se indicó. Se copian las líneas abiertas con la cantidad pendiente y BaseType/BaseEntry/BaseLine, con lo que SAP enlaza y cierra el origen.
  • Cerrado o cancelado: solo lectura (sin botón de guardar).
  • Navegación primero / anterior / siguiente / último por DocEntry.
  • La lógica sin Angular está en document-model.ts (totales, payload, validación, navegación) y el acceso HTTP en core/sl/sl-document.ts.

Campos por cliente (extensiones)

Cada módulo tiene su archivo de extensiones, vacío en main (ventas.extensions.ts, compras.extensions.ts, inventario.extensions.ts, socios.extensions.ts, articulos.extensions.ts, finanzas.extensions.ts). La rama cliente/<nombre> agrega ahí, por ruta de pantalla, campos de cabecera o pie, columnas de línea y de lista (estándar o U_*), y puede ocultar (hide…) o dejar de solo lectura (readOnly…) lo que no use. Un nombre inexistente, ocultar un campo obligatorio o una ruta ajena lanzan un error al arrancar.

Maestros (features/masters)

Clientes, proveedores, artículos y plan de cuentas comparten una lista y un formulario (MasterScreen / MasterLayout): código y nombre arriba y los demás campos en pestañas, con tablas editables (sbo-rows-table: direcciones, contactos, precios, stock por almacén). El código y los campos immutable no se cambian una vez creado el registro; al modificar se envía If-Match y un 412 avisa del conflicto. El plan de cuentas abre como árbol (chart-tree.page) y su formulario muestra las casillas de grupo.

Bancos y finanzas

  • Pagos recibidos y efectuados (features/banking): un mismo formulario configurado por PaymentScreen. Se aplican a las facturas y notas de crédito abiertas del socio, a cuenta (lo que sobra de los medios queda como anticipo) o a cuentas contables; medios: efectivo, transferencia, cheques y, en los recibidos, tarjetas de crédito. Un pago contabilizado no se edita: se consulta y se anula (Cancel). Lógica sin Angular en payment-model.ts.
  • Asientos contables (features/finance): líneas con cuenta, debe o haber, dimensiones, proyecto, referencias y vencimiento; solo se crea si cuadra. Los manuales se cancelan (SAP genera el reverso). Lógica en journal-model.ts.

Administración

/admin/permisos (permisos de menú) y /admin/auditoria (consulta de la auditoría: filtros por usuario, acción, recurso, resultado y fechas), solo con acceso total.

Permisos de menú (/admin/permisos)

Pantalla del módulo "Administración", solo para quien tiene acceso total (*; el API de permisos exige ser administrador). Lista los usuarios web con permisos y, al elegir uno, muestra los módulos y pantallas con casillas (el módulo completo se guarda como /modulo/*, las pantallas sueltas como rutas). "Acceso total" guarda *; "Administración" no tiene casilla: solo llega con acceso total. Los patrones que no corresponden a ninguna pantalla actual se conservan. Un usuario nuevo se agrega por su U_WEB_USER y no existe hasta pulsar Guardar. El servidor impide quitar al último administrador. La conversión casillas ↔ patrones está en features/admin/permissions-model.ts.

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 (con defaultValue, ej. Estado = Abierto), multi (chips + lista de valores) y date (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 salvo searchable: true; tone pinta 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.

Ventana de formulario (shared/window)

Marco de las pantallas nuevo y :docEntry, con la estructura del mockup:

<sbo-toolbar aria-label="Acciones del documento">
  <button class="tb" type="button"><sbo-icon name="new" />Nueva</button>
  <button class="tb" type="button"><sbo-icon name="find" />Buscar</button>
  <span class="tb-sep"></span>
  <button class="tb" type="button" aria-label="Primero"><sbo-icon name="first" /></button>
  <!-- prev · next · last · print -->
</sbo-toolbar>

<sbo-window title="Oferta de ventas" [badge]="{ label: 'Nueva', tone: 'info' }" [status]="status()">
  <div class="sbo-hdr"><!-- filas de campos --></div>
  <sbo-tabs>
    <sbo-tab label="Contenido">…</sbo-tab>
    <sbo-tab label="Logística">…</sbo-tab>
  </sbo-tabs>

  <button sboActions class="btn main" type="button">Crear</button>
  <button sboActions class="btn" type="button">Cancelar</button>
  <sbo-menu-button sboActionsEnd label="Copiar de" [items]="items" (picked)="copyFrom($event)" />
</sbo-window>
  • sbo-window: título, insignia de estado (tone: info Nueva, ok Abierta, muted Cerrada, err Cancelada, warn), cuerpo, zona de acciones (sboActions a la izquierda, sboActionsEnd a la derecha) y barra de estado ({ kind: 'info' | 'ok' | 'warn' | 'err', text }, con role="status").
  • sbo-toolbar + botones .tb + sbo-icon (iconos SVG en línea: new find first prev next last print).
  • sbo-tabs / sbo-tab: los paneles ocultos siguen en el DOM (conservan el estado); [(selected)]; flechas ←/→.
  • sbo-menu-button: lista de acciones que se abre hacia arriba (o direction="down"); se cierra con Esc o clic fuera; opciones disabled.
  • sbo-badge: la misma insignia que usan las listas.

Grilla de líneas (sbo-lines-grid)

Tabla editable de las líneas del documento, con columnas declarativas (GridColumn):

<sbo-lines-grid
  [columns]="DOCUMENT_LINE_COLUMNS"
  [(lines)]="lines"
  [newLine]="newDocumentLine"
  [recalc]="recalcLine"
  [multiAdd]="ITEMS_MULTI_ADD"
  [readonly]="documentClosed()" />
  • Columnas estándar (features/documents/line-columns.ts): Nº de artículo, Descripción, Cantidad, Almacén, Precio, % Dto., Impuesto y Total. El impuesto es TaxCode (maestro SalesTaxCodes, como en el mini-erp); un cliente que use VatGroup cambia esa columna en su rama.
  • Celdas con lista de valores (lookup): botón "…" o F4; al elegir, apply copia a la línea los campos de la fila elegida (artículo → código + descripción).
  • Las celdas se confirman al salir de ellas o con Enter, que baja a la misma columna de la fila siguiente y crea una línea al llegar a la última. ↑/↓ cambian de fila. Números con formato fuera de foco y valor crudo al editar; un texto que no es número conserva el anterior.
  • recalc recalcula la línea tras un cambio (recalcLine: cantidad × precio × (1 − % dto.)); SAP recalcula impuestos y totales al guardar.
  • multiAdd: "Elegir artículos…" agrega varias líneas de una vez (el "catálogo completo" del mini-erp).
  • readonly (documento cerrado o cancelado): sin edición, sin botones de añadir/quitar ni listas.

Página de componentes (solo desarrollo)

Con npm run dev:web, abrir /dev/componentes: muestra juntos la toolbar, la ventana, los campos, las pestañas, la grilla y las listas de valores sobre datos reales de SAP, sin guardar nada. La ruta solo coincide en modo desarrollo (canMatch: isDevMode), no aparece en el menú y se actualiza sola con cada cambio de componentes.