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 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)
- Servicio de una feature → dentro de su carpeta de página (
pages/deposit/deposit.service.ts). - Servicio global / cross-feature →
src/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(). Nuncaunsubscribe()a mano niOnDestroy. firstValueFrom()para consumir un único valor dentro de un handlerasync.- 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.
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 llenaEstá 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:
authInterceptor— adjunta el Bearer de Clerk.errorInterceptor— convierte elsuccess: falsedel backend en toast +throw.
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:
- Vuelve 401 → se pide un token fresco y se reintenta una vez.
- 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.
- 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
| Componente | Qué 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-error | Bloque de error con botón Reintentar y diálogo con el error crudo |
oxi-large-text | Texto truncado con tooltip + copiar al portapapeles |
oxi-comment | Textarea de comentario con Aceptar/Eliminar |
Selectores de catálogo
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
| Config | API | Socket |
|---|---|---|
| development | http://localhost:3000/v1 | — |
| pre | https://preoxi.4quinas.com.ar/api/v1 | producción |
| production | https://app.oxigenoytecnologia.com/api/v1 | wss://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).
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.