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