Files
basic-erp/apps/web/README.md
T
GeorgeandClaude Sonnet 5.5 fad66abec4 refactor(web): esquinas como variables del tema (--radius)
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>
2026-10-13 20:05:32 -05:00

138 lines
8.5 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).
### `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`.