@tinya-code/gas-admin-ui 0.2.0-alpha.3 → 0.2.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -2
- package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs → tinya-code-gas-admin-ui-calendar-view-ZTYs-r49.mjs} +12 -7
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-ZTYs-r49.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs → tinya-code-gas-admin-ui-condition-evaluator-CHzJuOx2.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-CHzJuOx2.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs → tinya-code-gas-admin-ui-data-source.service-Cw1rdrWZ.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs.map → tinya-code-gas-admin-ui-data-source.service-Cw1rdrWZ.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs → tinya-code-gas-admin-ui-error-mapper.service-BQ--gSnT.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-BQ--gSnT.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-file-DhLi3eO0.mjs → tinya-code-gas-admin-ui-file-JstsF8C5.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-file-DhLi3eO0.mjs.map → tinya-code-gas-admin-ui-file-JstsF8C5.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-CjC-GOQl.mjs → tinya-code-gas-admin-ui-form-view-BcjgVVKC.mjs} +9 -9
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-CjC-GOQl.mjs.map → tinya-code-gas-admin-ui-form-view-BcjgVVKC.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-image-D9-mpBhA.mjs → tinya-code-gas-admin-ui-image-CnGes7gs.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-image-D9-mpBhA.mjs.map → tinya-code-gas-admin-ui-image-CnGes7gs.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B4u2ibPb.mjs → tinya-code-gas-admin-ui-kanban-view-QK7OSFoV.mjs} +4 -4
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B4u2ibPb.mjs.map → tinya-code-gas-admin-ui-kanban-view-QK7OSFoV.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-list-CQpL6vyE.mjs → tinya-code-gas-admin-ui-list-CiNQ3DMg.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-list-CQpL6vyE.mjs.map → tinya-code-gas-admin-ui-list-CiNQ3DMg.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CTQPGBOK.mjs → tinya-code-gas-admin-ui-multiselect-BiKb7192.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CTQPGBOK.mjs.map → tinya-code-gas-admin-ui-multiselect-BiKb7192.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-relation-lr_XG6jt.mjs → tinya-code-gas-admin-ui-relation-CvxkEGHs.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-relation-lr_XG6jt.mjs.map → tinya-code-gas-admin-ui-relation-CvxkEGHs.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CcIgBsSl.mjs → tinya-code-gas-admin-ui-render-registry-B7mmgFSW.mjs} +10 -10
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CcIgBsSl.mjs.map → tinya-code-gas-admin-ui-render-registry-B7mmgFSW.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs → tinya-code-gas-admin-ui-schema-catalog-CsB26qUM.mjs} +190 -61
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CsB26qUM.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-seed-Bn6tl-yM.mjs → tinya-code-gas-admin-ui-seed-CJEt__kZ.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-seed-Bn6tl-yM.mjs.map → tinya-code-gas-admin-ui-seed-CJEt__kZ.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-select-BwefyMVx.mjs → tinya-code-gas-admin-ui-select-DPKRS6Gn.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-select-BwefyMVx.mjs.map → tinya-code-gas-admin-ui-select-DPKRS6Gn.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs → tinya-code-gas-admin-ui-upload.service-BNvH04S3.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs.map → tinya-code-gas-admin-ui-upload.service-BNvH04S3.mjs.map} +1 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +51 -53
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/tinya-code-gas-admin-ui.d.ts +190 -12
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { EnvironmentProviders, InjectionToken, Provider, Signal, TemplateRef, Type } from "@angular/core";
|
|
3
3
|
import { CanActivateFn, Route, Router, Routes } from "@angular/router";
|
|
4
4
|
import { Observable } from "rxjs";
|
|
5
|
-
import { HttpEvent, HttpInterceptorFn } from "@angular/common/http";
|
|
5
|
+
import { HttpClient, HttpEvent, HttpHeaders, HttpInterceptorFn, HttpParams } from "@angular/common/http";
|
|
6
6
|
import { CdkDragDrop } from "@angular/cdk/drag-drop";
|
|
7
7
|
import { LucideIcon } from "@lucide/angular";
|
|
8
8
|
import { FieldTree, PathKind, SchemaFn, SchemaPath, SchemaPathRules } from "@angular/forms/signals";
|
|
@@ -322,6 +322,12 @@ export interface WidgetBase {
|
|
|
322
322
|
help?: string;
|
|
323
323
|
/** Ancho en la grilla del panel (1–12), igual que un campo. */
|
|
324
324
|
width?: GridWidth;
|
|
325
|
+
/**
|
|
326
|
+
* Ancho en la grilla en mobile (1–12). Por defecto `12` (ancho completo,
|
|
327
|
+
* apilado). Declararlo permite controlar el layout responsive en pantallas
|
|
328
|
+
* chicas sin tocar el ancho de escritorio (`width`).
|
|
329
|
+
*/
|
|
330
|
+
mobileWidth?: GridWidth;
|
|
325
331
|
/**
|
|
326
332
|
* De dónde saca sus datos (F8 · R6.5).
|
|
327
333
|
*
|
|
@@ -516,6 +522,79 @@ export interface ResourceEndpoints {
|
|
|
516
522
|
create?: string;
|
|
517
523
|
update?: string;
|
|
518
524
|
remove?: string;
|
|
525
|
+
/**
|
|
526
|
+
* Declaración opcional de cómo unwrappear la respuesta de esta operación.
|
|
527
|
+
* Cuando se declara, el motor extrae el contenido de `dataKey` antes de
|
|
528
|
+
* devolverlo al consumidor (ej.: `{ data: [...] }` → `[...]`).
|
|
529
|
+
* Si no se declara, el motor mantiene el comportamiento actual:
|
|
530
|
+
* respuesta pasada tal cual (array plano, `{ items }`, o objeto crudo).
|
|
531
|
+
* Esta declaración es por operación; si se desea un valor global, usarlo
|
|
532
|
+
* en cada operación o considerar un default a nivel de config en futuro.
|
|
533
|
+
*/
|
|
534
|
+
responseUnwrap?: ResponseUnwrap;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Cómo el motor debe desempaquetar la respuesta HTTP antes de entregarla.
|
|
538
|
+
* Opcional y opt-in: schemas sin este campo siguen el contrato actual.
|
|
539
|
+
*
|
|
540
|
+
* - `dataKey`: clave en el body que contiene el payload real (ej. `data`,
|
|
541
|
+
* `items`, `result`). Si el body no tiene esta clave, se devuelve `undefined`
|
|
542
|
+
* para lista / objeto vacío según corresponda, y se lanza error en modo
|
|
543
|
+
* estricto si se configura.
|
|
544
|
+
* - `metaKey`: clave con metadatos (paginación, total, etc.). Se ignora en
|
|
545
|
+
* el valor devuelto al consumidor pero puede leerse si el motor la expone
|
|
546
|
+
* en futuro.
|
|
547
|
+
* - `apply`: `'list' | 'get' | 'create' | 'update' | 'remove' | 'all'`.
|
|
548
|
+
* Qué operaciones aplican el unwrap. Por defecto `'all'` cuando se declara.
|
|
549
|
+
*/
|
|
550
|
+
export interface ResponseUnwrap {
|
|
551
|
+
dataKey: string;
|
|
552
|
+
metaKey?: string;
|
|
553
|
+
apply?: 'list' | 'get' | 'create' | 'update' | 'remove' | 'all';
|
|
554
|
+
/**
|
|
555
|
+
* Claves de metadata de paginación dentro del sobre. Usadas por
|
|
556
|
+
* `ApiService.listPage()` para armar `PageResult<T>`. Opcional: sin
|
|
557
|
+
* declaración, el motor intenta auto-detectar claves comunes
|
|
558
|
+
* (`nextCursor`, `total`, `hasMore`) y, si no hay, `hasMore = false`.
|
|
559
|
+
*/
|
|
560
|
+
pagination?: {
|
|
561
|
+
/** Clave del cursor siguiente dentro de `meta` (o del body si no hay `metaKey`). */
|
|
562
|
+
cursorKey?: string;
|
|
563
|
+
/** Clave del total de registros. */
|
|
564
|
+
totalKey?: string;
|
|
565
|
+
/** Clave del flag booleano "hay más páginas". */
|
|
566
|
+
hasMoreKey?: string;
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Método HTTP soportado por endpoints personalizados.
|
|
571
|
+
* Declarado aquí (y no en api.service.ts) para evitar ciclos de importación
|
|
572
|
+
* entre modelos y servicios.
|
|
573
|
+
*/
|
|
574
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
575
|
+
/**
|
|
576
|
+
* Definición de un endpoint personalizado más allá de los cinco estándar
|
|
577
|
+
* (list/get/create/update/remove). Permite al consumidor declarar rutas con
|
|
578
|
+
* verbos arbitrarios, segmentos extra, payloads JSON o multipart, y unwrap de
|
|
579
|
+
* respuesta propio.
|
|
580
|
+
*
|
|
581
|
+
* Ejemplo:
|
|
582
|
+
* name: 'recalculate-ranks'
|
|
583
|
+
* method: 'POST'
|
|
584
|
+
* path: '/admin/businesses/{businessId}/ranks/recalculate'
|
|
585
|
+
* payloadMode: 'json'
|
|
586
|
+
*/
|
|
587
|
+
export interface CustomEndpointDefinition {
|
|
588
|
+
/** Nombre único del endpoint dentro del recurso. Se usa para invocarlo. */
|
|
589
|
+
name: string;
|
|
590
|
+
/** Método HTTP de la petición. */
|
|
591
|
+
method: HttpMethod;
|
|
592
|
+
/** Ruta relativa a `API_URL`. Puede incluir marcadores {key}, {auth.user.id}, etc. */
|
|
593
|
+
path: string;
|
|
594
|
+
/** Modo de payload: 'json' (default) o 'multipart' para FormData. */
|
|
595
|
+
payloadMode?: 'json' | 'multipart';
|
|
596
|
+
/** Unwrap de respuesta override para este endpoint. Si no se declara, se usa el del recurso. */
|
|
597
|
+
responseUnwrap?: ResponseUnwrap;
|
|
519
598
|
}
|
|
520
599
|
export interface ResourceSchema {
|
|
521
600
|
/** Identificador interno (`categories`). */
|
|
@@ -533,6 +612,8 @@ export interface ResourceSchema {
|
|
|
533
612
|
keyField: string;
|
|
534
613
|
/** Campo que se muestra como nombre del registro. */
|
|
535
614
|
titleField: string;
|
|
615
|
+
/** Endpoints personalizados más allá de list/get/create/update/remove. */
|
|
616
|
+
customEndpoints?: Record<string, CustomEndpointDefinition>;
|
|
536
617
|
/** Si el listado se reordena (requiere `positionField`). */
|
|
537
618
|
sortable?: boolean;
|
|
538
619
|
positionField?: string;
|
|
@@ -895,6 +976,14 @@ export declare const API_DRIVER: InjectionToken<"gas" | "rest">;
|
|
|
895
976
|
*/
|
|
896
977
|
type ValueSources = Readonly<Record<string, () => unknown>>;
|
|
897
978
|
export declare const VALUE_SOURCES: InjectionToken<Readonly<Record<string, () => unknown>>>;
|
|
979
|
+
/**
|
|
980
|
+
* Unwrap de respuesta global por defecto (`GasAdminConfig.defaultResponseUnwrap`).
|
|
981
|
+
*
|
|
982
|
+
* Se fusiona con la declaración `endpoint.responseUnwrap` de cada recurso:
|
|
983
|
+
* la del recurso tiene prioridad en las claves que declara. Sin token, sólo
|
|
984
|
+
* aplica lo declarado por recurso — cero impacto en schemas existentes.
|
|
985
|
+
*/
|
|
986
|
+
export declare const DEFAULT_RESPONSE_UNWRAP: InjectionToken<ResponseUnwrap>;
|
|
898
987
|
/** Fija el catálogo (uso interno de `provideGasAdmin`; testable directo). */
|
|
899
988
|
export declare function setSchemaCatalog(schemas: readonly ResourceSchema[], auth?: AuthSchema): void;
|
|
900
989
|
/** Busca un schema por `id`; `undefined` si no existe. */
|
|
@@ -930,6 +1019,13 @@ interface GasAdminConfig {
|
|
|
930
1019
|
* ningún path que no lleve marcadores — cero impacto en schemas existentes.
|
|
931
1020
|
*/
|
|
932
1021
|
valueSources?: ValueSources;
|
|
1022
|
+
/**
|
|
1023
|
+
* Unwrap de respuesta global por defecto. Se fusiona con la declaración
|
|
1024
|
+
* `endpoint.responseUnwrap` de cada recurso (la del recurso tiene prioridad
|
|
1025
|
+
* en claves declaradas). Opcional: sin él, sólo aplica lo declarado por
|
|
1026
|
+
* recurso — cero impacto en schemas existentes (R2).
|
|
1027
|
+
*/
|
|
1028
|
+
defaultResponseUnwrap?: ResponseUnwrap;
|
|
933
1029
|
}
|
|
934
1030
|
/**
|
|
935
1031
|
* Wiring completo del motor: catálogo + HTTP con el interceptor de
|
|
@@ -1013,6 +1109,23 @@ export declare class ApiError extends Error {
|
|
|
1013
1109
|
* dos: sin este archivo no hay ciclo de imports.
|
|
1014
1110
|
*/
|
|
1015
1111
|
export declare function renewedTokenOf(body: unknown): string | null;
|
|
1112
|
+
/**
|
|
1113
|
+
* Resultado de una página para `ApiService.listPage()`.
|
|
1114
|
+
*
|
|
1115
|
+
* El backend envía la metadata de paginación dentro del sobre (típicamente
|
|
1116
|
+
* en `meta`); el motor la extrae según la declaración `responseUnwrap.pagination`
|
|
1117
|
+
* y la entrega en este shape. `list()` NO usa este tipo: sigue devolviendo `T[]`.
|
|
1118
|
+
*/
|
|
1119
|
+
export interface PageResult<T> {
|
|
1120
|
+
/** Registros de la página actual. */
|
|
1121
|
+
items: T[];
|
|
1122
|
+
/** Cursor para pedir la página siguiente; `null` cuando no hay más. */
|
|
1123
|
+
nextCursor: string | null;
|
|
1124
|
+
/** Total de registros del conjunto, si el backend lo informa; `0` si no. */
|
|
1125
|
+
total: number;
|
|
1126
|
+
/** `true` si hay al menos una página más. `false` sin cursor informativo. */
|
|
1127
|
+
hasMore: boolean;
|
|
1128
|
+
}
|
|
1016
1129
|
/**
|
|
1017
1130
|
* Guards de sesión (auth.md §5.3).
|
|
1018
1131
|
*
|
|
@@ -1121,6 +1234,40 @@ export declare class NotificationService {
|
|
|
1121
1234
|
}
|
|
1122
1235
|
/** Métodos que entiende el envelope del backend (baseapi §2.3). */
|
|
1123
1236
|
type EnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
1237
|
+
/** Dependencias del transporte, aportadas por la fachada (no hay singleton acá). */
|
|
1238
|
+
interface TransportDeps {
|
|
1239
|
+
http: HttpClient;
|
|
1240
|
+
/** URL base (`API_URL`): el Web App de Apps Script o el origen REST. */
|
|
1241
|
+
apiUrl: string;
|
|
1242
|
+
/** Getter del token de SESIÓN, leído en CADA petición (R2.8). */
|
|
1243
|
+
sessionToken: () => string | null;
|
|
1244
|
+
/**
|
|
1245
|
+
* Refresco opcional de ID token (Fase Firebase): si existe, se llama antes
|
|
1246
|
+
* de armar la cabecera Authorization. Un rechazo viaja por el canal de
|
|
1247
|
+
* errores — nunca se manda una petición con token inválido.
|
|
1248
|
+
*/
|
|
1249
|
+
getIdToken?: () => Promise<string>;
|
|
1250
|
+
}
|
|
1251
|
+
/** `POST text/plain` a `apiUrl` con el envelope GAS. */
|
|
1252
|
+
declare function sendGas<T>(deps: TransportDeps, path: string, method: EnvelopeMethod, payload?: Record<string, unknown> | FormData, multipart?: boolean): Observable<T>;
|
|
1253
|
+
/**
|
|
1254
|
+
* Verbo HTTP real con `Authorization: Bearer`. Si el adaptador provee
|
|
1255
|
+
* `getIdToken()`, se resuelve el token fresco ANTES de la cabecera; si no,
|
|
1256
|
+
* se usa el de sesión almacenado (fallback). Cualquier error de `getIdToken()`
|
|
1257
|
+
* se propaga por el canal de errores.
|
|
1258
|
+
*/
|
|
1259
|
+
declare function sendRest<T>(deps: TransportDeps, path: string, method: EnvelopeMethod, payload?: Record<string, unknown> | FormData, multipart?: boolean): Observable<T>;
|
|
1260
|
+
/** Despacho REST concreto sobre una URL ya construida. */
|
|
1261
|
+
declare function doRestRequest<T>(deps: TransportDeps, url: string, method: EnvelopeMethod, payload: Record<string, unknown> | FormData | undefined, headers?: HttpHeaders, multipart?: boolean): Observable<T>;
|
|
1262
|
+
/** `${apiUrl}${path}` normalizada: sin dobles slashes ni path sin slash inicial. */
|
|
1263
|
+
declare function buildUrl(apiUrl: string, path: string): string;
|
|
1264
|
+
/**
|
|
1265
|
+
* Convierte un payload en HttpParams para peticiones GET y DELETE en modo REST.
|
|
1266
|
+
* Si un valor es un objeto plano (ej: `{ view: 'calendar', params: { orden: 'es' } }`),
|
|
1267
|
+
* lo aplana un nivel para que sus claves viajen como query params reales
|
|
1268
|
+
* en lugar de serializarse como `params=[object Object]`.
|
|
1269
|
+
*/
|
|
1270
|
+
declare function toHttpParams(payload?: Record<string, unknown>): HttpParams;
|
|
1124
1271
|
/**
|
|
1125
1272
|
* CRUD genérico sobre el contrato v2 (baseapi §2.3) con soporte dual-driver:
|
|
1126
1273
|
*
|
|
@@ -1150,6 +1297,14 @@ export declare class ApiService {
|
|
|
1150
1297
|
* parcial), devuelve `null` y el resolver falla ruidoso con «sin valor».
|
|
1151
1298
|
*/
|
|
1152
1299
|
private readonly injector;
|
|
1300
|
+
/** Unwrap global por defecto (`GasAdminConfig.defaultResponseUnwrap`); opcional. */
|
|
1301
|
+
private readonly defaultUnwrap;
|
|
1302
|
+
/**
|
|
1303
|
+
* Declaración de unwrap EFECTIVA para un endpoint: fusión del default
|
|
1304
|
+
* global con la declaración por recurso (recurso gana en las claves que
|
|
1305
|
+
* declara). Sin ninguna de las dos ⇒ `undefined` (comportamiento actual).
|
|
1306
|
+
*/
|
|
1307
|
+
private effectiveUnwrap;
|
|
1153
1308
|
/**
|
|
1154
1309
|
* Listado. El backend envuelve la respuesta cuando la operación declara
|
|
1155
1310
|
* `shape.envelope: 'list'` (F5) ⇒ `{ items, nextCursor, total }` (R6.4);
|
|
@@ -1165,6 +1320,21 @@ export declare class ApiService {
|
|
|
1165
1320
|
* llamada es la de siempre — la lectura llana del recurso.
|
|
1166
1321
|
*/
|
|
1167
1322
|
list<T>(endpoint: ResourceEndpoints, payload?: Record<string, unknown>): Observable<T[]>;
|
|
1323
|
+
/**
|
|
1324
|
+
* Listado paginado OPT-IN (plan doc/engine-refactor-plan.md · Fase 3).
|
|
1325
|
+
*
|
|
1326
|
+
* Misma petición que `list()` pero devuelve `PageResult<T>` con la metadata
|
|
1327
|
+
* de paginación que el backend envía en el sobre (`{ items, nextCursor,
|
|
1328
|
+
* total }` o dentro de `meta` con unwrap declarado). Las claves se leen
|
|
1329
|
+
* según `responseUnwrap.pagination` (cursorKey/totalKey/hasMoreKey) o se
|
|
1330
|
+
* auto-detectan (`nextCursor`, `total`, `hasMore`).
|
|
1331
|
+
*
|
|
1332
|
+
* `payload` acepta normalmente `{ cursor, limit }` para pedir la página
|
|
1333
|
+
* siguiente; viaja como cualquier otro payload (query params en GET REST,
|
|
1334
|
+
* campo `payload` del envelope en GAS). `list()` no cambia: sigue
|
|
1335
|
+
* devolviendo `T[]` (R11).
|
|
1336
|
+
*/
|
|
1337
|
+
listPage<T>(endpoint: ResourceEndpoints, payload?: Record<string, unknown>): Observable<PageResult<T>>;
|
|
1168
1338
|
/** `key` solo aplica a colecciones (`{key}` en la plantilla); los singleton no la llevan. */
|
|
1169
1339
|
get<T>(endpoint: ResourceEndpoints, key?: string): Observable<T>;
|
|
1170
1340
|
create<T>(endpoint: ResourceEndpoints, payload: Record<string, unknown>): Observable<T>;
|
|
@@ -1176,7 +1346,26 @@ export declare class ApiService {
|
|
|
1176
1346
|
* `/admin/upload-signature`, etc.
|
|
1177
1347
|
*/
|
|
1178
1348
|
request<T>(method: EnvelopeMethod, path: string | undefined, payload?: Record<string, unknown>): Observable<T>;
|
|
1349
|
+
/**
|
|
1350
|
+
* Invoca un endpoint personalizado declarado por el consumidor.
|
|
1351
|
+
*
|
|
1352
|
+
* Acepta bien la definición directa (`CustomEndpointDefinition`) o un schema
|
|
1353
|
+
* + nombre (resuelve en `schema.customEndpoints`). El despacho respeta el
|
|
1354
|
+
* método, la ruta, el modo de payload (json/multipart) y el unwrap propios
|
|
1355
|
+
* de la declaración.
|
|
1356
|
+
*/
|
|
1357
|
+
invokeCustomEndpoint<T>(definitionOrSchema: CustomEndpointDefinition | ResourceSchema, nameOrPayload?: string | Record<string, unknown> | FormData, payload?: Record<string, unknown> | FormData, key?: string): Observable<T>;
|
|
1358
|
+
/** Alias corto de `invokeCustomEndpoint`. */
|
|
1359
|
+
invokeCustom<T>(schemaOrDefinition: ResourceSchema | CustomEndpointDefinition, nameOrPayload?: string | Record<string, unknown> | FormData, payload?: Record<string, unknown> | FormData, key?: string): Observable<T>;
|
|
1360
|
+
/** Alias adicional por compatibilidad. */
|
|
1361
|
+
custom<T>(schemaOrDefinition: ResourceSchema | CustomEndpointDefinition, nameOrPayload?: string | Record<string, unknown> | FormData, payload?: Record<string, unknown> | FormData, key?: string): Observable<T>;
|
|
1179
1362
|
private send;
|
|
1363
|
+
/**
|
|
1364
|
+
* Dependencias del transporte (Fase 4): la fachada sólo arma este objeto y
|
|
1365
|
+
* delega en `transport.ts`. El token de sesión y el ID token fresco se
|
|
1366
|
+
* resuelven vía getters en el momento de la petición (R2.8 / refresh).
|
|
1367
|
+
*/
|
|
1368
|
+
private transportDeps;
|
|
1180
1369
|
/**
|
|
1181
1370
|
* Fuentes de contexto de ESTA petición: getters, no valores — se evalúan
|
|
1182
1371
|
* sólo si el path trae marcadores.
|
|
@@ -1185,10 +1374,6 @@ export declare class ApiService {
|
|
|
1185
1374
|
* dueño de sus fuentes y el registro del consumidor no las pisa.
|
|
1186
1375
|
*/
|
|
1187
1376
|
private buildSources;
|
|
1188
|
-
private sendGas;
|
|
1189
|
-
private sendRest;
|
|
1190
|
-
private doRestRequest;
|
|
1191
|
-
private buildUrl;
|
|
1192
1377
|
private withKey;
|
|
1193
1378
|
static ɵfac: i0.ɵɵFactoryDeclaration<ApiService, never>;
|
|
1194
1379
|
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
@@ -1286,10 +1471,6 @@ export declare class AuthService {
|
|
|
1286
1471
|
* que deja una sesión muerta en el servidor.
|
|
1287
1472
|
*/
|
|
1288
1473
|
logout(): Observable<void>;
|
|
1289
|
-
/** Revoca las sesiones del usuario en todos sus dispositivos. */
|
|
1290
|
-
logoutAll(): Observable<{
|
|
1291
|
-
effectiveWithinSeconds: number;
|
|
1292
|
-
}>;
|
|
1293
1474
|
/** Revalida la sesión restaurada contra `/auth/me`; sin token ⇒ sin viaje. */
|
|
1294
1475
|
restore(): Observable<void>;
|
|
1295
1476
|
/**
|
|
@@ -1536,7 +1717,6 @@ export declare class Sidebar {
|
|
|
1536
1717
|
private readonly auth;
|
|
1537
1718
|
private readonly notifications;
|
|
1538
1719
|
readonly isAuthenticated: import("@angular/core").Signal<boolean>;
|
|
1539
|
-
readonly closingAllSessions: import("@angular/core").WritableSignal<boolean>;
|
|
1540
1720
|
readonly loggingOut: import("@angular/core").WritableSignal<boolean>;
|
|
1541
1721
|
/**
|
|
1542
1722
|
* R2.10 — el menú se corta dos veces: `isVisible` (permisos declarados
|
|
@@ -1544,8 +1724,6 @@ export declare class Sidebar {
|
|
|
1544
1724
|
* Sólo UX: filtrar acá no protege nada, el back re-verifica en R2.6.
|
|
1545
1725
|
*/
|
|
1546
1726
|
readonly menuItems: import("@angular/core").Signal<MenuEntry[]>;
|
|
1547
|
-
closeAllSessions(): Promise<void>;
|
|
1548
|
-
/** Cierra la sesión individual del usuario (sesión + Firebase) y va al login. */
|
|
1549
1727
|
closeSession(): Promise<void>;
|
|
1550
1728
|
static ɵfac: i0.ɵɵFactoryDeclaration<Sidebar, never>;
|
|
1551
1729
|
static ɵcmp: i0.ɵɵComponentDeclaration<Sidebar, "app-sidebar", never, {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/views/calendar-view/calendar-model.ts","../../../projects/gas-admin-ui/src/lib/views/calendar-view/calendar-view.ts"],"sourcesContent":["import type { CalendarViewConfig, FieldSchema } from '../../core/models/schema.model';\nimport type { SourceRow } from '../../core/services/data-source.service';\n\n/**\n * Pure date + event-mapping model for the generic calendar view (R9.8.3).\n *\n * Date model (decided): backend `date` fields are ISO civil dates\n * `YYYY-MM-DD` treated as CALENDAR DAYS — never shifted through the browser\n * timezone. Every parse goes through `Date.UTC` on the literal parts, never\n * `new Date('YYYY-MM-DD')` in local time (that lands on the previous day\n * west of Greenwich).\n */\n\n/** Milliseconds in one civil day — every day-step of the model uses this. */\nexport const DAY_MS = 86_400_000;\n\n/** Visible modes of the calendar. */\nexport type CalendarMode = 'month' | 'week' | 'day' | 'agenda';\n\n/** Inclusive pair of civil days (`YYYY-MM-DD`) that the view paints. */\nexport interface CivilRange {\n desde: string;\n hasta: string;\n}\n\n/** One event painted from a source row. */\nexport interface CalEvent {\n key: string;\n title: string;\n /** True first day (`YYYY-MM-DD`), before any clamp to the visible range. */\n startDay: string;\n /** True last day, inclusive. Equals `startDay` for single-day events. */\n endDay: string;\n color?: string;\n row: SourceRow;\n}\n\n/** A row excluded from the grid because its interval is inconsistent. */\nexport interface CalendarProblem {\n row: SourceRow;\n reason: string;\n}\n\n/** Result of mapping raw rows onto calendar events (R9.8.3). */\nexport interface CalendarMapping {\n events: CalEvent[];\n /** Rows whose `startField` is empty or not a valid civil date. */\n undated: SourceRow[];\n /** Rows whose `endField` makes the interval impossible. */\n invalid: CalendarProblem[];\n}\n\n/** Spanish reasons shown next to problem records (UI copy). */\nexport const REASON_UNDATED = 'Sin fecha de inicio válida';\nexport const REASON_INVALID_END = 'Fecha de fin inválida';\nexport const REASON_END_BEFORE_START = 'El fin es anterior al inicio';\n\nconst MONTHS = [\n 'enero',\n 'febrero',\n 'marzo',\n 'abril',\n 'mayo',\n 'junio',\n 'julio',\n 'agosto',\n 'septiembre',\n 'octubre',\n 'noviembre',\n 'diciembre',\n];\n\nconst MONTHS_SHORT = [\n 'ene',\n 'feb',\n 'mar',\n 'abr',\n 'may',\n 'jun',\n 'jul',\n 'ago',\n 'sep',\n 'oct',\n 'nov',\n 'dic',\n];\n\n/** Weekday names Monday-first (the calendar's week start, decided). */\nconst WEEKDAYS_SHORT = ['lun', 'mar', 'mié', 'jue', 'vie', 'sáb', 'dom'];\nconst WEEKDAYS = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'];\n\n/** Weekday headings of the Monday-first grid, in display order. */\nexport const WEEKDAY_HEADINGS: readonly string[] = WEEKDAYS_SHORT;\n\nconst DAY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/**\n * `YYYY-MM-DD` → UTC milliseconds at midnight of that civil day.\n *\n * Strict: the string must be exactly three zero-padded parts and name a real\n * calendar day (`2026-02-30` is rejected, not rolled into March). Empty,\n * malformed or non-string input → `null` (the caller decides whether that\n * means \"undated\" or \"invalid\").\n */\nexport function parseDay(value: unknown): number | null {\n if (typeof value !== 'string') return null;\n const match = DAY_PATTERN.exec(value.trim());\n if (match === null) return null;\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n const ms = Date.UTC(year, month - 1, day);\n const back = new Date(ms);\n if (\n back.getUTCFullYear() !== year ||\n back.getUTCMonth() !== month - 1 ||\n back.getUTCDate() !== day\n ) {\n return null;\n }\n return ms;\n}\n\n/** UTC milliseconds at midnight → `YYYY-MM-DD` (inverse of `parseDay`). */\nexport function formatDay(ms: number): string {\n const date = new Date(ms);\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n return `${date.getUTCFullYear()}-${month}-${day}`;\n}\n\n/**\n * Today as a civil day, in the USER's local calendar date — the local parts\n * are read off `new Date()` and formatted manually, so no timezone shift can\n * sneak in through a round-trip.\n */\nexport function todayCivil(): string {\n const now = new Date();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n return `${now.getFullYear()}-${month}-${day}`;\n}\n\n/** Day-step on civil days (safe: every `ms` is a UTC midnight). */\nexport function addDays(ms: number, days: number): number {\n return ms + days * DAY_MS;\n}\n\n/** 0 = Monday … 6 = Sunday (the calendar's Monday-first convention). */\nexport function dayOfWeekMon0(ms: number): number {\n return (new Date(ms).getUTCDay() + 6) % 7;\n}\n\n/**\n * Move a civil day by whole months, clamping the day-of-month to the target\n * month's length (Jan 31 + 1 month → Feb 28).\n */\nexport function shiftMonths(ms: number, months: number): number {\n const date = new Date(ms);\n const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));\n const lastDay = new Date(\n Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0),\n ).getUTCDate();\n return Date.UTC(\n target.getUTCFullYear(),\n target.getUTCMonth(),\n Math.min(date.getUTCDate(), lastDay),\n );\n}\n\n/** Monday of the 6-week (42-day) grid that shows the cursor's month. */\nfunction monthGridStartMs(cursorMs: number): number {\n const date = new Date(cursorMs);\n const firstOfMonth = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);\n return addDays(firstOfMonth, -dayOfWeekMon0(firstOfMonth));\n}\n\n/**\n * The 42 civil days of the month grid (6 Monday-first weeks), so the grid\n * always has the same shape and the de-emphasized edge days are complete.\n */\nexport function monthGridDays(cursorDay: string): string[] {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return [];\n const start = monthGridStartMs(cursor);\n return Array.from({ length: 42 }, (_, index) => formatDay(addDays(start, index)));\n}\n\n/** The 7 civil days of the Monday-first week containing the cursor. */\nexport function weekDays(cursorDay: string): string[] {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return [];\n const start = addDays(cursor, -dayOfWeekMon0(cursor));\n return Array.from({ length: 7 }, (_, index) => formatDay(addDays(start, index)));\n}\n\n/** Every civil day of an inclusive range (empty when the range is invalid). */\nexport function daysInRange(range: CivilRange): string[] {\n const from = parseDay(range.desde);\n const to = parseDay(range.hasta);\n if (from === null || to === null || to < from) return [];\n const days: string[] = [];\n for (let ms = from; ms <= to; ms = addDays(ms, 1)) {\n days.push(formatDay(ms));\n }\n return days;\n}\n\n/**\n * Visible `{desde, hasta}` for a mode + cursor.\n *\n * - `month`: the 6-week grid range (superset of the month) — with the\n * backend-range optimization the fetch already covers the edge weeks.\n * - `week`: the Monday-first week of the cursor.\n * - `day`: the cursor itself.\n * - `agenda`: a fixed 30-day window from the cursor.\n */\nexport function visibleRangeFor(mode: CalendarMode, cursorDay: string): CivilRange {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return { desde: '', hasta: '' };\n switch (mode) {\n case 'month': {\n const start = monthGridStartMs(cursor);\n return { desde: formatDay(start), hasta: formatDay(addDays(start, 41)) };\n }\n case 'week': {\n const start = addDays(cursor, -dayOfWeekMon0(cursor));\n return { desde: formatDay(start), hasta: formatDay(addDays(start, 6)) };\n }\n case 'day':\n return { desde: formatDay(cursor), hasta: formatDay(cursor) };\n case 'agenda':\n return { desde: formatDay(cursor), hasta: formatDay(addDays(cursor, 29)) };\n }\n}\n\n/** Move the cursor one step of the current mode (agenda steps 30 days). */\nexport function shiftCursor(mode: CalendarMode, cursorDay: string, delta: 1 | -1): string {\n const cursor = parseDay(cursorDay) ?? parseDay(todayCivil()) ?? Date.UTC(1970, 0, 1);\n switch (mode) {\n case 'month':\n return formatDay(shiftMonths(cursor, delta));\n case 'week':\n return formatDay(addDays(cursor, 7 * delta));\n case 'day':\n return formatDay(addDays(cursor, delta));\n case 'agenda':\n return formatDay(addDays(cursor, 30 * delta));\n }\n}\n\n/** True when both civil days fall in the same year+month. */\nexport function sameMonth(a: string, b: string): boolean {\n const msA = parseDay(a);\n const msB = parseDay(b);\n if (msA === null || msB === null) return false;\n const dateA = new Date(msA);\n const dateB = new Date(msB);\n return (\n dateA.getUTCFullYear() === dateB.getUTCFullYear() && dateA.getUTCMonth() === dateB.getUTCMonth()\n );\n}\n\n/** Depth-first lookup of a field by key through `group`/`list` nesting. */\nexport function findFieldByKey(\n fields: readonly FieldSchema[],\n key: string,\n): FieldSchema | undefined {\n for (const field of fields) {\n if (field.key === key) return field;\n if (field.type === 'group') {\n const hit = findFieldByKey(field.fields, key);\n if (hit !== undefined) return hit;\n } else if (field.type === 'list') {\n const hit = findFieldByKey(field.itemFields, key);\n if (hit !== undefined) return hit;\n }\n }\n return undefined;\n}\n\n/**\n * Pure row → event mapping (R9.8.3): the single place that decides what is a\n * paintable event, what is undated and what is an impossible interval.\n *\n * - Empty/invalid `startField` → `undated` (surfaced, never hidden).\n * - `endField` of type `time` → same civil day as the start (time-of-day\n * within the start day; it never spans days).\n * - Empty `endField` (date) → single-day event on the start.\n * - `end < start` → `invalid` with a visible reason; not painted at all.\n */\nexport function mapCalendarRows(\n rows: readonly SourceRow[],\n config: CalendarViewConfig,\n options: { keyField: string; endFieldType?: string },\n): CalendarMapping {\n const events: CalEvent[] = [];\n const undated: SourceRow[] = [];\n const invalid: CalendarProblem[] = [];\n\n for (const row of rows) {\n const startMs = parseDay(row[config.startField]);\n if (startMs === null) {\n undated.push(row);\n continue;\n }\n let endMs = startMs;\n if (config.endField !== undefined && options.endFieldType !== 'time') {\n const rawEnd = row[config.endField];\n const empty = rawEnd === null || rawEnd === undefined || rawEnd === '';\n if (!empty) {\n const parsedEnd = parseDay(rawEnd);\n if (parsedEnd === null) {\n invalid.push({ row, reason: REASON_INVALID_END });\n continue;\n }\n if (parsedEnd < startMs) {\n invalid.push({ row, reason: REASON_END_BEFORE_START });\n continue;\n }\n endMs = parsedEnd;\n }\n }\n const key = String(row[options.keyField] ?? '');\n const title = String(row[config.titleField] ?? '').trim() || key;\n const rawColor = config.colorField !== undefined ? row[config.colorField] : undefined;\n const color = typeof rawColor === 'string' && rawColor !== '' ? rawColor : undefined;\n events.push({ key, title, startDay: formatDay(startMs), endDay: formatDay(endMs), color, row });\n }\n\n return { events, undated, invalid };\n}\n\n/**\n * Bucket events per civil day inside the visible range.\n *\n * A multi-day event appears under EVERY day it touches; the painted span is\n * clamped to the range (the event keeps its true `startDay`/`endDay` — only\n * the buckets are limited), which also keeps long intervals cheap.\n */\nexport function eventsByDay(\n events: readonly CalEvent[],\n range: CivilRange,\n): Map<string, CalEvent[]> {\n const byDay = new Map<string, CalEvent[]>();\n const from = parseDay(range.desde);\n const to = parseDay(range.hasta);\n if (from === null || to === null || to < from) return byDay;\n for (const event of events) {\n const start = parseDay(event.startDay);\n const end = parseDay(event.endDay);\n if (start === null || end === null) continue;\n const paintFrom = Math.max(start, from);\n const paintTo = Math.min(end, to);\n for (let ms = paintFrom; ms <= paintTo; ms = addDays(ms, 1)) {\n const day = formatDay(ms);\n const bucket = byDay.get(day);\n if (bucket === undefined) byDay.set(day, [event]);\n else bucket.push(event);\n }\n }\n return byDay;\n}\n\n/** \"12 de octubre de 2026\" — civil parts, UTC-based (no TZ shift). */\nexport function dayLabel(day: string): string {\n const ms = parseDay(day);\n if (ms === null) return day;\n const date = new Date(ms);\n return `${date.getUTCDate()} de ${MONTHS[date.getUTCMonth()]} de ${date.getUTCFullYear()}`;\n}\n\n/** Short weekday of a civil day, Monday-first (\"lunes\" → \"lun\"). */\nexport function weekdayShort(day: string): string {\n const ms = parseDay(day);\n if (ms === null) return '';\n return WEEKDAYS_SHORT[dayOfWeekMon0(ms)];\n}\n\n/** Full weekday of a civil day (\"lunes\"). */\nexport function weekdayFull(day: string): string {\n const ms = parseDay(day);\n if (ms === null) return '';\n return WEEKDAYS[dayOfWeekMon0(ms)];\n}\n\n/** \"lunes, 12 de octubre de 2026\" — agenda/day heading. */\nexport function agendaDayLabel(day: string): string {\n const weekday = weekdayFull(day);\n return weekday !== '' ? `${weekday}, ${dayLabel(day)}` : dayLabel(day);\n}\n\n/**\n * Header label for the visible range, built from civil parts with a fixed\n * Spanish vocabulary (deterministic regardless of the runtime locale).\n */\nexport function rangeLabelFor(mode: CalendarMode, cursorDay: string): string {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return '';\n const date = new Date(cursor);\n if (mode === 'month') {\n return `${MONTHS[date.getUTCMonth()]} de ${date.getUTCFullYear()}`;\n }\n const range = visibleRangeFor(mode, cursorDay);\n const desde = parseDay(range.desde);\n const hasta = parseDay(range.hasta);\n if (desde === null || hasta === null) return '';\n const startDate = new Date(desde);\n const endDate = new Date(hasta);\n const sameYear = startDate.getUTCFullYear() === endDate.getUTCFullYear();\n const left = `${startDate.getUTCDate()} ${MONTHS_SHORT[startDate.getUTCMonth()]}${\n sameYear ? '' : ` ${startDate.getUTCFullYear()}`\n }`;\n return `${left} – ${endDate.getUTCDate()} ${MONTHS_SHORT[endDate.getUTCMonth()]} ${endDate.getUTCFullYear()}`;\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute } from '@angular/router';\nimport { LucideChevronLeft, LucideChevronRight } from '@lucide/angular';\n\nimport type {\n CalendarViewConfig,\n ResourceSchema,\n UiViewCalendar,\n SelectionBinding,\n} from '../../core/models/schema.model';\nimport { ErrorMapperService } from '../../core/services/error-mapper.service';\nimport { DataSourceService, type SourceRow } from '../../core/services/data-source.service';\nimport type { SelectionEmitter } from '../composer/view-context';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { Button } from '../../shared/components/button/button';\nimport { EmptyState } from '../../shared/components/empty-state/empty-state';\nimport { Skeleton } from '../../shared/components/skeleton/skeleton';\nimport {\n agendaDayLabel,\n type CalendarMapping,\n type CalendarMode,\n type CalEvent,\n type CivilRange,\n daysInRange,\n eventsByDay,\n findFieldByKey,\n mapCalendarRows,\n monthGridDays,\n rangeLabelFor,\n REASON_END_BEFORE_START,\n REASON_UNDATED,\n sameMonth,\n shiftCursor,\n todayCivil,\n visibleRangeFor,\n weekDays,\n WEEKDAY_HEADINGS,\n weekdayShort,\n} from './calendar-model';\n\n/**\n * Narrowing helper: `UiViewBase` keeps `type: string` open, so comparing\n * `view.type === 'calendar'` alone does not remove the base member from the\n * union. Requiring `config` in the value is what actually proves the view is\n * a `UiViewCalendar` at runtime.\n */\nfunction calendarConfigOf(view: unknown): UiViewCalendar['config'] | undefined {\n if (!view || typeof view !== 'object') return undefined;\n const candidate = view as { type?: unknown; config?: unknown };\n return candidate.type === 'calendar' && candidate.config !== undefined\n ? (candidate.config as UiViewCalendar['config'])\n : undefined;\n}\n\ntype Status = 'loading' | 'ready' | 'error';\n\n/** Mode switcher, in display order (Spanish UI copy). */\nconst MODE_OPTIONS: readonly { mode: CalendarMode; label: string }[] = [\n { mode: 'month', label: 'Mes' },\n { mode: 'week', label: 'Semana' },\n { mode: 'day', label: 'Día' },\n { mode: 'agenda', label: 'Agenda' },\n];\n\n/** Stable empty bucket — avoids allocating a new array per day per cycle. */\nconst NO_EVENTS: readonly CalEvent[] = [];\n\n/**\n * Generic calendar view (R9.8.3): month / week / day / agenda over the rows\n * of the declared `CalendarViewConfig.source` (or the resource's own\n * listing), positioned client-side onto civil days.\n *\n * Data flow: ONE fetcher (`DataSourceService.rows`) per visible range. When\n * the schema declares `source.view` the visible `desde`/`hasta` travel as\n * `ctx` so the backend view can filter the range; without a view nothing\n * filters, the full listing is cached (30 s TTL) and reused while the user\n * navigates — both paths share this component, only the `ctx` differs.\n *\n * Read-only by design: no drag, no click-to-edit. Color never carries\n * meaning alone — every event pairs its color dot with the title text — and\n * the agenda mode is the no-grid fallback for small screens.\n */\n@Component({\n selector: 'app-calendar-view',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [Button, EmptyState, LucideChevronLeft, LucideChevronRight, Skeleton],\n template: `\n @if (config(); as cfg) {\n <section class=\"space-y-4\" aria-label=\"Calendario\" [attr.aria-busy]=\"status() === 'loading'\">\n <header class=\"flex flex-wrap items-center justify-between gap-3\">\n <h2 class=\"font-display text-xl font-semibold\">{{ rangeLabel() }}</h2>\n <div class=\"flex items-center gap-1\">\n <button\n app-button\n variant=\"outline\"\n size=\"icon\"\n type=\"button\"\n aria-label=\"Período anterior\"\n title=\"Período anterior\"\n (click)=\"previous()\"\n >\n <svg lucideChevronLeft size=\"16\" aria-hidden=\"true\" />\n </button>\n <button app-button variant=\"outline\" size=\"sm\" type=\"button\" (click)=\"today()\">\n Hoy\n </button>\n <button\n app-button\n variant=\"outline\"\n size=\"icon\"\n type=\"button\"\n aria-label=\"Período siguiente\"\n title=\"Período siguiente\"\n (click)=\"next()\"\n >\n <svg lucideChevronRight size=\"16\" aria-hidden=\"true\" />\n </button>\n </div>\n <div class=\"flex items-center gap-1\" role=\"group\" aria-label=\"Vista del calendario\">\n @for (option of modeOptions; track option.mode) {\n <button\n app-button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n [attr.aria-pressed]=\"mode() === option.mode\"\n (click)=\"setMode(option.mode)\"\n >\n {{ option.label }}\n </button>\n }\n </div>\n </header>\n\n @switch (status()) {\n @case ('loading') {\n <app-skeleton [lines]=\"8\" [height]=\"32\" />\n }\n @case ('error') {\n <app-empty-state title=\"No se pudo cargar el calendario\" [message]=\"errorMessage()\">\n <button app-button variant=\"outline\" type=\"button\" (click)=\"retry()\">\n Reintentar\n </button>\n </app-empty-state>\n }\n @case ('ready') {\n @if (mapping().events.length === 0) {\n <app-empty-state\n title=\"Sin eventos\"\n message=\"No hay eventos con fecha válida en este período.\"\n />\n } @else {\n @switch (mode()) {\n @case ('month') {\n <div class=\"overflow-x-auto rounded-xl border border-neutral/20 bg-surface\">\n <div class=\"grid min-w-[640px] grid-cols-7\">\n @for (heading of weekdayHeadings; track heading) {\n <div\n class=\"border-b border-neutral/15 px-2 py-1.5 text-xs font-medium uppercase text-neutral\"\n >\n {{ heading }}\n </div>\n }\n @for (day of monthGrid(); track day) {\n <div\n class=\"min-h-24 border-b border-r border-neutral/10 p-1.5\"\n [attr.data-day]=\"day\"\n [class.opacity-40]=\"!isInCursorMonth(day)\"\n >\n <span class=\"text-xs font-medium\">{{ dayNumber(day) }}</span>\n <ul class=\"mt-1 space-y-0.5\">\n @for (event of eventsFor(day); track event.key) {\n <li\n class=\"flex items-center gap-1 text-xs\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"truncate\">{{ event.title }}</span>\n </li>\n }\n </ul>\n </div>\n }\n </div>\n </div>\n }\n @case ('week') {\n <div class=\"overflow-x-auto rounded-xl border border-neutral/20 bg-surface\">\n <div class=\"grid min-w-[560px] grid-cols-7\">\n @for (day of weekColumnDays(); track day) {\n <div class=\"min-h-32 border-r border-neutral/10 p-2\" [attr.data-day]=\"day\">\n <span class=\"text-xs font-medium uppercase text-neutral\">\n {{ weekdayShort(day) }} {{ dayNumber(day) }}\n </span>\n <ul class=\"mt-2 space-y-1\">\n @for (event of eventsFor(day); track event.key) {\n <li\n class=\"flex items-center gap-1.5 text-sm\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"truncate\">{{ event.title }}</span>\n </li>\n }\n </ul>\n </div>\n }\n </div>\n </div>\n }\n @case ('day') {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-4\"\n [attr.data-day]=\"cursor()\"\n >\n <h3 class=\"font-display text-base font-semibold\">\n {{ dayHeading(cursor()) }}\n </h3>\n @if (eventsFor(cursor()).length === 0) {\n <p class=\"mt-2 text-sm text-neutral\">Sin eventos este día.</p>\n } @else {\n <ul class=\"mt-3 space-y-2\">\n @for (event of eventsFor(cursor()); track event.key) {\n <li\n class=\"flex items-center gap-1 text-xs\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2.5 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span>{{ event.title }}</span>\n </li>\n }\n </ul>\n }\n </div>\n }\n @case ('agenda') {\n <ol class=\"space-y-4\">\n @for (day of agendaDays(); track day) {\n <li class=\"rounded-xl border border-neutral/20 bg-surface p-4\">\n <h3 class=\"text-sm font-semibold\">{{ dayHeading(day) }}</h3>\n <ul class=\"mt-2 space-y-1\">\n @for (event of eventsFor(day); track event.key) {\n <li\n class=\"flex items-center gap-2 text-sm\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span>{{ event.title }}</span>\n </li>\n }\n </ul>\n </li>\n }\n </ol>\n }\n }\n }\n\n <!-- Problem records are VISIBLE, never silently dropped: a row\n without a valid start or with an impossible interval cannot\n be painted, but the operator must see why. -->\n @if (hasProblemRecords()) {\n <section\n class=\"rounded-xl border border-neutral/30 p-4\"\n aria-label=\"Registros sin fecha válida\"\n >\n <h3 class=\"font-display text-sm font-semibold\">Registros sin fecha válida</h3>\n <p class=\"mt-1 text-help text-neutral\">\n Estos registros no se pueden pintar en el calendario:\n </p>\n <ul class=\"mt-2 space-y-1 text-sm\">\n @for (row of mapping().undated; track $index) {\n <li>{{ recordLabel(row) }} — {{ reasonUndated }}</li>\n }\n @for (item of mapping().invalid; track $index) {\n <li>{{ recordLabel(item.row) }} — {{ item.reason }}</li>\n }\n </ul>\n </section>\n }\n }\n }\n </section>\n } @else {\n <app-empty-state\n title=\"Configuración inválida\"\n message=\"Esta vista no declara una configuración de calendario válida.\"\n />\n }\n `,\n})\nexport class CalendarView {\n private readonly route = inject(ActivatedRoute);\n // The shell mounts views via `loadComponent` (no input bindings), so the\n // schema is resolved from route data exactly like FormView does.\n private readonly routeData = toSignal(this.route.data, { requireSync: true });\n private readonly dataSource = inject(DataSourceService);\n private readonly errorMapper = inject(ErrorMapperService);\n\n /**\n * Resource schema for this view. The shell mounts views via\n * `loadComponent` (no input bindings), so the schema is resolved from\n * route data: either an explicit `schema` object, or a `resourceId` looked\n * up in the registry (same route shape FormView uses).\n */\n readonly schema = computed<ResourceSchema | undefined>(() => {\n const data = this.routeData();\n const explicit = data['schema'];\n if (explicit !== undefined) return explicit as ResourceSchema;\n const resourceId = data['resourceId'];\n return typeof resourceId === 'string' ? getSchema(resourceId) : undefined;\n });\n\n /**\n * Config resolved from `schema.uiViews[<route uiView>]`; `undefined` when\n * the route does not name a calendar view with a usable config — the\n * template then renders the invalid-config empty state instead of a grid.\n */\n readonly config = computed<CalendarViewConfig | undefined>(() => {\n const schema = this.schema();\n const viewName = this.routeData()['uiView'];\n if (schema === undefined || typeof viewName !== 'string') return undefined;\n const cfg = calendarConfigOf(schema.uiViews?.[viewName]);\n if (cfg === undefined) return undefined;\n const usable =\n typeof cfg.startField === 'string' &&\n cfg.startField !== '' &&\n typeof cfg.titleField === 'string' &&\n cfg.titleField !== '';\n return usable ? cfg : undefined;\n });\n\n /** Explicit user choice; `undefined` ⇒ the config's `defaultView`. */\n private readonly explicitMode = signal<CalendarMode | undefined>(undefined);\n\n readonly mode = computed<CalendarMode>(\n () => this.explicitMode() ?? this.config()?.defaultView ?? 'month',\n );\n\n /** Anchor civil day of the visible range; init = today (civil). */\n readonly cursor = signal<string>(todayCivil());\n\n /** Visible `{desde, hasta}` derived from cursor + mode. */\n readonly visibleRange = computed<CivilRange | null>(() =>\n this.config() === undefined ? null : visibleRangeFor(this.mode(), this.cursor()),\n );\n\n readonly rangeLabel = computed(() =>\n this.config() === undefined ? '' : rangeLabelFor(this.mode(), this.cursor()),\n );\n\n readonly status = signal<Status>('loading');\n readonly rows = signal<SourceRow[]>([]);\n /** Clave seleccionada localmente cuando el binding declara `mode:'inline'`. */\n readonly seleccionada = signal<string | null>(null);\n readonly errorMessage = signal('');\n\n readonly modeOptions = MODE_OPTIONS;\n readonly weekdayHeadings = WEEKDAY_HEADINGS;\n readonly reasonUndated = REASON_UNDATED;\n\n /** Guards against a slow response overwriting a newer range's rows. */\n private loadEpoch = 0;\n\n constructor() {\n effect(() => {\n const cfg = this.config();\n const range = this.visibleRange();\n if (cfg === undefined || range === null) return;\n void this.load(cfg, range);\n });\n }\n\n /** Rows mapped to events + visible problem records (pure model). */\n readonly mapping = computed<CalendarMapping>(() => {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return { events: [], undated: [], invalid: [] };\n const endFieldType =\n cfg.endField !== undefined ? findFieldByKey(schema.fields, cfg.endField)?.type : undefined;\n return mapCalendarRows(this.rows(), cfg, {\n keyField: schema.keyField,\n endFieldType,\n });\n });\n\n /** Events bucketed per civil day, clamped to the visible range. */\n readonly eventsByDay = computed(() => {\n const range = this.visibleRange();\n if (range === null) return new Map<string, CalEvent[]>();\n return eventsByDay(this.mapping().events, range);\n });\n\n readonly monthGrid = computed(() => monthGridDays(this.cursor()));\n readonly weekColumnDays = computed(() => weekDays(this.cursor()));\n\n /** Agenda headings: the days of the range that actually have events. */\n readonly agendaDays = computed(() => {\n const range = this.visibleRange();\n if (range === null) return [] as string[];\n return daysInRange(range).filter((day) => this.eventsByDay().has(day));\n });\n\n readonly hasProblemRecords = computed(\n () => this.mapping().undated.length > 0 || this.mapping().invalid.length > 0,\n );\n\n previous(): void {\n this.cursor.set(shiftCursor(this.mode(), this.cursor(), -1));\n }\n\n next(): void {\n this.cursor.set(shiftCursor(this.mode(), this.cursor(), 1));\n }\n\n /** Reset the cursor to today (civil). */\n today(): void {\n this.cursor.set(todayCivil());\n }\n\n setMode(mode: CalendarMode): void {\n this.explicitMode.set(mode);\n }\n\n retry(): void {\n const cfg = this.config();\n const range = this.visibleRange();\n if (cfg !== undefined && range !== null) void this.load(cfg, range);\n }\n\n /** Binding `select` declarado por la vista (R9.9.4 · decision table). */\n private binding(): SelectionBinding | undefined {\n return this.routeData()['select'] as SelectionBinding | undefined;\n }\n\n /**\n * Clic de evento. Sin declaración ⇒ inerte (el calendario no navegaba);\n * con declaración ⇒ sólo el contrato declarado corre: `route` emite al\n * dispatcher, `inline` selecciona en local, `none` es no-op explícito.\n */\n seleccionar(event: CalEvent): void {\n const binding = this.binding();\n if (binding === undefined) return;\n if (binding.mode === 'inline') {\n this.seleccionada.set(event.key);\n return;\n }\n if (binding.mode === 'none') return;\n const schema = this.schema();\n const emit = this.routeData()['emit'] as SelectionEmitter | undefined;\n emit?.({\n resource: schema?.id ?? String(this.routeData()['resourceId'] ?? ''),\n key: event.key,\n action: binding.on,\n });\n }\n\n /** El DOM declara clicable sólo si hay contrato que responder. */\n seleccionable(): boolean {\n const binding = this.binding();\n return binding !== undefined && binding.mode !== 'none';\n }\n\n esSeleccionada(event: CalEvent): boolean {\n return this.seleccionable() && this.seleccionada() === event.key;\n }\n\n eventsFor(day: string): readonly CalEvent[] {\n return this.eventsByDay().get(day) ?? NO_EVENTS;\n }\n\n dayNumber(day: string): number {\n return Number(day.slice(8, 10));\n }\n\n isInCursorMonth(day: string): boolean {\n return sameMonth(day, this.cursor());\n }\n\n /** Weekday + number (\"lun 12\") for week columns. */\n weekdayShort(day: string): string {\n return weekdayShort(day);\n }\n\n /** Full heading (\"lunes, 12 de octubre de 2026\") for day/agenda. */\n dayHeading(day: string): string {\n return agendaDayLabel(day);\n }\n\n recordLabel(row: SourceRow): string {\n const schema = this.schema();\n if (schema === undefined) return '';\n const title = String(row[schema.titleField] ?? '').trim();\n return title !== '' ? title : String(row[schema.keyField] ?? '');\n }\n\n private async load(cfg: CalendarViewConfig, range: CivilRange): Promise<void> {\n const schema = this.schema();\n if (schema === undefined) return;\n const epoch = ++this.loadEpoch;\n this.status.set('loading');\n // Optimization branch (decided): range ctx ONLY when the schema declares\n // a backend view — that's the signal that a `where` may filter on\n // `param: 'desde'`/`'hasta'`. Without a view the params would be ignored\n // (and the full listing is cached while the user navigates).\n const ctx =\n cfg.source?.view !== undefined && cfg.source.view !== ''\n ? { desde: range.desde, hasta: range.hasta }\n : undefined;\n const source = cfg.source ?? { resource: schema.id };\n try {\n const rows = await this.dataSource.rows(source, ctx);\n if (epoch !== this.loadEpoch) return;\n this.rows.set(rows);\n this.errorMessage.set('');\n this.status.set('ready');\n } catch (error) {\n if (epoch !== this.loadEpoch) return;\n this.errorMessage.set(\n this.errorMapper.map(error).general ?? 'No se pudo cargar el calendario.',\n );\n this.status.set('error');\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/models/api.model.ts","../../../projects/gas-admin-ui/src/lib/core/tokens.ts","../../../projects/gas-admin-ui/src/lib/core/services/auth.service.ts","../../../projects/gas-admin-ui/src/lib/core/interceptors/api-error.interceptor.ts","../../../projects/gas-admin-ui/src/lib/core/utils/context-resolver.ts","../../../projects/gas-admin-ui/src/lib/core/services/api.service.ts","../../../projects/gas-admin-ui/src/lib/core/services/config.service.ts","../../../projects/gas-admin-ui/src/lib/core/registry/schema-catalog.ts"],"sourcesContent":["/**\n * Contrato HTTP con el backend (doc/baseapi.md §8 «Errores»).\n *\n * El backend responde SIEMPRE HTTP 200; los errores viajan en el body\n * con `{ error: { status, message, errors?: [{ path, message }] } }`.\n * El interceptor (Fase 3) los convierte en `ApiError`.\n */\n\n/**\n * Un error con ruta del422 (auth.md §6): `path` para pintarlo en el campo y\n * `code` MAYUS_SNAKE (`VALIDATION_MIN_LENGTH`) para mapearlo sin leer texto.\n */\nexport interface ApiIssue {\n /** Ruta del campo afectado (`faq[2].answer`, `sku`). */\n path: string;\n /** Mensaje en español, listo para pintar. */\n message: string;\n /** Código estable del validador (B11); el mensaje puede cambiar, el código no. */\n code?: string;\n}\n\n/** Shape del body de error (doc/baseapi.md §8 «Errores»). */\nexport interface ApiErrorBody {\n error: {\n status: number;\n message: string;\n /** Ruta del campo afectado cuando el backend la envía (base.md §10). */\n path?: string;\n /**\n * Código de sesión de §4.5 (R2.9): `AUTH_REQUIRED` · `AUTH_INVALID` ·\n * `AUTH_EXPIRED` · `FORBIDDEN` · `RATE_LIMITED`. Aparece SÓLO en esas\n * cinco respuestas — el sobre de error es aditivo (O7 / D10-5).\n */\n code?: string;\n /**\n * Errores de validación con ruta (base.md §5, guía §10): el 422 los manda\n * ACÁ (`error.errors[]`), no en `path`. Sin modelarlos el form veía\n * «Errores de validación» genérico y ningún campo se pintaba (D42).\n */\n errors?: ApiIssue[];\n };\n}\n\n/** Error de API estandarizado que lanza el interceptor de errores. */\nexport class ApiError extends Error {\n readonly status: number;\n /** Ruta del campo que provocó el error (ej. `faq[2].answer`), si existe. */\n readonly fieldPath?: string;\n /** Código de sesión de §4.5 si el backend lo envió; `undefined` en el resto. */\n readonly code?: string;\n /** Issues con ruta (422); vacío cuando el error no trae `errors[]`. */\n readonly issues: readonly ApiIssue[];\n\n constructor(\n status: number,\n message: string,\n fieldPath?: string,\n code?: string,\n issues: readonly ApiIssue[] = [],\n ) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.fieldPath = fieldPath;\n this.code = code;\n this.issues = issues;\n }\n}\n\n/**\n * `meta.renewedToken` (auth.md §3.2/§3.4): el back lo adjunta a la respuesta\n * cuando la vida del token bajó del umbral de renovación. Es el ÚNICO modo\n * de que el front se entere — el token nuevo no viaja en cabeceras ni en la\n * URL. Devuelve `null` cuando no hay renovación (que es lo normal).\n *\n * Vive acá y no en el `ApiService` porque lo consumen DOS servicios\n * (`ApiService` y `AuthService`) y éste modelo no importa a ninguno de los\n * dos: sin este archivo no hay ciclo de imports.\n */\nexport function renewedTokenOf(body: unknown): string | null {\n if (body === null || typeof body !== 'object') return null;\n const meta = (body as { meta?: unknown }).meta;\n if (meta === null || typeof meta !== 'object') return null;\n const token = (meta as { renewedToken?: unknown }).renewedToken;\n return typeof token === 'string' && token !== '' ? token : null;\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * URL base de la API (baseapi §2.3): es la URL directa del Web App de Apps\n * Script; ahí se hace el `POST` del envelope `{ token, method, path, payload }`.\n * OBLIGATORIO proporcionarlo en `app.config.ts`:\n * `{ provide: API_URL, useValue: 'https://…' }` (Fase 3). Vacío = mismo origen.\n */\nexport const API_URL = new InjectionToken<string>('API_URL');\n\n/**\n * Superficie mínima que `gas-admin-ui` usa de Firebase Auth.\n * Implementada por la aplicación consumidora; inyectada vía `FIREBASE_AUTH_ADAPTER`.\n * Mantener la interfaz fina evita acoplar la librería a una versión específica de Firebase.\n */\nexport interface FirebaseAuthAdapter {\n /**\n * Abre un popup de Google sign-in y resuelve al ID token de Firebase.\n * Rechaza si el usuario cancela o hay error.\n */\n signInWithGoogle(): Promise<string>;\n\n /**\n * Inicia sesión con email y contraseña vía Firebase y resuelve al ID token.\n * Se usa cuando provider === 'password' Y firebase está configurado (Firebase email auth).\n */\n signInWithEmailAndPassword(email: string, password: string): Promise<string>;\n\n /**\n * Cierra sesión en Firebase (llamado junto al clear de la sesión de la app).\n */\n signOut(): Promise<void>;\n\n /**\n * Obtiene un ID token de Firebase FRESCO para usarlo en la cabecera\n * `Authorization: Bearer <token>` de las peticiones REST.\n *\n * OPCIONAL: si el adaptador no lo implementa, `gas-admin-ui` envía el token\n * de sesión almacenado tal cual (comportamiento actual). Implementarlo\n * permite refrescar el ID token antes de cada petición REST, evitando\n * errores \"ID token inválido\" tras la expiración de Firebase (1 hora).\n *\n * El adaptador decide cuándo refrescar (p. ej. `getIdToken(true)` de Firebase\n * SDK fuerza la renovación; sin argumento devuelve el cacheado si aún es válido).\n */\n getIdToken?(): Promise<string>;\n}\n\n/**\n * Token DI opcional para el adaptador de Firebase Auth.\n * Proporcionado por la aplicación consumidora cuando usa proveedores federados o Firebase Auth.\n */\nexport const FIREBASE_AUTH_ADAPTER = new InjectionToken<FirebaseAuthAdapter>(\n 'gas.admin.firebaseAuthAdapter',\n);\n\n/**\n * Driver de transporte HTTP del motor.\n *\n * Vive acá (capa de infraestructura) y NO en `ResourceSchema` (capa de dominio)\n * porque es una decisión de red, no de modelo de datos. Cambiar el driver\n * no debería requerir tocar ningún schema de recursos.\n *\n * - `'gas'` (default): tunnel `POST text/plain` con envelope\n * `{ token, method, path, payload }` — contrato de Google Apps Script.\n * - `'rest'`: verbos HTTP reales (`GET`, `POST`, `PUT`, `DELETE`) con\n * `Authorization: Bearer <token>` en cabeceras y params en la URL (GET/DELETE)\n * o en el body JSON (POST/PUT/PATCH).\n *\n * Se fija en `provideGasAdmin({ driver })`. Sin declaración explícita el\n * motor usa `'gas'` — cero impacto para consumidores existentes.\n */\nexport const API_DRIVER = new InjectionToken<'gas' | 'rest'>('gas.admin.driver');\n\n/**\n * Registro de FUENTES DE CONTEXTO para marcadores en paths de endpoints.\n *\n * Cada clave es el nombre de una fuente — `{ <fuente> }` o\n * `{ <fuente>.<ruta.anidada> }` dentro de un path declarado en\n * `ResourceEndpoints` — y cada valor es un getter sin argumentos que devuelve\n * el dato ACTUAL en el momento de la petición (no al registrar).\n *\n * El motor ya aporta dos fuentes construidas y éstas NO se declaran acá:\n *\n * - `auth` → la sesión que dueña `AuthService` (usuario y token).\n * - `config` → el mapa global que puebla `ConfigService` desde los singletons.\n *\n * Este registro es sólo para fuentes que aporta el consumidor, para que el\n * motor no conozca nombres de dominio: qué campo se lee lo decide el schema,\n * no el engine.\n *\n * Un marcador que apunte a una fuente no registrada, o a un valor ausente,\n * revienta con un error que nombra el path y el marcador — jamás se manda un\n * segmento vacío al backend.\n */\nexport type ValueSources = Readonly<Record<string, () => unknown>>;\n\nexport const VALUE_SOURCES = new InjectionToken<ValueSources>('gas.admin.valueSources');\n\n","import { HttpClient, HttpHeaders } from '@angular/common/http';\nimport { computed, inject, Service, signal } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { catchError, from, map, Observable, of, switchMap, tap, throwError } from 'rxjs';\n\nimport type { FieldSchema, ResourceSchema } from '../models/schema.model';\nimport { renewedTokenOf } from '../models/api.model';\nimport { API_DRIVER, API_URL, FIREBASE_AUTH_ADAPTER } from '../tokens';\nimport { getAuth } from '../registry/schema-catalog';\n\n\n/**\n * Sesión del admin (auth.md §5.2) — **única dueña** del token y del usuario.\n *\n * Qué vive acá y por qué:\n *\n * - **Señales** `user` / `token` / `isAuthenticated` / `role`: el único\n * estado que el resto de la app lee (guards, menú, interceptor).\n * - **`login()` / `logout()` / `logoutAll()` / `restore()`**: operaciones de sesión.\n * El `AuthService` NO inyecta `ApiService` — sólo `HttpClient` — porque\n * `ApiService` inyecta a éste para leer el token, y un ciclo de DI en\n * Angular es un error en tiempo de arranque, no un warning.\n * - **`applyAuthCode()` (R2.9): EL único intérprete de la tabla de §4.5.**\n * Ningún otro archivo decide qué pasa ante `AUTH_*` o `FORBIDDEN`.\n * - **Almacenamiento (R2.12): `sessionStorage`, siempre.** `rememberMe`\n * no puede usar `localStorage` porque D10-3 lo negó — ver más abajo.\n *\n * La sesión arranca desde el storage y `restore()` la revalida contra\n * `/auth/me` en el initializer: un usuario desactivado mientras la pestaña\n * estaba abierta cae al primer reload, no en mitad de una edición.\n */\n\n/** Usuario tal y como lo sirve `publicUser_` (lista blanca, R2.17). */\nexport interface SessionUser {\n id?: string;\n email?: string;\n name?: string;\n role?: string;\n active?: boolean;\n last_login_at?: string;\n created_at?: string;\n}\n\n/** Los cinco códigos de §4.5. El resto de errores no son de sesión. */\nexport type AuthErrorCode =\n 'AUTH_REQUIRED' | 'AUTH_INVALID' | 'AUTH_EXPIRED' | 'FORBIDDEN' | 'RATE_LIMITED';\n\n/** Ruta de login: fuera del shell para que ningún guard la proteja. */\nexport const LOGIN_PATH = '/login';\n\n/**\n * Claves del `sessionStorage`. Exportadas porque **la sesión se restaura\n * desde ahí en el constructor**: un test que quiera una sesión arrancando\n * tiene que sembrarlas antes de inyectar el servicio, y el que las use con\n * otro nombre quebraría el restore en silencio.\n */\nexport const SESSION_TOKEN_KEY = 'admin.session.token';\nexport const SESSION_USER_KEY = 'admin.session.user';\n\ninterface LoginResponse {\n token: string;\n user: SessionUser;\n}\ninterface MeResponse {\n user: SessionUser;\n}\n\n@Service()\nexport class AuthService {\n private readonly http = inject(HttpClient);\n private readonly apiUrl = inject(API_URL);\n private readonly router = inject(Router);\n private readonly firebaseAdapter = inject(FIREBASE_AUTH_ADAPTER, { optional: true });\n private readonly driver = inject(API_DRIVER, { optional: true }) ?? 'gas';\n\n /**\n * D10-3: **sólo `sessionStorage`.** La opción «recordarme» con\n * `localStorage` quedó denegada en §9.3 — muere con la pestaña ⇒ cero\n * token legible por un XSS futuro. `authSchema.rememberMe` es `false`\n * por la misma razón: declarar `true` sería prometer un storage que\n * este servicio no usa.\n *\n * Nunca en la URL ni en el cuerpo de un GET (§5.4): el token sólo viaja\n * dentro del envelope `{ token, method, path, payload }`.\n */\n private readonly storage: Storage = sessionStorage;\n\n private readonly _user = signal<SessionUser | null>(null);\n private readonly _token = signal<string | null>(null);\n /** Ruta previa a un `AUTH_EXPIRED`, para volver tras reloguear (§4.5). */\n private readonly _returnUrl = signal<string | null>(null);\n\n readonly user = this._user.asReadonly();\n readonly token = this._token.asReadonly();\n readonly returnUrl = this._returnUrl.asReadonly();\n /** Sólo hay sesión con token Y usuario: uno sin el otro es estado corrupto. */\n readonly isAuthenticated = computed(() => this._token() !== null && this._user() !== null);\n readonly role = computed(() => this._user()?.role ?? '');\n\n constructor() {\n const token = this.storage.getItem(SESSION_TOKEN_KEY);\n if (token !== null && token !== '') this._token.set(token);\n const raw = this.storage.getItem(SESSION_USER_KEY);\n if (raw !== null && raw !== '') {\n try {\n this._user.set(JSON.parse(raw) as SessionUser);\n } catch {\n // JSON corrupto ⇒ no hay sesión que restaurar; storage sucio.\n this.storage.removeItem(SESSION_USER_KEY);\n this.storage.removeItem(SESSION_TOKEN_KEY);\n this._token.set(null);\n }\n }\n }\n\n // ── Sesión ───────────────────────────────────────────────────────────────\n\n login(email: string, password: string): Observable<SessionUser> {\n return this.post<LoginResponse>('/auth/login', { email, password }).pipe(\n map((res) => {\n this.open(res.token, res.user);\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.applyRenewedToken(renewed);\n return res.user;\n }),\n );\n }\n\n /**\n * Inicia sesión con Google mediante el adaptador de Firebase y canjea\n * el ID token contra el backend.\n */\n loginWithGoogle(): Observable<SessionUser> {\n if (!this.firebaseAdapter) {\n return throwError(() => new Error('[gas-admin-ui] FIREBASE_AUTH_ADAPTER is not provided.'));\n }\n return from(this.firebaseAdapter.signInWithGoogle()).pipe(\n switchMap((idToken) => this.exchangeFirebaseToken(idToken)),\n );\n }\n\n /**\n * Inicia sesión con correo y contraseña en Firebase y canjea el ID token\n * resultante contra el backend.\n */\n loginWithFirebasePassword(email: string, password: string): Observable<SessionUser> {\n if (!this.firebaseAdapter) {\n return throwError(() => new Error('[gas-admin-ui] FIREBASE_AUTH_ADAPTER is not provided.'));\n }\n return from(this.firebaseAdapter.signInWithEmailAndPassword(email, password)).pipe(\n switchMap((idToken) => this.exchangeFirebaseToken(idToken)),\n );\n }\n\n /**\n * Canjea un token de identidad de Firebase por la sesión propia de la app,\n * manteniendo idéntico el contrato del envelope y almacenamiento en sessionStorage.\n */\n private exchangeFirebaseToken(idToken: string): Observable<SessionUser> {\n const endpoint = getAuth()?.firebase?.exchangeEndpoint ?? '/auth/firebase';\n return this.post<LoginResponse>(endpoint, { idToken }).pipe(\n map((res) => {\n this.open(res.token, res.user);\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.applyRenewedToken(renewed);\n return res.user;\n }),\n );\n }\n\n /**\n * Revocación best-effort (§9.7): se pide al back que ponga el `sid` en la\n * lista de revocados, pero **la sesión local se ciere haya o no respuesta**.\n * Un logout que «falla» por red y deja al usuario dentro es peor que uno\n * que deja una sesión muerta en el servidor.\n */\n logout(): Observable<void> {\n return this.post<{ loggedOut?: boolean }>('/auth/logout').pipe(\n tap({\n next: () => {\n this.clear();\n void this.firebaseAdapter?.signOut();\n this.goLogin();\n },\n error: () => {\n // La sesión local se cierra haya o no respuesta del back (§9.7).\n // Firebase también se cierra best-effort y se va al login igual.\n this.clear();\n void this.firebaseAdapter?.signOut();\n this.goLogin();\n },\n }),\n map(() => undefined),\n );\n }\n\n\n /** Revoca las sesiones del usuario en todos sus dispositivos. */\n logoutAll(): Observable<{ effectiveWithinSeconds: number }> {\n return this.post<{ sessionsRevoked: boolean; effectiveWithinSeconds: number }>(\n '/auth/logout-all',\n ).pipe(\n tap(() => {\n this.clear();\n this.goLogin();\n }),\n map((res) => ({ effectiveWithinSeconds: res.effectiveWithinSeconds })),\n );\n }\n\n /** Revalida la sesión restaurada contra `/auth/me`; sin token ⇒ sin viaje. */\n restore(): Observable<void> {\n if (this._token() === null) return of(undefined);\n return this.post<MeResponse>('/auth/me').pipe(\n tap((res) => {\n this.open(this._token(), res.user);\n }),\n // El interceptor ya interpretó el `AUTH_*` (R2.9) antes de llegar\n // acá: esto sólo evita que un fallo rompa el arranque de la app.\n catchError(() => of(undefined)),\n map(() => undefined),\n );\n }\n\n /**\n * Renovación deslizante (§3.2): el back manda `meta.renewedToken` cuando\n * la vida del token bajó del umbral. Se persiste el MISMO día que el\n * token original — el `sid` no cambia, sólo se emite una firma nueva.\n */\n applyRenewedToken(token: string): void {\n if (token === '' || this._token() === null) return;\n this._token.set(token);\n this.storage.setItem(SESSION_TOKEN_KEY, token);\n }\n\n // ── R2.9 · la tabla de §4.5, en un solo sitio ───────────────────────────\n\n /**\n * ÚNICO intérprete de los códigos de sesión (auth.md §4.5 / §5.2).\n * El interceptor `apiErrorInterceptor` lo llama y nada más: cualquier\n * archivo que decida «si ve AUTH_EXPIRED hago X» está fuera de contrato.\n *\n * | Código | Qué hace |\n * | --------------- | ------------------------------------------- |\n * | `AUTH_REQUIRED` | limpia sesión y va a login |\n * | `AUTH_INVALID` | limpia sesión y va a login |\n * | `AUTH_EXPIRED` | guarda la ruta previa, limpia, va a login |\n * | `FORBIDDEN` | **no** cierra sesión (el usuario sigue dentro) |\n * | `RATE_LIMITED` | no hace nada: lo espera el formulario |\n */\n applyAuthCode(code: string | undefined): void {\n switch (code) {\n case 'AUTH_EXPIRED':\n // La ruta previa se guarda ANTES de borrar: después de limpiar no\n // queda desde dónde navegar (y `router.url` ya cambió al llegar).\n this._returnUrl.set(this.currentUrl());\n this.clear();\n this.goLogin();\n break;\n case 'AUTH_REQUIRED':\n case 'AUTH_INVALID':\n // «La sesión terminó» — no hay vuelta atrás que conservar.\n this.clear();\n this.goLogin();\n break;\n case 'FORBIDDEN':\n case 'RATE_LIMITED':\n // Decisión explícita de NO tocar la sesión: §4.5 exige que el\n // usuario vea «sin permiso» o «espera» **siguiendo dentro**.\n break;\n default:\n // No es un código de sesión: nada que interpretar.\n break;\n }\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n\n /** ¿El rol de la sesión cubre `required`? (mínimo: admin ≥ staff, §4.4) */\n hasRole(required: string): boolean {\n if (required === '' || required === 'session') return this.isAuthenticated();\n const role = this.role();\n if (role === 'admin') return true;\n return role !== '' && role === required;\n }\n\n /**\n * §5.3 — el ÚNICO criterio de visibilidad del front (menú, rutas y\n * botones). `requires` viene del vocabulario de §4.4: `public` no pide\n * sesión; todo lo demás sí; `roles` estrecha.\n *\n * Es **pura decisión de UI**. El back re-verifica `accessLevel` en cada\n * petición (R2.6), así que filtrar acá no protege nada: sólo evita\n * enseñarle a alguien un botón que le va a devolver 403.\n */\n canAccess(access?: { requires?: string; roles?: string[] }): boolean {\n const requires = access?.requires ?? 'session';\n if (requires === 'public') return true;\n if (!this.isAuthenticated()) return false;\n\n // §4.4 escribe el requerimiento como `role:admin`; §5.3 como\n // `roles: ['admin']`. Se aceptan las dos formas en lugar de elegir una:\n // son el mismo dato visto desde el nivel de acceso y desde el recurso.\n const role = requires.startsWith('role:') ? requires.slice('role:'.length) : '';\n const required = role === '' ? (access?.roles ?? []) : [role];\n if (required.length === 0) return true;\n return required.some((one) => this.hasRole(one));\n }\n\n // ── R7.5 · Consumo de políticas en UI (F10) ───────────────────────────────\n\n /**\n * Evalúa si el usuario puede ejecutar una operación específica según\n * `policies.access.operations` del recurso (B8 · R7.4a).\n *\n * El override por operación tiene prioridad sobre el umbral del grupo.\n * Si no hay override, se usa el umbral `policies.access.read` o\n * `policies.access.write` según la operación.\n *\n * Fallback a `permissions` (legacy) si `policies` no existe: mientras\n * los schemas locales no tengan `policies`, se mantiene compatibilidad.\n *\n * Es **pura decisión de UI**. El back vuelve a verificar en cada petición\n * (24-crud), así que esto sólo evita mostrar botones que darían 403.\n */\n canDoOperation(\n schema: ResourceSchema,\n operation: 'list' | 'get' | 'create' | 'update' | 'remove',\n ): boolean {\n // R7.5: intentar policies primero\n const ops = schema.policies?.access?.operations;\n if (ops && ops[operation]) {\n return this.meetsLevel(ops[operation]);\n }\n\n // Sin override en policies: usar el umbral del grupo\n if (schema.policies?.access) {\n const isWrite = operation === 'create' || operation === 'update' || operation === 'remove';\n const groupLevel = isWrite ? schema.policies.access.write : schema.policies.access.read;\n return this.meetsLevel(groupLevel ?? 'admin');\n }\n\n // Fallback a permissions (legacy, R7.5 transición)\n const perms = schema.permissions;\n if (perms) {\n const permMap: Record<string, boolean | undefined> = {\n list: true, // list no tiene en permissions, siempre true si no hay policies\n get: true,\n create: perms.create,\n update: perms.update,\n remove: perms.remove,\n };\n return permMap[operation] !== false;\n }\n\n // Sin policies ni permissions: UX optimista (§5.3 legacy) — el back es\n // la autoridad y responde 403 si el rol no alcanza.\n return true;\n }\n\n /**\n * Evalúa si el usuario puede leer un campo según `field.access.read`\n * (B8 · R7.4b).\n *\n * Es **pura decisión de UI**. El back vuelve a filtrar la respuesta en\n * cada petición (24-crud), así que esto sólo evita mostrar datos que el\n * usuario no debería ver.\n */\n canReadField(field: FieldSchema): boolean {\n if (!field.access || !field.access.read) return true; // sin restricción ⇒ visible\n return this.meetsLevel(field.access.read);\n }\n\n /**\n * Evalúa si el usuario puede escribir un campo según `field.access.write`\n * (B8 · R7.4b).\n *\n * Es **pura decisión de UI**. El back rechaza la escritura con 422 si el\n * usuario no tiene permiso, así que esto sólo pone el campo en\n * solo-lectura en el frontend.\n */\n canWriteField(field: FieldSchema): boolean {\n if (!field.access || !field.access.write) return true; // sin restricción ⇒ editable\n return this.meetsLevel(field.access.write);\n }\n\n /**\n * Verifica si el rol actual cumple un nivel de acceso dado.\n * Usa la misma jerarquía que `ACCESS_ROLE_RANK` del back (11-auth):\n * admin ≥ staff.\n */\n private meetsLevel(level: string): boolean {\n if (level === 'public') return true;\n if (level === 'session') return this.isAuthenticated();\n if (level.startsWith('role:')) {\n const requiredRole = level.slice('role:'.length);\n return this.hasRole(requiredRole);\n }\n // fallback conservador: nivel desconocido ⇒ sólo admin\n return this.hasRole('admin');\n }\n\n private open(token: string | null, user: SessionUser | null): void {\n if (token !== null && token !== '') this._token.set(token);\n this._user.set(user);\n if (token !== null && token !== '') this.storage.setItem(SESSION_TOKEN_KEY, token);\n if (user !== null) this.storage.setItem(SESSION_USER_KEY, JSON.stringify(user));\n }\n\n /**\n * Borra token **y** usuario (§5.4) — en ese orden de importancia: un token\n * sin usuario no autentica, pero un usuario sin token engaña al menú.\n */\n /**\n * Ruta previa guardada por un `AUTH_EXPIRED` (§4.5), **consumida**: se\n * devuelve una sola vez y se borra. Sin el consume, cada login futuro\n * volvería a la misma URL vieja aunque el usuario hubiera llegado al\n * login desde el menú.\n */\n consumeReturnUrl(): string | null {\n const url = this._returnUrl();\n this._returnUrl.set(null);\n return url;\n }\n\n private clear(): void {\n this._token.set(null);\n this._user.set(null);\n this.storage.removeItem(SESSION_TOKEN_KEY);\n this.storage.removeItem(SESSION_USER_KEY);\n }\n\n private goLogin(): void {\n if (this.currentUrl().startsWith(LOGIN_PATH)) return;\n // Angular rechaza la promesa cuando no hay ruta que empate (NG04002).\n // Navegar es un efecto, no una operación cuyo resultado alguien espere:\n // el catch evita una rejection sin handler en arranques y en tests.\n void this.router.navigate([LOGIN_PATH]).catch(() => undefined);\n }\n\n private currentUrl(): string {\n return this.router.url;\n }\n\n private buildUrl(path: string): string {\n const base = this.apiUrl ? this.apiUrl.replace(/\\/+$/, '') : '';\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return `${base}${cleanPath}`;\n }\n\n private envelope(path: string, payload: Record<string, unknown> = {}) {\n return { token: this._token(), method: 'POST' as const, path, payload };\n }\n\n /**\n * Despacho dual-driver para peticiones de autenticación:\n *\n * - **`gas` (default)**: `POST` a `API_URL` con cuerpo `text/plain` y envelope\n * `{ token, method: 'POST', path, payload }`.\n * - **`rest`**: llamadas REST reales:\n * - `/auth/me`: `GET` a `${apiUrl}/auth/me` con `Authorization: Bearer ${token}`.\n * - resto (`/auth/login`, `/auth/firebase`, `/auth/logout`, etc.): `POST` con\n * `Content-Type: application/json` y payload directo como body JSON. Si existe\n * token activo, se adjunta en cabecera `Authorization`.\n */\n private buildAuthRequest<T>(\n path: string,\n payload: Record<string, unknown> = {},\n ): Observable<T> {\n if (this.driver === 'rest') {\n const url = this.buildUrl(path);\n let headers = new HttpHeaders();\n const token = this._token();\n if (token) {\n headers = headers.set('Authorization', `Bearer ${token}`);\n }\n\n if (path === '/auth/me') {\n return this.http.get<T>(url, { headers });\n }\n\n headers = headers.set('Content-Type', 'application/json');\n return this.http.post<T>(url, payload, { headers });\n }\n\n return this.http.post<T>(this.apiUrl, this.envelope(path, payload), {\n headers: { 'Content-Type': 'text/plain' },\n });\n }\n\n private post<T>(path: string, payload: Record<string, unknown> = {}): Observable<T> {\n return this.buildAuthRequest<T>(path, payload);\n }\n}\n","import { HttpErrorResponse, HttpInterceptorFn, HttpResponse } from '@angular/common/http';\nimport { inject } from '@angular/core';\nimport { catchError, map, throwError } from 'rxjs';\n\nimport { ApiError } from '../models/api.model';\nimport type { ApiErrorBody } from '../models/api.model';\nimport { AuthService } from '../services/auth.service';\n\n/**\n * Mensaje humano para un error HTTP real (§2).\n *\n * El backend siempre responde 200 con el error dentro del body —eso ya\n * viene en español—, así que esto sólo cubre lo que el transporte falla:\n * red caída, proxy y 5xx. Sin esto el usuario ve el texto crudo de Angular\n * («Http failure response for http://…»), que es inglés técnico.\n */\nfunction httpMessage(status: number): string {\n switch (status) {\n case 0:\n return 'No se pudo conectar con el servidor. Revisa tu conexión.';\n case 401:\n return 'Tu sesión ha expirado. Vuelve a iniciar sesión.';\n case 403:\n return 'No tienes permisos para realizar esta acción.';\n case 404:\n return 'No se encontró el recurso solicitado.';\n case 409:\n return 'El registro cambió o ya existe. Recarga la página y vuelve a intentarlo.';\n case 413:\n return 'El archivo es demasiado grande.';\n case 422:\n return 'Los datos enviados no son válidos.';\n default:\n if (status >= 500) {\n return 'El servidor tuvo un problema. Vuelve a intentarlo en unos segundos.';\n }\n return status >= 400 ? 'No se pudo completar la operación.' : 'Error inesperado.';\n }\n}\n\nfunction isErrorBody(body: unknown): body is ApiErrorBody {\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n const error = (body as ApiErrorBody).error;\n return typeof error === 'object' && error !== null && typeof error.message === 'string';\n}\n\n/**\n * El backend responde SIEMPRE HTTP 200 y pone los errores en el body\n * (api.md §7), así que se detectan en el canal de **éxito** y se relanzan\n * como `ApiError` (con `fieldPath` cuando el backend envía `path`).\n * Los errores HTTP reales (red caída, proxy) también se normalizan.\n *\n * **R2.9 — paso dos de dos.** Éste archivo es el ÚNICO punto donde el\n * sobre de error se abre; lo que decide qué ocurre con un `AUTH_*` es\n * `AuthService.applyAuthCode` (la tabla de §4.5). Aquí sólo se le entrega\n * el código y se propaga el `ApiError` con él — un segundo sitio que\n * «interprete» el código sería una regla duplicada y, a la larga, una que\n * se contradiga con la primera.\n */\nexport const apiErrorInterceptor: HttpInterceptorFn = (req, next) => {\n const auth = inject(AuthService);\n\n return next(req).pipe(\n map((event) => {\n if (event instanceof HttpResponse && isErrorBody(event.body)) {\n const { status, message, path, code, errors } = event.body.error;\n // R2.9: ésta es la ÚNICA llamada a la tabla de §4.5.\n auth.applyAuthCode(code);\n // `errors[]` es el422 con rutas (D42): va entero al ApiError para que\n // `ErrorMapperService` lo convierta en `serverErrors[prefix + key]`.\n throw new ApiError(status, message, path, code, errors ?? []);\n }\n return event;\n }),\n catchError((err: unknown) => {\n if (err instanceof ApiError) {\n return throwError(() => err);\n }\n if (err instanceof HttpErrorResponse) {\n return throwError(() => new ApiError(err.status, httpMessage(err.status)));\n }\n return throwError(() => err);\n }),\n );\n};\n","/**\n * Resolución de marcadores de contexto en paths de endpoints.\n *\n * Plan `doc/context-sources-plan.md` · Fase 2. Lógica PURA: ni HTTP ni DI —\n * recibe un path y un mapa de getters de fuentes y devuelve el path ya\n * resuelto, o revienta.\n *\n * Reglas del contrato (R1–R4 del plan):\n *\n * - `{<fuente>}` / `{<fuente>.<ruta.anidada>}` → valor de la fuente en el\n * momento de la llamada (los getters se evalúan SOLO si hay marcadores).\n * - Cada valor se codifica como segmento de URL (`encodeURIComponent`).\n * - **Fail loud**: fuente no registrada, valor ausente/vacío, valor no\n * escalar o `{key}` sin resolver ⇒ error que nombra el path y el marcador.\n * Jamás se devuelve un segmento vacío ni el texto literal del marcador.\n * - Sin marcadores ⇒ el path vuelve intacto y ninguna fuente se evalúa.\n *\n * OJO con `interpolateTemplate` (R3): su semántica «ausente ⇒ cadena vacía»\n * es correcta para acciones y fuentes de datos, pero acá sería un 404 mudo.\n * Por eso la lectura y la validación viven en este fichero y ese helper no\n * se toca.\n */\n\n/** `{nombre}` o `{nombre.ruta.anidada}` — misma forma que usa el interpolador. */\nconst MARKER = /\\{([a-zA-Z0-9_.]+)\\}/g;\n\n/**\n * Resuelve los marcadores de `path` contra `sources`.\n *\n * @param path Path declarado en `ResourceEndpoints`, tal cual vino del schema.\n * @param sources Getters por nombre de fuente (`auth`, `config`, `valueSources`…).\n * @returns El path con cada marcador sustituido y codificado.\n * @throws Error nombrando path y marcador ante cualquier valor irresoluble.\n */\nexport function resolveEndpointPath(\n path: string,\n sources: Readonly<Record<string, () => unknown>>,\n): string {\n if (!path.includes('{')) return path;\n return path.replace(MARKER, (marker, nombre: string) =>\n sustituir(path, marker, nombre, sources),\n );\n}\n\n/** Sustituye UN marcador. `path` completo sólo viaja para el mensaje de error. */\nfunction sustituir(\n path: string,\n marker: string,\n nombre: string,\n sources: Readonly<Record<string, () => unknown>>,\n): string {\n if (nombre === 'key') {\n throw new Error(\n `[gas-admin-ui] path '${path}': ${marker} llegó sin resolver — ` +\n `la clave sólo la sustituyen get/update/remove antes de resolver el contexto.`,\n );\n }\n\n const punto = nombre.indexOf('.');\n const fuente = punto === -1 ? nombre : nombre.slice(0, punto);\n const getter = sources[fuente];\n if (typeof getter !== 'function') {\n throw new Error(\n `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — ` +\n `la fuente '${fuente}' no está registrada.`,\n );\n }\n\n // El getter se evalúa acá y sólo acá: si revienta, su error se propaga.\n const valor = punto === -1 ? getter() : leerRuta(getter(), nombre.slice(punto + 1));\n\n if (valor === undefined || valor === null) {\n throw new Error(\n `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — sin valor para '${nombre}'.`,\n );\n }\n if (typeof valor === 'object') {\n throw new Error(\n `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — ` +\n `el valor de '${fuente}' no es un segmento de URL (no es escalar).`,\n );\n }\n\n const texto = String(valor);\n if (texto === '') {\n throw new Error(\n `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — valor vacío en '${fuente}'.`,\n );\n }\n return encodeURIComponent(texto);\n}\n\n/** Lectura profunda `a.b.c` sobre el valor de la fuente; inexistente ⇒ `undefined`. */\nfunction leerRuta(valor: unknown, ruta: string): unknown {\n let actual: unknown = valor;\n for (const paso of ruta.split('.')) {\n if (actual === null || typeof actual !== 'object') return undefined;\n actual = (actual as Record<string, unknown>)[paso];\n }\n return actual;\n}\n","import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';\nimport { inject, Injector, Service } from '@angular/core';\nimport { from, map, Observable, switchMap, tap, throwError } from 'rxjs';\n\nimport { renewedTokenOf } from '../models/api.model';\nimport type { ResourceEndpoints } from '../models/schema.model';\nimport { API_DRIVER, API_URL, FIREBASE_AUTH_ADAPTER, VALUE_SOURCES } from '../tokens';\nimport type { ValueSources } from '../tokens';\nimport { resolveEndpointPath } from '../utils/context-resolver';\nimport { ConfigService } from './config.service';\nimport { AuthService } from './auth.service';\n\n/** Métodos que entiende el envelope del backend (baseapi §2.3). */\nexport type EnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/**\n * CRUD genérico sobre el contrato v2 (baseapi §2.3) con soporte dual-driver:\n *\n * - **`gas` (default)**: TODA llamada a la API es un `POST` a `API_URL` con cuerpo\n * `text/plain` y el JSON `{ token, method, path, payload }`.\n * - **`rest`**: Verbos HTTP reales (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) hacia\n * `${API_URL}${path}` con `Authorization: Bearer ${token}`. `payload` va como query\n * params en GET/DELETE (aplanando objetos anidados como `params`) o como body JSON\n * en POST/PUT/PATCH.\n */\n@Service()\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly apiUrl = inject(API_URL);\n private readonly auth = inject(AuthService);\n private readonly driver = inject(API_DRIVER, { optional: true }) ?? 'gas';\n /** Fuentes que aporta el consumidor (`provideGasAdmin({ valueSources })`). */\n private readonly valueSources: ValueSources = inject(VALUE_SOURCES, { optional: true }) ?? {};\n /** Adaptador opcional de Firebase Auth para refrescar el ID token antes de peticiones REST. */\n private readonly firebaseAuthAdapter = inject(FIREBASE_AUTH_ADAPTER, { optional: true });\n /**\n * Acceso PEREZOSO a `ConfigService` (R2 · plan doc/context-sources-plan.md).\n *\n * `config` es fuente construida del motor, pero inyectarla en el constructor\n * formaría un ciclo: `ConfigService` inyecta `ApiService` (opcional) para\n * cargar los singletons ⇒ NG0200 en el arranque. Se pide en el momento de la\n * petición, cuando ambos servicios ya están vivos. Si no existe (TestBed\n * parcial), devuelve `null` y el resolver falla ruidoso con «sin valor».\n */\n private readonly injector = inject(Injector);\n\n /**\n * Listado. El backend envuelve la respuesta cuando la operación declara\n * `shape.envelope: 'list'` (F5) ⇒ `{ items, nextCursor, total }` (R6.4);\n * la UI consume `T[]` en todos lados (F7-3) y este es el ÚNICO punto que\n * habla de listas, así que el desenvolvimiento vive acá. `nextCursor` es\n * el token de la página siguiente y `null` en la última — éste servicio\n * lo descarta hoy porque la lista no pagina todavía (backlog-ui-ux).\n * Un listado sin envelope (arreglo plano, el contrato histórico) pasa\n * tal cual.\n *\n * `payload` sólo lo usa una fuente declarada (F8 · R6.5): `{ view, params }`\n * pide el listado filtrado por una vista y sus parámetros. Sin él, la\n * llamada es la de siempre — la lectura llana del recurso.\n */\n list<T>(endpoint: ResourceEndpoints, payload?: Record<string, unknown>): Observable<T[]> {\n return this.send<unknown>(endpoint.list, 'GET', payload).pipe(map((raw) => unwrapList<T>(raw)));\n }\n\n /** `key` solo aplica a colecciones (`{key}` en la plantilla); los singleton no la llevan. */\n get<T>(endpoint: ResourceEndpoints, key?: string): Observable<T> {\n return this.send<T>(this.withKey(endpoint.get, key), 'GET');\n }\n\n create<T>(endpoint: ResourceEndpoints, payload: Record<string, unknown>): Observable<T> {\n return this.send<T>(endpoint.create, 'POST', payload);\n }\n\n update<T>(\n endpoint: ResourceEndpoints,\n key: string | undefined,\n payload: Record<string, unknown>,\n ): Observable<T> {\n return this.send<T>(this.withKey(endpoint.update, key), 'PUT', payload);\n }\n\n remove(endpoint: ResourceEndpoints, key?: string): Observable<void> {\n return this.send<void>(this.withKey(endpoint.remove, key), 'DELETE');\n }\n\n /**\n * Operación genérica sobre una ruta ya resuelta: reorder de colección\n * (`PUT /admin/x` con `{ reorder: … }`), `/admin/schema`,\n * `/admin/upload-signature`, etc.\n */\n request<T>(\n method: EnvelopeMethod,\n path: string | undefined,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n return this.send<T>(path, method, payload);\n }\n\n private send<T>(\n path: string | undefined,\n method: EnvelopeMethod,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n if (!path) {\n return throwError(() => new Error('Operación no disponible para este recurso'));\n }\n\n // Resolución de marcadores de contexto ANTES del despacho (plan Fase 3):\n // un solo punto para todas las operaciones y para ambos drivers. Sin\n // marcadores el path vuelve intacto y ninguna fuente se evalúa.\n let resolvedPath: string;\n try {\n resolvedPath = resolveEndpointPath(path, this.buildSources());\n } catch (error) {\n // Fail loud por el CANAL de errores (no como throw síncrono): el\n // consumidor lo trata como cualquier otro fallo de petición y JAMÁS\n // sale un request con un segmento vacío o el marcador literal.\n return throwError(() => error);\n }\n\n const request$ =\n this.driver === 'rest'\n ? this.sendRest<T>(resolvedPath, method, payload)\n : this.sendGas<T>(resolvedPath, method, payload);\n\n return request$.pipe(\n // Renovación deslizante (§3.2): si el back adjuntó `meta.renewedToken`\n // se adopta YA, con lo que la siguiente petición ya va firmada nueva.\n tap((res) => {\n const renewed = renewedTokenOf(res);\n if (renewed !== null) this.auth.applyRenewedToken(renewed);\n }),\n );\n }\n\n /**\n * Fuentes de contexto de ESTA petición: getters, no valores — se evalúan\n * sólo si el path trae marcadores.\n *\n * Las construidas del motor (`auth`, `config`) van ÚLTIMAS: el motor es\n * dueño de sus fuentes y el registro del consumidor no las pisa.\n */\n private buildSources(): Readonly<Record<string, () => unknown>> {\n return {\n ...this.valueSources,\n auth: () => ({ user: this.auth.user(), token: this.auth.token() }),\n config: () => this.injector.get(ConfigService, null)?.globalConfig() ?? null,\n };\n }\n\n private sendGas<T>(\n path: string,\n method: EnvelopeMethod,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n // Token de la SESIÓN, leído en cada petición (R2.8): no hay credencial\n // fija que inyectar en el arranque.\n const body = { token: this.auth.token(), method, path, payload: payload ?? {} };\n return this.http.post<T>(this.apiUrl, body, { headers: { 'Content-Type': 'text/plain' } });\n }\n\n private sendRest<T>(\n path: string,\n method: EnvelopeMethod,\n payload?: Record<string, unknown>,\n ): Observable<T> {\n const url = this.buildUrl(path);\n const headers = new HttpHeaders();\n\n // Resuelve el token ANTES de armar la cabecera Authorization:\n // si el adaptador de Firebase provee `getIdToken()`, se llama para obtener\n // un ID token fresco (evita \"ID token inválido\" tras la expiración de 1 hora).\n // Si no provee `getIdToken()`, se usa el token de sesión almacenado (fallback).\n // Cualquier error de `getIdToken()` se propaga por el canal de errores:\n // nunca se envía una petición con un token inválido ni se falla silenciosamente.\n if (this.firebaseAuthAdapter?.getIdToken) {\n return from(this.firebaseAuthAdapter.getIdToken()).pipe(\n switchMap((freshToken) =>\n this.doRestRequest<T>(\n url,\n method,\n payload,\n headers.set('Authorization', `Bearer ${freshToken}`),\n ),\n ),\n );\n }\n\n const token = this.auth.token();\n const finalHeaders = token ? headers.set('Authorization', `Bearer ${token}`) : headers;\n return this.doRestRequest<T>(url, method, payload, finalHeaders);\n }\n\n private doRestRequest<T>(\n url: string,\n method: EnvelopeMethod,\n payload: Record<string, unknown> | undefined,\n headers: HttpHeaders,\n ): Observable<T> {\n if (method === 'GET' || method === 'DELETE') {\n const params = toHttpParams(payload);\n return this.http.request<T>(method, url, { headers, params });\n }\n\n return this.http.request<T>(method, url, {\n headers: headers.set('Content-Type', 'application/json'),\n body: payload ?? {},\n });\n }\n\n private buildUrl(path: string): string {\n const base = this.apiUrl ? this.apiUrl.replace(/\\/+$/, '') : '';\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return `${base}${cleanPath}`;\n }\n\n private withKey(path: string | undefined, key?: string): string | undefined {\n if (!path) {\n return undefined;\n }\n return path.includes('{key}') ? path.replace('{key}', encodeURIComponent(key ?? '')) : path;\n }\n}\n\n/**\n * Convierte un payload en HttpParams para peticiones GET y DELETE en modo REST.\n * Si un valor es un objeto plano (ej: `{ view: 'calendar', params: { orden: 'es' } }`),\n * lo aplana un nivel para que sus claves viajen como query params reales\n * en lugar de serializarse como `params=[object Object]`.\n */\nfunction toHttpParams(payload?: Record<string, unknown>): HttpParams {\n let params = new HttpParams();\n if (!payload) {\n return params;\n }\n\n for (const [key, value] of Object.entries(payload)) {\n if (value === undefined || value === null) {\n continue;\n }\n if (typeof value === 'object' && !Array.isArray(value)) {\n for (const [nestedKey, nestedValue] of Object.entries(value as Record<string, unknown>)) {\n if (nestedValue !== undefined && nestedValue !== null) {\n params = params.set(nestedKey, String(nestedValue));\n }\n }\n } else if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null) {\n params = params.append(key, String(item));\n }\n }\n } else {\n params = params.set(key, String(value));\n }\n }\n return params;\n}\n\n/** `{ items, nextCursor, total }` (F5/R6.4) → `T[]`; arreglo plano → tal cual; otro ⇒ []. */\nfunction unwrapList<T>(raw: unknown): T[] {\n if (Array.isArray(raw)) {\n return raw as T[];\n }\n if (raw !== null && typeof raw === 'object') {\n const items = (raw as { items?: unknown }).items;\n if (Array.isArray(items)) {\n return items as T[];\n }\n }\n return [];\n}\n","import { inject, Service, signal } from '@angular/core';\nimport { catchError, firstValueFrom, forkJoin, of } from 'rxjs';\n\nimport { ApiService } from './api.service';\nimport type { ResourceEndpoints } from '../models/schema.model';\n\n/**\n * Servicio global de configuración: reúne los singletons declarativos y los\n * deja disponibles para interpolación de plantillas (enlaces públicos,\n * mensajes para compartir).\n *\n * Es genérico a propósito: NO conoce ningún singleton concreto. Los\n * endpoints los inyecta `app.config.ts` con `provideAppInitializer`,\n * derivándolos del registry — agregar un singleton de configuración nuevo\n * = declarar su schema, sin tocar este fichero (refactormotor.md §1 — el\n * schema es el único plano de control).\n *\n * No hay efectos en el constructor: la carga es explícita (`load`), así un\n * TestBed que sólo provee un ApiService parcial no recibe una llamada\n * síncrona a `get` durante la construcción.\n */\n@Service()\nexport class ConfigService {\n private readonly api = inject(ApiService, { optional: true });\n\n /** Mapa reactivo de configuración global (p. ej. `public_base_url`). */\n readonly globalConfig = signal<Record<string, unknown>>({\n // Fallback genérico: el origen actual. Lo que declare el singleton\n // (p. ej. `share_message_template`) lo pone el backend, no el front.\n public_base_url: typeof window !== 'undefined' ? window.location.origin : '',\n });\n\n /** Endpoints conocidos; los guarda `load` para que `ensureLoaded` pueda reintentar. */\n private endpoints: readonly ResourceEndpoints[] = [];\n\n /**\n * `true` cuando la última carga terminó SIN fallos. Si algún singleton\n * falló (o ninguno respondió), queda `false` y el próximo punto crítico\n * vuelve a intentar: no hay estado «cargado a medias para siempre».\n */\n private loaded = false;\n\n /** Carga en curso: `ensureLoaded` la reutiliza en vez de duplicar viajes. */\n private inFlight: Promise<void> | null = null;\n\n /**\n * Carga todos los endpoints de configuración recibidos. Silenciosa: un\n * singleton que falle o que devuelva vacío no rompe nada, simplemente no\n * aporta claves (y deja `loaded = false` para poder reintentar).\n */\n load(endpoints: readonly ResourceEndpoints[]): void {\n this.endpoints = endpoints;\n void this.ensure();\n }\n\n /**\n * Garantiza la config global antes de un punto que la necesita.\n *\n * El load del bootstrap NO es bloqueante y sólo intenta una vez: si la app\n * arrancó sin sesión (el 401 de los singletons se traga en el `catchError`)\n * o la red tardó, la sesión seguía con la config vacía y, por ejemplo, el\n * mensaje de `copy-share` degradaba a «sólo la URL» (R8.7). Acá el reintento\n * corre con la sesión ya iniciada y responde bien; si ya se cargó, es un\n * no-op síncrono.\n */\n ensureLoaded(): Promise<void> {\n return this.ensure();\n }\n\n private ensure(): Promise<void> {\n if (this.loaded) return Promise.resolve();\n if (this.inFlight) return this.inFlight;\n if (!this.api || typeof this.api.get !== 'function' || this.endpoints.length === 0) {\n return Promise.resolve();\n }\n\n this.inFlight = firstValueFrom(\n forkJoin(\n this.endpoints.map((endpoint) =>\n this.api!.get<Record<string, unknown>>(endpoint).pipe(\n // `null` marca el fallo: con `of({})` un singleton caído era\n // indistinguible de uno realmente vacío y la carga quedaría\n // «hecha» sin datos.\n catchError(() => of(null)),\n ),\n ),\n ),\n ).then((blocks) => {\n this.inFlight = null;\n blocks.forEach((data) => {\n if (data && typeof data === 'object' && Object.keys(data).length > 0) {\n this.globalConfig.update((current) => ({ ...current, ...data }));\n }\n });\n this.loaded = blocks.every((data) => data !== null);\n });\n\n return this.inFlight;\n }\n\n /**\n * Contexto de datos para interpolar plantillas del schema\n * (`urlTemplate`, `shareTextTemplate`, `postCreate.description`, …).\n *\n * Sólo aporta datos y la base pública — la FORMA de la URL pública\n * (`/p/{ref}`) la declara el propio schema en su `urlTemplate`; este\n * servicio no decide cómo se arma el enlace.\n */\n buildInterpolationContext(record: Record<string, unknown>): Record<string, unknown> {\n const config = this.globalConfig();\n const baseUrl = String(config['public_base_url'] || window.location.origin).replace(/\\/+$/, '');\n\n return {\n ...config,\n ...record,\n public_base_url: baseUrl,\n };\n }\n}\n","import {\n EnvironmentProviders,\n InjectionToken,\n Provider,\n inject,\n provideAppInitializer,\n} from '@angular/core';\nimport { provideHttpClient, withInterceptors } from '@angular/common/http';\n\nimport { apiErrorInterceptor } from '../interceptors/api-error.interceptor';\nimport { AuthService } from '../services/auth.service';\nimport { ConfigService } from '../services/config.service';\nimport type { AuthSchema, ResourceSchema } from '../models/schema.model';\nimport type { FirebaseAuthAdapter } from '../adapters/firebase-auth.adapter';\nimport { API_DRIVER, FIREBASE_AUTH_ADAPTER, VALUE_SOURCES } from '../tokens';\nimport type { ValueSources } from '../tokens';\n\n\n/**\n * Catálogo de schemas del consumidor (F1 · G-F2).\n *\n * Antes de la extracción el motor importaba `getSchema` del\n * `schemas/registry.ts` de la app. Ese acoplamiento se rompe con un\n * catálogo a nivel módulo: el consumidor llama\n * `provideGasAdmin({ schemas, auth })` en su `app.config.ts` y el motor\n * resuelve en runtime con la MISMA semántica que antes (un solo catálogo\n * por aplicación, seteado una vez en el bootstrap).\n *\n * `provideGasAdmin` se ejecuta al evaluarse el `app.config` (módulo), y\n * todo uso del catálogo (guards, componentes, rutas dinámicas) ocurre en\n * runtime posterior al bootstrap — el orden es determinístico.\n */\n\nlet catalog: readonly ResourceSchema[] = [];\nlet authCatalog: AuthSchema | undefined;\n\n/** Fija el catálogo (uso interno de `provideGasAdmin`; testable directo). */\nexport function setSchemaCatalog(\n schemas: readonly ResourceSchema[],\n auth?: AuthSchema,\n): void {\n catalog = schemas;\n authCatalog = auth;\n}\n\n/** Busca un schema por `id`; `undefined` si no existe. */\nexport function getSchema(id: string): ResourceSchema | undefined {\n return catalog.find((schema) => schema.id === id);\n}\n\n/** Catálogo completo en orden (menú lateral). */\nexport function allSchemas(): readonly ResourceSchema[] {\n return catalog;\n}\n\n/** Declaración de la pantalla de login; `undefined` sin `auth` en config. */\nexport function getAuth(): AuthSchema | undefined {\n return authCatalog;\n}\n\n/** Token DI del catálogo (para inyectarlo donde un `inject()` es posible). */\nexport const GAS_SCHEMAS = new InjectionToken<readonly ResourceSchema[]>(\n 'gas.admin.schemas',\n);\n\n/** Configuración del motor. */\nexport interface GasAdminConfig {\n /** Catálogo ordenado de recursos (menú + rutas + resolución `getSchema`). */\n schemas: readonly ResourceSchema[];\n /** Declaración de la pantalla de login (opcional: sin ella, sin `/login`). */\n auth?: AuthSchema;\n /**\n * Driver de transporte HTTP.\n *\n * - `'gas'` (default): tunnel `POST text/plain` con envelope Apps Script.\n * - `'rest'`: verbos HTTP reales con `Authorization: Bearer` en cabeceras.\n *\n * Sin declaración el motor usa `'gas'` — cero impacto en consumidores existentes.\n */\n driver?: 'gas' | 'rest';\n /**\n * Fuentes de contexto que el consumidor aporta para marcadores en paths de\n * endpoints: `{ <fuente> }` / `{ <fuente>.<ruta> }` se resuelven en el\n * momento de la petición contra este registro más las fuentes construidas\n * `auth` (sesión) y `config` (config global).\n *\n * Opcional: sin él, sólo existen `auth` y `config`. Declararlo no cambia\n * ningún path que no lleve marcadores — cero impacto en schemas existentes.\n */\n valueSources?: ValueSources;\n}\n\n/**\n * Wiring completo del motor: catálogo + HTTP con el interceptor de\n * errores del envelope + loaders de config y sesión en el bootstrap\n * (antes en el `app.config` del consumidor).\n *\n * El consumidor conserva `provideRouter` y `{ provide: API_URL, ... }`\n * (su environment decide la URL del Web App).\n */\nexport function provideGasAdmin(\n config: GasAdminConfig,\n): (Provider | EnvironmentProviders)[] {\n setSchemaCatalog(config.schemas, config.auth);\n // Singletons de configuración: los declara el catálogo, no el servicio.\n // Un singleton nuevo con `endpoint.get` se carga solo.\n const configEndpoints = () =>\n config.schemas.flatMap((schema) =>\n schema.kind === 'singleton' && schema.endpoint?.get\n ? [{ get: schema.endpoint.get }]\n : [],\n );\n return [\n { provide: GAS_SCHEMAS, useValue: config.schemas },\n // Driver de transporte: 'gas' por defecto para cero breaking changes.\n { provide: API_DRIVER, useValue: config.driver ?? 'gas' },\n // Fuentes de contexto del consumidor: vacío por defecto ⇒ sólo `auth` y\n // `config`, que el motor arma por su cuenta en cada petición.\n { provide: VALUE_SOURCES, useValue: config.valueSources ?? {} },\n // Sin `authInterceptor` en modo GAS: el token viaja en el cuerpo del envelope\n // (baseapi §2.3, criterio 11: ninguna cabecera custom).\n // En modo REST el token viaja como cabecera `Authorization` dentro de ApiService.\n provideHttpClient(withInterceptors([apiErrorInterceptor])),\n // Carga de config en bootstrap (fuera del constructor del servicio):\n // un singleton que falle no rompe el arranque.\n provideAppInitializer(() => inject(ConfigService).load(configEndpoints())),\n // Sesión (R2.12): revalida contra `/auth/me` el token que restauró el\n // AuthService de `sessionStorage`. Corre ANTES del primer guard.\n provideAppInitializer(() => inject(AuthService).restore()),\n ];\n}\n\n/**\n * Registra una implementación concreta de FirebaseAuthAdapter en el árbol de DI.\n * Usar junto con provideGasAdmin() en app.config.ts al configurar proveedores federados\n * (como 'google') o autenticación por Firebase.\n *\n * @example\n * // app.config.ts\n * provideGasAdmin({ schemas, auth }),\n * provideFirebaseAdapter(new MyFirebaseAuthAdapter(getAuth(firebaseApp))),\n */\nexport function provideFirebaseAdapter(adapter: FirebaseAuthAdapter): Provider {\n return { provide: FIREBASE_AUTH_ADAPTER, useValue: adapter };\n}\n\n"],"mappings":"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"}
|