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

206 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: 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:
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ú; las que aún no existen muestran "no disponible" (`ComingSoonPage`). Para construir una pantalla, se reemplaza su `loadComponent`.
- 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 las pantallas con `partner` en su `DocumentScreen` (ventas y compras); el resto de documentos muestra aún un aviso.
- **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` (ventas) o `PurchaseTaxCodes` (compras).
- **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".
- **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`.
### `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.