Files
basic-erp/apps/web/README.md
T

228 lines
17 KiB
Markdown
Raw Normal View History

# web
Frontend Angular (standalone, zoneless, señales) del ERP base. Interfaz densa y ligera estilo SAP Business One.
```bash
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.
```html
<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)" />
```
```ts
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:
```html
<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:
```html
<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`):
```html
<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.