Skip to main content

Estructura y convenciones

oxitesa-front es la consola web (back-office): Angular 19 standalone, PrimeNG 19, Tailwind 3.4, TypeScript 5.7 en modo estricto.

Comandos

npm start # dev server en localhost:4200
npm run build # build
npm run build:prod # build de producción
npm run build:pre # genera environment-pre.ts desde env vars + build de PRE
El package manager es pnpm

El repo tiene pnpm-lock.yaml. Los scripts npm delegan al Angular CLI, pero para instalar paquetes usá pnpm.

Estructura

src/app/
├── pages/ # pantallas, lazy-loaded por ruta
├── components/ # componentes compartidos
├── services/ # servicios globales (auth, menu, loading, notification, user-access…)
├── interceptors/ # auth + error-to-toast
├── guards/ # auth.guard, permission.guard
├── interfaces/ # ApiResponse, FindPaginableDTO, SortDTO…
├── enums/ # permission-codes, table-state-key, local-storage…
├── api/ # URLs de API tipadas (typesafe-routes)
├── constants/ # mensajes de error, locale es-AR de PrimeNG
├── pipes/ # apiCall, maxLength, time-lapse
├── app.routes.ts
├── app.config.ts
└── primeng.preset.ts # tema custom (cyan/azul, dark mode)
Dónde va un servicio
  • Servicio de una feature → dentro de su carpeta de página (pages/deposit/deposit.service.ts).
  • Servicio global / cross-featuresrc/app/services/.

Nunca llames HttpClient desde un componente: siempre a través de un servicio que devuelva Observable<ApiResponse<T>>.

Convenciones de código

  • Standalone components, sin NgModules.
  • DI con inject() siempre, nunca por constructor.
  • input<T>() / output<T>() de Angular 19, nunca @Input/@Output.
  • Signals para estado de componente y de servicio; RxJS para HTTP y flujos complejos.
  • Reactive Forms con inject(FormBuilder). La definición del form va en la clase, no en el template.
  • Limpieza de suscripciones con takeUntilDestroyed(). Nunca unsubscribe() a mano ni OnDestroy.
  • firstValueFrom() para consumir un único valor dentro de un handler async.
  • Prefijo de selector oxi-.
  • Formateo con Prettier + prettier-plugin-tailwindcss. No hay configuración de ESLint.
  • Alias de paths: @pages, @services, @components, @pipes, @env, @interfaces, @api.
Conviven tres estilos

Vas a encontrar componentes nuevos con signals, componentes viejos con @Input/@Output y template: inline, y componentes por atributo ([oxiCard], [oxiChip], [oxiFieldError]). Los viejos siguen en uso pero no son molde: escribí lo nuevo con las convenciones de arriba.

Sin tests

No hay archivos .spec en src/ y el Angular CLI está configurado con skipTests: true. No escribas specs acá. La validación es tsc --noEmit, ng build y revisión visual. El TDD aplica al backend — ver Testing.

El patrón de datos asíncronos

Es el molde estándar del repo y evita tener tres signals (loading, data, error) por tabla.

refresh$ = new Subject<void>();

data$ = this.refresh$.pipe(
startWith(null),
switchMap(() => this.service.getAll()),
);

recargar() { this.refresh$.next(); }
@switch ((data$ | apiCall | async)!.state) {
@case ('loading') { <p-progressSpinner /> }
@case ('success') { <!-- la tabla --> }
@case ('error') { <oxi-api-error (onRetry)="recargar()" /> }
}

El pipe apiCall transforma Observable<ApiResponse<T>> en una unión discriminada loading | success | error, arrancando en loading.

totalRecords del pipe no se llena

Está declarado en el tipo Success pero el pipe nunca lo asigna. Si necesitás el total para paginar server-side, leelo de ApiResponse.totalRows por tu cuenta.

Interceptores y errores

Corren dos, en orden:

  1. authInterceptor — adjunta el Bearer de Clerk.
  2. errorInterceptor — convierte el success: false del backend en toast + throw.
No toastees el error de un request a la API

Ya está toasteado por el interceptor. Si lo hacés vos también, el usuario ve dos toasts.

Toastear a mano tiene sentido solo para: errores que no vienen de la API (validación local, permisos del navegador), y éxitos (el interceptor no dice nada cuando todo sale bien).

El interceptor reconoce una respuesta de la API porque el body tiene success + data + err. Los pocos endpoints que responden con CustomResponseDTO no incluyen err, así que no toastean y no tiran. Si un error "no se ve", suele ser eso.

La política de 401

No es "401 → desloguear". La secuencia es:

  1. Vuelve 401 → se pide un token fresco y se reintenta una vez.
  2. Si no se pudo obtener token nuevo → no desloguea. Casi siempre es un fallo transitorio de Clerk (429 o red). Deslogar ahí convertía un rate-limit pasajero en un logout duro.
  3. Si con el token fresco vuelve a dar 401 → ahí sí la sesión es inválida y se cierra.

Componentes reutilizables

No hay un "list shell" ni un contrato de armonía formal: cada listado arma su propia p-table. Lo que sí existe y conviene reusar:

Superficie y feedback

ComponenteQué es
[oxiCard]Card por atributo: borde, sombra, rounded-xl
[oxiChip]Chip inline por atributo
[oxiFieldError]Error de validación de un FormControl. Solo se muestra si el control fue tocado; el host tiene empty:hidden así que no ocupa espacio cuando no hay error. Muestra un solo error por vez
oxi-api-errorBloque de error con botón Reintentar y diálogo con el error crudo
oxi-large-textTexto truncado con tooltip + copiar al portapapeles
oxi-commentTextarea de comentario con Aceptar/Eliminar

Cinco componentes con la misma forma (dropdown + botón "Elegir" que emite selectionChange). Fijate si el tuyo existe antes de escribir el sexto:

oxi-warehouses (depósitos del usuario) · oxi-transportist · oxi-products-search (por elementId) · elements · app-find-supply (busca por producto o número de serie, con filtro server-side y un predicado validate opcional).

Todos usan appendTo="body" en el dropdown: sin eso el overlay queda cortado dentro de un diálogo o de una tabla con overflow.

Bloque de remitos

components/delivery-note/ es el subárbol más grande y más reutilizable: un directorio por tipo (delivery/, visit/, retirement/) más un shared/ con las piezas que se reusan entre los tres:

oxi-delivery-note-info (cabecera) · app-handle-amounts (cantidades pedida/aceptada/entregada) · app-set-serial-number-button + modal · app-set-product-button + modal · oxi-auto-match-product-button (matchea producto por prefijo de serie) · app-change-element-by · find-supply-from-warehouse.

Los componentes "grandes" (aceptar / recibir / crear, uno por tipo de remito) son pantallas completas que se importan y montan en el template del listado, controladas por un p-dialog con flag de visibilidad — no se abren con DialogService.open().

Otros

patient-supplies + add-patient-supply-dialog · vehicles/ (asignaciones, eventos, imágenes) · notifications/ (campana, ítem, detalle) · app-geolocation (mapa Google + autocomplete + geocoding) · supply-note-files.

Pipes de dominio

time-lapse.pipe.ts exporta tres: timeago, faltan_aceptacion y faltan_envio. Los dos últimos calculan con horario hábil argentino, no con horas corridas.

Entornos

ConfigAPISocket
developmenthttp://localhost:3000/v1
prehttps://preoxi.4quinas.com.ar/api/v1producción
productionhttps://app.oxigenoytecnologia.com/api/v1wss://app.oxigenoytecnologia.com

environment.ts también trae la publishable key de Clerk, la config de Firebase, el tenant de Power BI, las claves de PostHog y defaultRowSize (15, el page size inicial de las tablas).

El WebSocket está apagado

SocketService tiene un return; incondicional en el constructor antes del efecto que conecta, y además ningún componente lo inyecta. El tiempo real de la consola web hoy es el polling de 30 s del contador de notificaciones. No es un bug a arreglar sin hablarlo: es el estado actual.