feat(web): app Angular, librería compartida sap-types y sistema de temas

- apps/web: Angular 22 standalone, zoneless, OnPush, vitest; proxy /api
  hacia el backend; presupuesto de bundle (aviso 260 kB, error 400 kB).
- libs/sap-types: tipos de auth/permisos/Service Layer y el evaluador de
  patrones de menú, compartidos por api y web (se compila en postinstall).
  El backend deja de tener su propia copia del evaluador.
- Temas por variables CSS: claro por defecto con la paleta del mockup de
  Oferta de ventas, oscuro incluido; ThemeService + selector, con el tema
  guardado aplicado antes del primer pintado. Agregar un tema = 1 css +
  1 import + 1 entrada.
- Criterios de interfaz ligera en CLAUDE.md y README.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-13 19:11:05 -05:00
1 parent c0990ca83e
commit f1156929c7
40 files changed
+6231 -5

No files matched your search

@@ -1,27 +0,0 @@
import { isPathAllowed, matchesPattern } from './path-matcher.js';
describe('matchesPattern', () => {
it('"*" permite todo', () => expect(matchesPattern('*', '/lo/que/sea')).toBe(true));
it('"/ventas/*" permite la ruta y sus hijas, no prefijos parecidos', () => {
expect(matchesPattern('/ventas/*', '/ventas')).toBe(true);
expect(matchesPattern('/ventas/*', '/ventas/ordenes')).toBe(true);
expect(matchesPattern('/ventas/*', '/ventas/ordenes/mantenimiento')).toBe(true);
expect(matchesPattern('/ventas/*', '/ventas-x')).toBe(false);
expect(matchesPattern('/ventas/*', '/compras/ordenes')).toBe(false);
});
it('ruta exacta solo coincide consigo misma', () => {
expect(matchesPattern('/ventas/ordenes', '/ventas/ordenes')).toBe(true);
expect(matchesPattern('/ventas/ordenes', '/ventas/ordenes/x')).toBe(false);
});
});
describe('isPathAllowed', () => {
it('null = cualquier sesión, incluso sin permisos', () => expect(isPathAllowed([], null)).toBe(true));
it('sin patrones no hay acceso', () => expect(isPathAllowed([], '/ventas/ordenes')).toBe(false));
it('basta con una de varias rutas requeridas', () => {
expect(isPathAllowed(['/socios/proveedores'], ['/socios/clientes', '/socios/proveedores'])).toBe(true);
expect(isPathAllowed(['/socios/otro'], ['/socios/clientes', '/socios/proveedores'])).toBe(false);
});
});
-16
View File
@@ -1,16 +0,0 @@
/** Patrón de permiso: `*`, `/ventas/*` (la ruta y todo lo que cuelga) o una ruta exacta. */
export function matchesPattern(pattern: string, path: string): boolean {
if (pattern === '*') return true;
if (pattern.endsWith('/*')) {
const base = pattern.slice(0, -2);
return path === base || path.startsWith(`${base}/`);
}
return pattern === path;
}
/** `required`: una ruta o varias (basta con una). `null` = cualquier sesión. */
export function isPathAllowed(patterns: string[], required: string | string[] | null): boolean {
if (required === null) return true;
const paths = Array.isArray(required) ? required : [required];
return paths.some((path) => patterns.some((pattern) => matchesPattern(pattern, path)));
}
+1 -1
View File
@@ -5,8 +5,8 @@ import {
MethodNotAllowedException,
NotFoundException,
} from '@nestjs/common';
import { isPathAllowed } from '@basic-erp/sap-types';
import type { AuthUser } from '../auth/auth.types.js';
import { isPathAllowed } from '../permissions/path-matcher.js';
import { PermissionsService } from '../permissions/permissions.service.js';
import { SapClientService } from '../sap/sap-client.service.js';
import {