@tinya-code/gas-admin-ui 0.2.0-alpha.1 → 0.2.0-alpha.3

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.
Files changed (38) hide show
  1. package/README.md +115 -1
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs → tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs} +4 -4
  3. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map → tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs.map} +1 -1
  4. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs → tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs} +2 -2
  5. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs.map} +1 -1
  6. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs → tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs} +2 -2
  7. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map → tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs → tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs.map} +1 -1
  10. package/fesm2022/{tinya-code-gas-admin-ui-file-B6_vveO5.mjs → tinya-code-gas-admin-ui-file-DhLi3eO0.mjs} +3 -3
  11. package/fesm2022/{tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map → tinya-code-gas-admin-ui-file-DhLi3eO0.mjs.map} +1 -1
  12. package/fesm2022/{tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs → tinya-code-gas-admin-ui-form-view-CjC-GOQl.mjs} +9 -9
  13. package/fesm2022/{tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs.map → tinya-code-gas-admin-ui-form-view-CjC-GOQl.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-image-lUe8nrUf.mjs → tinya-code-gas-admin-ui-image-D9-mpBhA.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-image-lUe8nrUf.mjs.map → tinya-code-gas-admin-ui-image-D9-mpBhA.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs → tinya-code-gas-admin-ui-kanban-view-B4u2ibPb.mjs} +4 -4
  17. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs.map → tinya-code-gas-admin-ui-kanban-view-B4u2ibPb.mjs.map} +1 -1
  18. package/fesm2022/{tinya-code-gas-admin-ui-list-CH7b7dKh.mjs → tinya-code-gas-admin-ui-list-CQpL6vyE.mjs} +2 -2
  19. package/fesm2022/{tinya-code-gas-admin-ui-list-CH7b7dKh.mjs.map → tinya-code-gas-admin-ui-list-CQpL6vyE.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs → tinya-code-gas-admin-ui-multiselect-CTQPGBOK.mjs} +3 -3
  21. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs.map → tinya-code-gas-admin-ui-multiselect-CTQPGBOK.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs → tinya-code-gas-admin-ui-relation-lr_XG6jt.mjs} +3 -3
  23. package/fesm2022/{tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs.map → tinya-code-gas-admin-ui-relation-lr_XG6jt.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs → tinya-code-gas-admin-ui-render-registry-CcIgBsSl.mjs} +10 -10
  25. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs.map → tinya-code-gas-admin-ui-render-registry-CcIgBsSl.mjs.map} +1 -1
  26. package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs → tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs} +63 -8
  27. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs.map +1 -0
  28. package/fesm2022/{tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs → tinya-code-gas-admin-ui-seed-Bn6tl-yM.mjs} +2 -2
  29. package/fesm2022/{tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs.map → tinya-code-gas-admin-ui-seed-Bn6tl-yM.mjs.map} +1 -1
  30. package/fesm2022/{tinya-code-gas-admin-ui-select-CL2kj7pM.mjs → tinya-code-gas-admin-ui-select-BwefyMVx.mjs} +3 -3
  31. package/fesm2022/{tinya-code-gas-admin-ui-select-CL2kj7pM.mjs.map → tinya-code-gas-admin-ui-select-BwefyMVx.mjs.map} +1 -1
  32. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs → tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs} +2 -2
  33. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs.map → tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs.map} +1 -1
  34. package/fesm2022/tinya-code-gas-admin-ui.mjs +58 -11
  35. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/types/tinya-code-gas-admin-ui.d.ts +125 -40
  38. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs.map +0 -1
package/README.md CHANGED
@@ -113,9 +113,123 @@ When `driver: 'rest'` is active:
113
113
  - **`POST /api/v1/auth/login`**: body is `{ email, password }`, returns `{ token, user }`.
114
114
  - **`GET /api/v1/auth/me`**: requires `Authorization: Bearer <token>`, returns `{ user }`.
115
115
  - **`POST /api/v1/auth/logout`**: requires `Authorization: Bearer <token>`.
116
+ - **Parameterized paths (context markers)**: the engine resolves markers such as `{auth.user.id}` client-side *before* sending, so the backend just declares the real route — e.g. `GET /api/v1/businesses/:idBusiness/orders` matches an endpoint declared as `/businesses/{auth.user.id}/orders`. See **Dynamic Values in Endpoint Paths** below.
116
117
 
117
118
  `driver` defaults to `'gas'` if omitted. Existing Apps Script projects do not require any configuration changes.
118
119
 
120
+ ### Dynamic Values in Endpoint Paths (context sources)
121
+
122
+ Standard REST backends (NestJS, Express, FastAPI) often route by an id in the path — `/businesses/:idBusiness/orders`. Declare the value as a **context marker** in the endpoint and the engine resolves it right before the request leaves. It works for both drivers; endpoints without markers are untouched.
123
+
124
+ #### Marker syntax
125
+
126
+ | Marker | Resolves from | Example |
127
+ | --- | --- | --- |
128
+ | `{key}` | The record key of the current detail/edit operation (unchanged behavior) | `/orders/{key}` → `/orders/ord-7` |
129
+ | `{auth.<path>}` | The session: the `user` payload and `token` your backend returned at login | `{auth.user.id}` → `u_1` |
130
+ | `{config.<path>}` | The global config map, merged from your `singleton` schemas at bootstrap | `{config.public_base_url}` → `https://…` |
131
+ | `{<source>}` / `{<source>.<path>}` | Any source you register through `valueSources` | `{tenant}` → `acme` |
132
+
133
+ Every substituted value is URL-encoded per segment, exactly like `{key}`. Nested paths work (`{auth.user.id}`), and markers can be combined in one path and mixed with `{key}`.
134
+
135
+ #### Built-in sources
136
+
137
+ - **`auth`** — the session held by `AuthService`. Any field your backend includes in the login / `/auth/me` payload is reachable: the library stores the whole `user` object, so nothing new has to be wired for a field your backend already sends.
138
+ - **`config`** — the merged response of every `singleton` schema loaded at bootstrap.
139
+
140
+ #### Register your own source
141
+
142
+ ```ts
143
+ // src/app/app.config.ts
144
+ import { provideGasAdmin } from '@tinya-code/gas-admin-ui';
145
+
146
+ provideGasAdmin({
147
+ schemas,
148
+ auth: authSchema,
149
+ driver: 'rest',
150
+ valueSources: {
151
+ // Name → getter evaluated at request time (not at registration).
152
+ tenant: () => myStore.currentTenantId(),
153
+ },
154
+ });
155
+ ```
156
+
157
+ Then any endpoint can use it:
158
+
159
+ ```ts
160
+ // schemas/orders.schema.ts
161
+ endpoint: {
162
+ list: '/tenants/{tenant}/businesses/{auth.user.id}/orders',
163
+ get: '/tenants/{tenant}/businesses/{auth.user.id}/orders/{key}',
164
+ update: '/tenants/{tenant}/businesses/{auth.user.id}/orders/{key}',
165
+ }
166
+ ```
167
+
168
+ #### Fail loud, never half-filled
169
+
170
+ A marker that cannot resolve surfaces as an error in the request's **error channel** — the same place any request failure already lands. The engine never sends a URL with an empty segment or with the literal marker text.
171
+
172
+ | Situation | Result |
173
+ | --- | --- |
174
+ | Source not registered | Error naming the path and the marker; **no request is sent** |
175
+ | Session/config field missing or empty | Error naming the path and the marker; **no request is sent** |
176
+ | A registered getter throws | Its error propagates as-is |
177
+ | `{key}` left unresolved (`list`, `create`) | Error explaining that `key` applies to `get`/`update`/`remove` only |
178
+
179
+ #### Notes
180
+
181
+ - Resolution covers every operation — `list`, `get`, `create`, `update`, `remove` and custom `request` — and both drivers.
182
+ - Built-in sources win: `valueSources` cannot shadow `auth` or `config`.
183
+ - Nothing changes for existing schemas: a path without markers produces a byte-identical request.
184
+
185
+ ### Firebase ID Token Refresh (REST driver)
186
+
187
+ Firebase ID tokens expire after **1 hour**. When `driver: 'rest'` is active, `gas-admin-ui` calls your adapter's optional `getIdToken()` before every REST request so the `Authorization: Bearer <token>` header always carries a fresh token.
188
+
189
+ ```ts
190
+ // src/app/auth/firebase-auth.adapter.ts
191
+ import type { FirebaseAuthAdapter } from '@tinya-code/gas-admin-ui';
192
+ import { getAuth } from 'firebase/auth';
193
+
194
+ export class AppFirebaseAuthAdapter implements FirebaseAuthAdapter {
195
+ private readonly auth = getAuth();
196
+
197
+ async signInWithGoogle(): Promise<string> { /* ... */ }
198
+ async signInWithEmailAndPassword(email: string, password: string): Promise<string> { /* ... */ }
199
+ async signOut(): Promise<void> { /* ... */ }
200
+
201
+ /** Returns a fresh Firebase ID token before each REST request. */
202
+ async getIdToken(): Promise<string> {
203
+ const user = this.auth.currentUser;
204
+ if (!user) throw new Error('No Firebase user signed in');
205
+ return user.getIdToken(true); // force refresh if expired or near-expiry
206
+ }
207
+ }
208
+ ```
209
+
210
+ ```ts
211
+ // src/app/app.config.ts
212
+ import { provideFirebaseAdapter } from '@tinya-code/gas-admin-ui';
213
+ import { AppFirebaseAuthAdapter } from './auth/firebase-auth.adapter';
214
+
215
+ export const appConfig = {
216
+ providers: [
217
+ // ...
218
+ provideFirebaseAdapter(new AppFirebaseAuthAdapter()),
219
+ ],
220
+ };
221
+ ```
222
+
223
+ **Behavior:**
224
+
225
+ | Adapter implements `getIdToken()`? | Result |
226
+ |---|---|
227
+ | Yes | Called before every REST request; returned value used as `Authorization: Bearer <token>` |
228
+ | No | Current stored session token is used (unchanged behavior) |
229
+ | `getIdToken()` throws | Error surfaces in the request's error channel; **no request is sent** |
230
+
231
+ This only affects `driver: 'rest'`. The GAS driver continues sending the session token inside the envelope exactly as today.
232
+
119
233
  ### 4. Declare routes — `app.routes.ts`
120
234
 
121
235
  ```ts
@@ -633,7 +747,7 @@ Only what `public-api.ts` exports is stable contract; everything else is private
633
747
  | Group | Exports |
634
748
  | --- | --- |
635
749
  | Wiring & catalog | `provideGasAdmin`, `provideFirebaseAdapter`, `GAS_SCHEMAS`, `setSchemaCatalog`, `getSchema`, `allSchemas`, `getAuth`, type `GasAdminConfig` |
636
- | Tokens & adapters | `API_URL`, `API_DRIVER`, `FIREBASE_AUTH_ADAPTER`, type `FirebaseAuthAdapter` |
750
+ | Tokens & adapters | `API_URL`, `API_DRIVER`, `VALUE_SOURCES`, `FIREBASE_AUTH_ADAPTER`, type `FirebaseAuthAdapter`, type `ValueSources` |
637
751
  | Routes & guard | `authGuard`, `shellRoutes`, `resourceRoutes` |
638
752
  | Pages | `Login`, `Dashboard`, `ListView`, `FormView`, `ResourcePage` |
639
753
  | Components | `Sidebar`, `ComposerHost`, `FieldHost`, `ConfirmDialog`, `Toast` |
@@ -1,7 +1,7 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
2
2
  import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
3
- import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs";
4
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs";
3
+ import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs";
4
+ import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs";
5
5
  import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
6
6
  import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
7
7
  import * as i0 from "@angular/core";
@@ -1019,4 +1019,4 @@ i0.ɵɵngDeclareClassMetadata({
1019
1019
  });
1020
1020
  export { CalendarView };
1021
1021
 
1022
- //# sourceMappingURL=tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map
1022
+ //# sourceMappingURL=tinya-code-gas-admin-ui-calendar-view-DkYfP4n9.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.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
+ {"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,4 +1,4 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
2
2
  import * as i0 from "@angular/core";
3
3
  import { Injectable, inject } from "@angular/core";
4
4
  import { Router } from "@angular/router";
@@ -98,4 +98,4 @@ function contains(actual, expected) {
98
98
  }
99
99
  export { ViewNavigationService as n, evaluateCondition as t };
100
100
 
101
- //# sourceMappingURL=tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map
101
+ //# sourceMappingURL=tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/view-navigation/view-navigation.service.ts","../../../projects/gas-admin-ui/src/lib/shared/utils/condition-evaluator.ts"],"sourcesContent":["import { Injectable, inject } from '@angular/core';\nimport { Router, type ActivatedRouteSnapshot } from '@angular/router';\n\nimport type { NavigationTarget, SelectionBinding } from '../models/schema.model';\nimport { getSchema } from '../registry/schema-catalog';\nimport type { SelectionEvent } from '../../views/composer/view-context';\n\n/**\n * R9.9 · D4 — Servicio de navegación declarativa.\n *\n * Un leaf (hoja) sólo NAVEGA cuando hay `select` declarado; el motor vive\n * aquí, no en cada hoja. El flujo completo es:\n *\n * `ctx.emit/leaf → dispatch → buildSegments(pattern, {key}) →\n * router.navigate(matriz de comandos) → roleGuard`\n *\n * Invariantes:\n *\n * - **key-only**: la ÚNICA identidad que viaja es `event.key`. Los comandos\n * son una matriz de segmentos (nunca concat de strings) ⇒ el Router\n * codifica la clave (una clave `A/B` queda `A%2FB` en la URL) y ningún\n * dato del registro llega a la URL.\n * - **Fail closed**: sin binding, con acción distinta a la declarada o con\n * `mode` distinto de `route`, NO hay navegación — simplemente no ocurre.\n * - **Fail loud**: un target que el registry no declara lanza ruido antes de\n * navegar (el validador pre-render ya lo garantiza; esto es defensa en\n * profundidad).\n * - **Guards reales**: `router.navigate` ⇒ `roleGuard` corre igual que en\n * cualquier navegación declarada. Si el guard bloquea, no se registra\n * origen (no hay «atrás» que una sesión sin permiso deba volver a ver).\n */\n\n/**\n * Patrón de segmentos de la ruta destino (`['orders', ':key']`). Usa la\n * MISMA fuente que el validador (`getSchema`) para que lo validado en build\n * sea exactamente lo navegado en runtime. Inexistente ⇒ `undefined` (la\n * defensa en profundidad: el validador pre-render garantiza que esto no\n * ocurre en schemas válidos).\n */\nexport function targetRoutePattern(target: NavigationTarget): string[] | undefined {\n const destino = getSchema(target.resource);\n const vista = destino?.uiViews?.[target.view];\n if (!destino || !vista) return undefined;\n const relativa = vista.path ?? '';\n return [destino.id, ...(relativa === '' ? [] : relativa.split('/'))];\n}\n\n/**\n * Segmentos de la ruta destino con la clave sustituida. La clave es SIEMPRE\n * un segmento completo (no se parte en `/`); un param distinto de `:key` no\n * tiene fuente declarada ⇒ falla ruido, no navega a medias.\n */\nexport function buildSegments(pattern: readonly string[], key: string): string[] {\n const commands: string[] = [];\n for (const segmento of pattern) {\n if (segmento.startsWith(':')) {\n const nombre = segmento.slice(1);\n if (nombre !== 'key') {\n throw new Error(`Destino ':${nombre}' sin fuente declarada en SelectionEvent`);\n }\n commands.push(key);\n } else {\n commands.push(segmento);\n }\n }\n return commands;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class ViewNavigationService {\n private readonly router = inject(Router);\n\n /** URL desde la que se navegó la última selección válida (origen del «atrás»). */\n private origen: string | undefined;\n\n /**\n * Transición declarada de un leaf. Inerte si no hay binding, si la acción\n * no es la declarada o si el modo no es `route`; falla ruido si el target\n * no existe en el registry.\n *\n * @returns `true` si el Router completó la navegación (guards aceptados).\n */\n async dispatch(binding: SelectionBinding | undefined, event: SelectionEvent): Promise<boolean> {\n if (binding === undefined) return false; // sin declaración ⇒ inerte\n if (binding.on !== event.action) return false; // fuera del scope declarado\n if (binding.mode !== 'route') return false; // comportamiento local del leaf\n\n const pattern = targetRoutePattern(binding.target);\n if (pattern === undefined) {\n throw new Error(\n `Destino '${binding.target.resource}/${binding.target.view}' no existe en el registry`,\n );\n }\n\n const urlOrigen = this.router.url;\n const ok = await this.router.navigate(['/', ...buildSegments(pattern, event.key)]);\n if (ok) this.origen = urlOrigen;\n return ok;\n }\n\n /**\n * Forma de volver (D4). Orden: `returnPath` declarado por el leaf ⇒\n * origen registrado por `dispatch` ⇒ raíz del recurso declarado en la ruta\n * actual (entrada directa a la URL: no hay origen en memoria). Fallback\n * final: la raíz de la aplicación.\n */\n async back(returnPath?: string): Promise<boolean> {\n if (returnPath !== undefined) return this.router.navigate([returnPath]);\n\n if (this.origen !== undefined) {\n const destino = this.origen;\n this.origen = undefined;\n return this.router.navigateByUrl(destino);\n }\n\n const recurso = this.recursoActual();\n return recurso === undefined\n ? this.router.navigate(['/'])\n : this.router.navigate(['/', recurso]);\n }\n\n /** Primer `resourceId` declarado en el árbol de rutas activo. */\n private recursoActual(): string | undefined {\n let nodo: ActivatedRouteSnapshot | null = this.router.routerState.snapshot.root;\n while (nodo !== null) {\n const id = nodo.data?.['resourceId'];\n if (id !== undefined) return String(id);\n nodo = nodo.firstChild;\n }\n return undefined;\n }\n}\n","import type { FieldCondition } from '../../core/models/schema.model';\n\n/**\n * Evaluador de condiciones `visibleWhen` / `readonlyWhen` (base.md §4):\n * comparaciones simples (igual, distinto, está vacío, contiene) sobre otros\n * campos del mismo nivel o del registro raíz.\n *\n * Nota de arquitectura: la importación de `FieldCondition` es SOLO de tipo\n * (se elimina en compilación) — no hay dependencia de runtime con `core`.\n */\n\n/** Resuelve una condición. Se resuelve contra el nivel propio; si la clave\n * no existe ahí, contra la raíz del registro. */\nexport function evaluateCondition(\n condition: FieldCondition,\n siblingValues: Record<string, unknown>,\n rootValues: Record<string, unknown> = {},\n): boolean {\n const source = condition.field in siblingValues ? siblingValues : rootValues;\n const actual = source[condition.field];\n const expected = condition.value;\n\n switch (condition.operator) {\n case 'eq':\n return actual === expected;\n case 'ne':\n return actual !== expected;\n case 'empty':\n return isEmpty(actual);\n case 'contains':\n return contains(actual, expected);\n }\n}\n\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null || value === '') {\n return true;\n }\n if (Array.isArray(value)) {\n return value.length === 0;\n }\n return false;\n}\n\nfunction contains(actual: unknown, expected: unknown): boolean {\n if (typeof actual === 'string') {\n return actual.includes(String(expected));\n }\n if (Array.isArray(actual)) {\n return actual.includes(expected);\n }\n return false;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-condition-evaluator-C1tjeb6D.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/view-navigation/view-navigation.service.ts","../../../projects/gas-admin-ui/src/lib/shared/utils/condition-evaluator.ts"],"sourcesContent":["import { Injectable, inject } from '@angular/core';\nimport { Router, type ActivatedRouteSnapshot } from '@angular/router';\n\nimport type { NavigationTarget, SelectionBinding } from '../models/schema.model';\nimport { getSchema } from '../registry/schema-catalog';\nimport type { SelectionEvent } from '../../views/composer/view-context';\n\n/**\n * R9.9 · D4 — Servicio de navegación declarativa.\n *\n * Un leaf (hoja) sólo NAVEGA cuando hay `select` declarado; el motor vive\n * aquí, no en cada hoja. El flujo completo es:\n *\n * `ctx.emit/leaf → dispatch → buildSegments(pattern, {key}) →\n * router.navigate(matriz de comandos) → roleGuard`\n *\n * Invariantes:\n *\n * - **key-only**: la ÚNICA identidad que viaja es `event.key`. Los comandos\n * son una matriz de segmentos (nunca concat de strings) ⇒ el Router\n * codifica la clave (una clave `A/B` queda `A%2FB` en la URL) y ningún\n * dato del registro llega a la URL.\n * - **Fail closed**: sin binding, con acción distinta a la declarada o con\n * `mode` distinto de `route`, NO hay navegación — simplemente no ocurre.\n * - **Fail loud**: un target que el registry no declara lanza ruido antes de\n * navegar (el validador pre-render ya lo garantiza; esto es defensa en\n * profundidad).\n * - **Guards reales**: `router.navigate` ⇒ `roleGuard` corre igual que en\n * cualquier navegación declarada. Si el guard bloquea, no se registra\n * origen (no hay «atrás» que una sesión sin permiso deba volver a ver).\n */\n\n/**\n * Patrón de segmentos de la ruta destino (`['orders', ':key']`). Usa la\n * MISMA fuente que el validador (`getSchema`) para que lo validado en build\n * sea exactamente lo navegado en runtime. Inexistente ⇒ `undefined` (la\n * defensa en profundidad: el validador pre-render garantiza que esto no\n * ocurre en schemas válidos).\n */\nexport function targetRoutePattern(target: NavigationTarget): string[] | undefined {\n const destino = getSchema(target.resource);\n const vista = destino?.uiViews?.[target.view];\n if (!destino || !vista) return undefined;\n const relativa = vista.path ?? '';\n return [destino.id, ...(relativa === '' ? [] : relativa.split('/'))];\n}\n\n/**\n * Segmentos de la ruta destino con la clave sustituida. La clave es SIEMPRE\n * un segmento completo (no se parte en `/`); un param distinto de `:key` no\n * tiene fuente declarada ⇒ falla ruido, no navega a medias.\n */\nexport function buildSegments(pattern: readonly string[], key: string): string[] {\n const commands: string[] = [];\n for (const segmento of pattern) {\n if (segmento.startsWith(':')) {\n const nombre = segmento.slice(1);\n if (nombre !== 'key') {\n throw new Error(`Destino ':${nombre}' sin fuente declarada en SelectionEvent`);\n }\n commands.push(key);\n } else {\n commands.push(segmento);\n }\n }\n return commands;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class ViewNavigationService {\n private readonly router = inject(Router);\n\n /** URL desde la que se navegó la última selección válida (origen del «atrás»). */\n private origen: string | undefined;\n\n /**\n * Transición declarada de un leaf. Inerte si no hay binding, si la acción\n * no es la declarada o si el modo no es `route`; falla ruido si el target\n * no existe en el registry.\n *\n * @returns `true` si el Router completó la navegación (guards aceptados).\n */\n async dispatch(binding: SelectionBinding | undefined, event: SelectionEvent): Promise<boolean> {\n if (binding === undefined) return false; // sin declaración ⇒ inerte\n if (binding.on !== event.action) return false; // fuera del scope declarado\n if (binding.mode !== 'route') return false; // comportamiento local del leaf\n\n const pattern = targetRoutePattern(binding.target);\n if (pattern === undefined) {\n throw new Error(\n `Destino '${binding.target.resource}/${binding.target.view}' no existe en el registry`,\n );\n }\n\n const urlOrigen = this.router.url;\n const ok = await this.router.navigate(['/', ...buildSegments(pattern, event.key)]);\n if (ok) this.origen = urlOrigen;\n return ok;\n }\n\n /**\n * Forma de volver (D4). Orden: `returnPath` declarado por el leaf ⇒\n * origen registrado por `dispatch` ⇒ raíz del recurso declarado en la ruta\n * actual (entrada directa a la URL: no hay origen en memoria). Fallback\n * final: la raíz de la aplicación.\n */\n async back(returnPath?: string): Promise<boolean> {\n if (returnPath !== undefined) return this.router.navigate([returnPath]);\n\n if (this.origen !== undefined) {\n const destino = this.origen;\n this.origen = undefined;\n return this.router.navigateByUrl(destino);\n }\n\n const recurso = this.recursoActual();\n return recurso === undefined\n ? this.router.navigate(['/'])\n : this.router.navigate(['/', recurso]);\n }\n\n /** Primer `resourceId` declarado en el árbol de rutas activo. */\n private recursoActual(): string | undefined {\n let nodo: ActivatedRouteSnapshot | null = this.router.routerState.snapshot.root;\n while (nodo !== null) {\n const id = nodo.data?.['resourceId'];\n if (id !== undefined) return String(id);\n nodo = nodo.firstChild;\n }\n return undefined;\n }\n}\n","import type { FieldCondition } from '../../core/models/schema.model';\n\n/**\n * Evaluador de condiciones `visibleWhen` / `readonlyWhen` (base.md §4):\n * comparaciones simples (igual, distinto, está vacío, contiene) sobre otros\n * campos del mismo nivel o del registro raíz.\n *\n * Nota de arquitectura: la importación de `FieldCondition` es SOLO de tipo\n * (se elimina en compilación) — no hay dependencia de runtime con `core`.\n */\n\n/** Resuelve una condición. Se resuelve contra el nivel propio; si la clave\n * no existe ahí, contra la raíz del registro. */\nexport function evaluateCondition(\n condition: FieldCondition,\n siblingValues: Record<string, unknown>,\n rootValues: Record<string, unknown> = {},\n): boolean {\n const source = condition.field in siblingValues ? siblingValues : rootValues;\n const actual = source[condition.field];\n const expected = condition.value;\n\n switch (condition.operator) {\n case 'eq':\n return actual === expected;\n case 'ne':\n return actual !== expected;\n case 'empty':\n return isEmpty(actual);\n case 'contains':\n return contains(actual, expected);\n }\n}\n\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null || value === '') {\n return true;\n }\n if (Array.isArray(value)) {\n return value.length === 0;\n }\n return false;\n}\n\nfunction contains(actual: unknown, expected: unknown): boolean {\n if (typeof actual === 'string') {\n return actual.includes(String(expected));\n }\n if (Array.isArray(actual)) {\n return actual.includes(expected);\n }\n return false;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
1
+ import { i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
2
2
  import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
3
3
  import * as i0 from "@angular/core";
4
4
  import { Service, inject } from "@angular/core";
@@ -85,4 +85,4 @@ i0.ɵɵngDeclareClassMetadata({
85
85
  });
86
86
  export { dataSourceKey as n, DataSourceService as t };
87
87
 
88
- //# sourceMappingURL=tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map
88
+ //# sourceMappingURL=tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/data-source.service.ts"],"sourcesContent":["import { inject, Service } from '@angular/core';\nimport { firstValueFrom } from 'rxjs';\n\nimport type { DataSource } from '../models/schema.model';\nimport { interpolateTemplate } from '../utils/template-interpolator';\nimport { getSchema } from '../registry/schema-catalog';\nimport { ApiService } from './api.service';\n\n/** Fila cruda de un listado: el motor no impone forma a las columnas. */\nexport type SourceRow = Record<string, unknown>;\n\n/**\n * Cuánto se reutiliza una respuesta pedida igual (F8 · R6.5).\n *\n * Un panel de administración lee mucho y escribe poco: medio minuto de\n * frescura cubre un recorrido por varios formularios sin repetir la misma\n * vuelta. Es una decisión de UX, no de contrato — `invalidate()` la salta\n * cuando el usuario acaba de crear lo que la fuente lista.\n */\nconst TTL_MS = 30_000;\n\n/** Techo de claves cacheadas: un Map sin límite es una fuga en una sesión larga. */\nconst MAX_CLAVES = 100;\n\n/**\n * `source.params` con cada `{campo}` resuelto contra `ctx`.\n *\n * - Un valor que se resuelve **vacío se OMITE** en vez de mandar `''`: el\n * back distingue «parámetro ausente» (fail-closed ⇒ sin filas) de un\n * `eq ''` que podría cruzarse con filas realmente vacías.\n * - Un parámetro sin marcador `{}` es literal y viaja tal cual.\n */\nexport function interpolateParams(\n params: Record<string, string> | undefined,\n ctx?: Record<string, unknown>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (params === undefined) return out;\n for (const name of Object.keys(params)) {\n const raw = params[name];\n const value = interpolateTemplate(raw, ctx ?? {});\n if (value !== '') out[name] = value;\n }\n return out;\n}\n\n/**\n * Clave de caché de una fuente: `resource|view|params` (F8 · R6.5).\n *\n * Los parámetros van **resueltos** y ordenados, así que dos fuentes que\n * terminan pidiendo lo mismo comparten entrada, y un `{campo}` que cambia\n * es otra entrada. Pura: es también el valor que `resource({ params })` de\n * Angular compara para decidir si vuelve a cargar.\n */\nexport function dataSourceKey(source: DataSource, ctx?: Record<string, unknown>): string {\n const params = interpolateParams(source.params, ctx);\n const names = Object.keys(params).sort();\n const query = names\n .map((name) => `${encodeURIComponent(name)}=${encodeURIComponent(params[name])}`)\n .join('&');\n return `${source.resource}|${source.view ?? ''}|${query}`;\n}\n\n/**\n * Fetcher único de `source` (F8 · R6.5): relations, selects y widgets del\n * panel piden sus filas ACÁ y no cada uno desde su componente.\n *\n * - **Caché por `resource+view+params`** con TTL e invalidación explícita —\n * es la pieza que evita que seis widgets del panel disparen seis vueltas\n * iguales a la misma vista.\n * - **Deduplicación de vuelta en vuelo**: dos consumidores que pidan la misma\n * clave en el mismo ciclo comparten la promesa (no la respuesta, la vuelta).\n * - Un error **no queda cacheado**: se borra la entrada para que la recarga\n * del consumidor vuelva a intentarlo en vez de devolver siempre la misma\n * rechazada.\n */\n@Service()\nexport class DataSourceService {\n private readonly api = inject(ApiService);\n\n private readonly cache = new Map<string, { promise: Promise<SourceRow[]>; at: number }>();\n\n /**\n * Filas de la fuente declarada.\n *\n * `fresh` salta la caché (recarga tras una escritura); `ctx` sólo se usa\n * para resolver `{campo}` en `params` y no cambia la forma de la respuesta.\n */\n async rows(\n source: DataSource,\n ctx?: Record<string, unknown>,\n fresh = false,\n ): Promise<SourceRow[]> {\n const key = dataSourceKey(source, ctx);\n if (!fresh) {\n const hit = this.cache.get(key);\n if (hit !== undefined && Date.now() - hit.at < TTL_MS) return hit.promise;\n }\n const promise = this.fetch(source, ctx).catch((error: unknown) => {\n this.cache.delete(key);\n throw error;\n });\n this.cache.set(key, { promise, at: Date.now() });\n if (this.cache.size > MAX_CLAVES) {\n // El Map conserva el orden de inserción: sale la más vieja.\n const oldest = this.cache.keys().next();\n if (!oldest.done) this.cache.delete(oldest.value);\n }\n return promise;\n }\n\n /**\n * Vacía las filas ya pedidas. Sin argumento, todas; con un `resourceId`,\n * sólo las de ese recurso (lo que acaba de crear el drawer de relación).\n */\n invalidate(resourceId?: string): void {\n if (resourceId === undefined) {\n this.cache.clear();\n return;\n }\n const prefix = `${resourceId}|`;\n for (const key of [...this.cache.keys()]) {\n if (key.startsWith(prefix)) this.cache.delete(key);\n }\n }\n\n private async fetch(source: DataSource, ctx?: Record<string, unknown>): Promise<SourceRow[]> {\n const target = getSchema(source.resource);\n if (!target) {\n // Un id que ni siquiera existe en el registry es error de\n // configuración, no de red: el consumidor lo pinta como «no se\n // pudieron cargar» con este texto.\n throw new Error(`Recurso desconocido: ${source.resource}`);\n }\n const payload: Record<string, unknown> = {};\n if (source.view !== undefined && source.view !== '') payload['view'] = source.view;\n const params = interpolateParams(source.params, ctx);\n if (Object.keys(params).length > 0) payload['params'] = params;\n const rows = await firstValueFrom(this.api.list<SourceRow>(target.endpoint, payload));\n return Array.isArray(rows) ? rows : [];\n }\n}\n"],"mappings":";;;;;AAmBA,MAAM,SAAS;AAGf,MAAM,aAAa;AAUnB,SAAgB,kBACd,QACA,KAA6B;CAE7B,MAAM,MAA8B,CAAA;CACpC,IAAI,WAAW,QAAW,OAAO;CACjC,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,GAAG;EACtC,MAAM,MAAM,OAAO;EACnB,MAAM,QAAQ,oBAAoB,KAAK,OAAO,CAAA,CAAE;EAChD,IAAI,UAAU,IAAI,IAAI,QAAQ;CAChC;CACA,OAAO;AACT;AAUA,SAAgB,cAAc,QAAoB,KAA6B;CAC7E,MAAM,SAAS,kBAAkB,OAAO,QAAQ,GAAG;CAEnD,MAAM,QADQ,OAAO,KAAK,MAAM,CAAC,CAAC,KACpB,CAAA,CACX,KAAK,SAAS,GAAG,mBAAmB,IAAI,EAAC,GAAI,mBAAmB,OAAO,KAAK,GAAG,CAAC,CAChF,KAAK,GAAG;CACX,OAAO,GAAG,OAAO,SAAQ,GAAI,OAAO,QAAQ,GAAE,GAAI;AACpD;AAgBA,IAAa,oBAAb,MAAa,kBAAiB;CACX,MAAM,OAAO,UAAU;CAEvB,wBAAQ,IAAI,IAAG;CAQhC,MAAM,KACJ,QACA,KACA,QAAQ,OAAK;EAEb,MAAM,MAAM,cAAc,QAAQ,GAAG;EACrC,IAAI,CAAC,OAAO;GACV,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG;GAC9B,IAAI,QAAQ,UAAa,KAAK,IAAG,IAAK,IAAI,KAAK,QAAQ,OAAO,IAAI;EACpE;EACA,MAAM,UAAU,KAAK,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,UAAkB;GAC/D,KAAK,MAAM,OAAO,GAAG;GACrB,MAAM;EACR,CAAC;EACD,KAAK,MAAM,IAAI,KAAK;GAAE;GAAS,IAAI,KAAK,IAAG;EAAE,CAAE;EAC/C,IAAI,KAAK,MAAM,OAAO,YAAY;GAEhC,MAAM,SAAS,KAAK,MAAM,KAAI,CAAE,CAAC,KAAI;GACrC,IAAI,CAAC,OAAO,MAAM,KAAK,MAAM,OAAO,OAAO,KAAK;EAClD;EACA,OAAO;CACT;CAMA,WAAW,YAAmB;EAC5B,IAAI,eAAe,QAAW;GAC5B,KAAK,MAAM,MAAK;GAChB;EACF;EACA,MAAM,SAAS,GAAG,WAAU;EAC5B,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,MAAM,KAAI,CAAE,GACrC,IAAI,IAAI,WAAW,MAAM,GAAG,KAAK,MAAM,OAAO,GAAG;CAErD;CAEQ,MAAM,MAAM,QAAoB,KAA6B;EACnE,MAAM,SAAS,UAAU,OAAO,QAAQ;EACxC,IAAI,CAAC,QAIH,MAAM,IAAI,MAAM,wBAAwB,OAAO,UAAU;EAE3D,MAAM,UAAmC,CAAA;EACzC,IAAI,OAAO,SAAS,UAAa,OAAO,SAAS,IAAI,QAAQ,UAAU,OAAO;EAC9E,MAAM,SAAS,kBAAkB,OAAO,QAAQ,GAAG;EACnD,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,QAAQ,YAAY;EACxD,MAAM,OAAO,MAAM,eAAe,KAAK,IAAI,KAAgB,OAAO,UAAU,OAAO,CAAC;EACpF,OAAO,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAA;CACtC;CA/DW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAiB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAjB,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAiB,CAAA;;AAAjB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAiB,YAAA,CAAA,EAD7B,MAAA,QAAA,CAAA"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-data-source.service-BUoA9kBh.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/data-source.service.ts"],"sourcesContent":["import { inject, Service } from '@angular/core';\nimport { firstValueFrom } from 'rxjs';\n\nimport type { DataSource } from '../models/schema.model';\nimport { interpolateTemplate } from '../utils/template-interpolator';\nimport { getSchema } from '../registry/schema-catalog';\nimport { ApiService } from './api.service';\n\n/** Fila cruda de un listado: el motor no impone forma a las columnas. */\nexport type SourceRow = Record<string, unknown>;\n\n/**\n * Cuánto se reutiliza una respuesta pedida igual (F8 · R6.5).\n *\n * Un panel de administración lee mucho y escribe poco: medio minuto de\n * frescura cubre un recorrido por varios formularios sin repetir la misma\n * vuelta. Es una decisión de UX, no de contrato — `invalidate()` la salta\n * cuando el usuario acaba de crear lo que la fuente lista.\n */\nconst TTL_MS = 30_000;\n\n/** Techo de claves cacheadas: un Map sin límite es una fuga en una sesión larga. */\nconst MAX_CLAVES = 100;\n\n/**\n * `source.params` con cada `{campo}` resuelto contra `ctx`.\n *\n * - Un valor que se resuelve **vacío se OMITE** en vez de mandar `''`: el\n * back distingue «parámetro ausente» (fail-closed ⇒ sin filas) de un\n * `eq ''` que podría cruzarse con filas realmente vacías.\n * - Un parámetro sin marcador `{}` es literal y viaja tal cual.\n */\nexport function interpolateParams(\n params: Record<string, string> | undefined,\n ctx?: Record<string, unknown>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (params === undefined) return out;\n for (const name of Object.keys(params)) {\n const raw = params[name];\n const value = interpolateTemplate(raw, ctx ?? {});\n if (value !== '') out[name] = value;\n }\n return out;\n}\n\n/**\n * Clave de caché de una fuente: `resource|view|params` (F8 · R6.5).\n *\n * Los parámetros van **resueltos** y ordenados, así que dos fuentes que\n * terminan pidiendo lo mismo comparten entrada, y un `{campo}` que cambia\n * es otra entrada. Pura: es también el valor que `resource({ params })` de\n * Angular compara para decidir si vuelve a cargar.\n */\nexport function dataSourceKey(source: DataSource, ctx?: Record<string, unknown>): string {\n const params = interpolateParams(source.params, ctx);\n const names = Object.keys(params).sort();\n const query = names\n .map((name) => `${encodeURIComponent(name)}=${encodeURIComponent(params[name])}`)\n .join('&');\n return `${source.resource}|${source.view ?? ''}|${query}`;\n}\n\n/**\n * Fetcher único de `source` (F8 · R6.5): relations, selects y widgets del\n * panel piden sus filas ACÁ y no cada uno desde su componente.\n *\n * - **Caché por `resource+view+params`** con TTL e invalidación explícita —\n * es la pieza que evita que seis widgets del panel disparen seis vueltas\n * iguales a la misma vista.\n * - **Deduplicación de vuelta en vuelo**: dos consumidores que pidan la misma\n * clave en el mismo ciclo comparten la promesa (no la respuesta, la vuelta).\n * - Un error **no queda cacheado**: se borra la entrada para que la recarga\n * del consumidor vuelva a intentarlo en vez de devolver siempre la misma\n * rechazada.\n */\n@Service()\nexport class DataSourceService {\n private readonly api = inject(ApiService);\n\n private readonly cache = new Map<string, { promise: Promise<SourceRow[]>; at: number }>();\n\n /**\n * Filas de la fuente declarada.\n *\n * `fresh` salta la caché (recarga tras una escritura); `ctx` sólo se usa\n * para resolver `{campo}` en `params` y no cambia la forma de la respuesta.\n */\n async rows(\n source: DataSource,\n ctx?: Record<string, unknown>,\n fresh = false,\n ): Promise<SourceRow[]> {\n const key = dataSourceKey(source, ctx);\n if (!fresh) {\n const hit = this.cache.get(key);\n if (hit !== undefined && Date.now() - hit.at < TTL_MS) return hit.promise;\n }\n const promise = this.fetch(source, ctx).catch((error: unknown) => {\n this.cache.delete(key);\n throw error;\n });\n this.cache.set(key, { promise, at: Date.now() });\n if (this.cache.size > MAX_CLAVES) {\n // El Map conserva el orden de inserción: sale la más vieja.\n const oldest = this.cache.keys().next();\n if (!oldest.done) this.cache.delete(oldest.value);\n }\n return promise;\n }\n\n /**\n * Vacía las filas ya pedidas. Sin argumento, todas; con un `resourceId`,\n * sólo las de ese recurso (lo que acaba de crear el drawer de relación).\n */\n invalidate(resourceId?: string): void {\n if (resourceId === undefined) {\n this.cache.clear();\n return;\n }\n const prefix = `${resourceId}|`;\n for (const key of [...this.cache.keys()]) {\n if (key.startsWith(prefix)) this.cache.delete(key);\n }\n }\n\n private async fetch(source: DataSource, ctx?: Record<string, unknown>): Promise<SourceRow[]> {\n const target = getSchema(source.resource);\n if (!target) {\n // Un id que ni siquiera existe en el registry es error de\n // configuración, no de red: el consumidor lo pinta como «no se\n // pudieron cargar» con este texto.\n throw new Error(`Recurso desconocido: ${source.resource}`);\n }\n const payload: Record<string, unknown> = {};\n if (source.view !== undefined && source.view !== '') payload['view'] = source.view;\n const params = interpolateParams(source.params, ctx);\n if (Object.keys(params).length > 0) payload['params'] = params;\n const rows = await firstValueFrom(this.api.list<SourceRow>(target.endpoint, payload));\n return Array.isArray(rows) ? rows : [];\n }\n}\n"],"mappings":";;;;;AAmBA,MAAM,SAAS;AAGf,MAAM,aAAa;AAUnB,SAAgB,kBACd,QACA,KAA6B;CAE7B,MAAM,MAA8B,CAAA;CACpC,IAAI,WAAW,QAAW,OAAO;CACjC,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,GAAG;EACtC,MAAM,MAAM,OAAO;EACnB,MAAM,QAAQ,oBAAoB,KAAK,OAAO,CAAA,CAAE;EAChD,IAAI,UAAU,IAAI,IAAI,QAAQ;CAChC;CACA,OAAO;AACT;AAUA,SAAgB,cAAc,QAAoB,KAA6B;CAC7E,MAAM,SAAS,kBAAkB,OAAO,QAAQ,GAAG;CAEnD,MAAM,QADQ,OAAO,KAAK,MAAM,CAAC,CAAC,KACpB,CAAA,CACX,KAAK,SAAS,GAAG,mBAAmB,IAAI,EAAC,GAAI,mBAAmB,OAAO,KAAK,GAAG,CAAC,CAChF,KAAK,GAAG;CACX,OAAO,GAAG,OAAO,SAAQ,GAAI,OAAO,QAAQ,GAAE,GAAI;AACpD;AAgBA,IAAa,oBAAb,MAAa,kBAAiB;CACX,MAAM,OAAO,UAAU;CAEvB,wBAAQ,IAAI,IAAG;CAQhC,MAAM,KACJ,QACA,KACA,QAAQ,OAAK;EAEb,MAAM,MAAM,cAAc,QAAQ,GAAG;EACrC,IAAI,CAAC,OAAO;GACV,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG;GAC9B,IAAI,QAAQ,UAAa,KAAK,IAAG,IAAK,IAAI,KAAK,QAAQ,OAAO,IAAI;EACpE;EACA,MAAM,UAAU,KAAK,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,UAAkB;GAC/D,KAAK,MAAM,OAAO,GAAG;GACrB,MAAM;EACR,CAAC;EACD,KAAK,MAAM,IAAI,KAAK;GAAE;GAAS,IAAI,KAAK,IAAG;EAAE,CAAE;EAC/C,IAAI,KAAK,MAAM,OAAO,YAAY;GAEhC,MAAM,SAAS,KAAK,MAAM,KAAI,CAAE,CAAC,KAAI;GACrC,IAAI,CAAC,OAAO,MAAM,KAAK,MAAM,OAAO,OAAO,KAAK;EAClD;EACA,OAAO;CACT;CAMA,WAAW,YAAmB;EAC5B,IAAI,eAAe,QAAW;GAC5B,KAAK,MAAM,MAAK;GAChB;EACF;EACA,MAAM,SAAS,GAAG,WAAU;EAC5B,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,MAAM,KAAI,CAAE,GACrC,IAAI,IAAI,WAAW,MAAM,GAAG,KAAK,MAAM,OAAO,GAAG;CAErD;CAEQ,MAAM,MAAM,QAAoB,KAA6B;EACnE,MAAM,SAAS,UAAU,OAAO,QAAQ;EACxC,IAAI,CAAC,QAIH,MAAM,IAAI,MAAM,wBAAwB,OAAO,UAAU;EAE3D,MAAM,UAAmC,CAAA;EACzC,IAAI,OAAO,SAAS,UAAa,OAAO,SAAS,IAAI,QAAQ,UAAU,OAAO;EAC9E,MAAM,SAAS,kBAAkB,OAAO,QAAQ,GAAG;EACnD,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,QAAQ,YAAY;EACxD,MAAM,OAAO,MAAM,eAAe,KAAK,IAAI,KAAgB,OAAO,UAAU,OAAO,CAAC;EACpF,OAAO,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAA;CACtC;CA/DW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAiB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAjB,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAiB,CAAA;;AAAjB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAiB,YAAA,CAAA,EAD7B,MAAA,QAAA,CAAA"}
@@ -1,4 +1,4 @@
1
- import { v as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
1
+ import { y as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
2
2
  import * as i0 from "@angular/core";
3
3
  import { Service } from "@angular/core";
4
4
  var ErrorMapperService = class ErrorMapperService {
@@ -42,4 +42,4 @@ i0.ɵɵngDeclareClassMetadata({
42
42
  });
43
43
  export { ErrorMapperService as t };
44
44
 
45
- //# sourceMappingURL=tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map
45
+ //# sourceMappingURL=tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/error-mapper.service.ts"],"sourcesContent":["import { Service } from '@angular/core';\n\nimport { ApiError } from '../models/api.model';\n\nexport interface MappedError {\n /** Errores por ruta de campo (ej. `faq[2].answer`); se pintan en el campo exacto. */\n fields: Record<string, string>;\n /** Mensaje general; presente cuando el error no tiene ruta de campo (base.md §10). */\n general?: string;\n}\n\n/**\n * Convierte un error recibido en el formato que consume la UI:\n * rutas de campo para pintar el error donde corresponde, o mensaje general.\n *\n * Un422 trae VARIOS issues con ruta (`error.errors[]`, auth.md §6); cada uno\n * se convierte en `fields[path] = mensaje`, que es exactamente la clave que\n * después resuelve el host como `serverErrors[prefix + key]`. Si al final no\n * hay ninguna ruta pintable (error sin `errors[]`, issue sin `path`), el\n * mensaje del sobre queda en `general` como hasta ahora (base.md §10).\n */\n@Service()\nexport class ErrorMapperService {\n map(error: unknown): MappedError {\n if (error instanceof ApiError) {\n const fields: Record<string, string> = {};\n for (const issue of error.issues) {\n if (issue.path) {\n fields[issue.path] = issue.message;\n }\n }\n // Sobre con `path` suelto (sin `errors[]`): misma historia, un campo.\n if (error.fieldPath && fields[error.fieldPath] === undefined) {\n fields[error.fieldPath] = error.message;\n }\n if (Object.keys(fields).length > 0) {\n return { fields };\n }\n }\n return { fields: {}, general: this.messageOf(error) };\n }\n\n messageOf(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return 'Error inesperado';\n }\n}\n"],"mappings":";;;AAsBA,IAAa,qBAAb,MAAa,mBAAkB;CAC7B,IAAI,OAAc;EAChB,IAAI,iBAAiB,UAAU;GAC7B,MAAM,SAAiC,CAAA;GACvC,KAAK,MAAM,SAAS,MAAM,QACxB,IAAI,MAAM,MACR,OAAO,MAAM,QAAQ,MAAM;GAI/B,IAAI,MAAM,aAAa,OAAO,MAAM,eAAe,QACjD,OAAO,MAAM,aAAa,MAAM;GAElC,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,OAAO,EAAE,OAAM;EAEnB;EACA,OAAO;GAAE,QAAQ,CAAA;GAAI,SAAS,KAAK,UAAU,KAAK;EAAC;CACrD;CAEA,UAAU,OAAc;EACtB,IAAI,iBAAiB,OACnB,OAAO,MAAM;EAEf,OAAO;CACT;CAzBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAlB,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAkB,CAAA;;AAAlB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAkB,YAAA,CAAA,EAD9B,MAAA,QAAA,CAAA"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-error-mapper.service-BDA1dL5V.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/error-mapper.service.ts"],"sourcesContent":["import { Service } from '@angular/core';\n\nimport { ApiError } from '../models/api.model';\n\nexport interface MappedError {\n /** Errores por ruta de campo (ej. `faq[2].answer`); se pintan en el campo exacto. */\n fields: Record<string, string>;\n /** Mensaje general; presente cuando el error no tiene ruta de campo (base.md §10). */\n general?: string;\n}\n\n/**\n * Convierte un error recibido en el formato que consume la UI:\n * rutas de campo para pintar el error donde corresponde, o mensaje general.\n *\n * Un422 trae VARIOS issues con ruta (`error.errors[]`, auth.md §6); cada uno\n * se convierte en `fields[path] = mensaje`, que es exactamente la clave que\n * después resuelve el host como `serverErrors[prefix + key]`. Si al final no\n * hay ninguna ruta pintable (error sin `errors[]`, issue sin `path`), el\n * mensaje del sobre queda en `general` como hasta ahora (base.md §10).\n */\n@Service()\nexport class ErrorMapperService {\n map(error: unknown): MappedError {\n if (error instanceof ApiError) {\n const fields: Record<string, string> = {};\n for (const issue of error.issues) {\n if (issue.path) {\n fields[issue.path] = issue.message;\n }\n }\n // Sobre con `path` suelto (sin `errors[]`): misma historia, un campo.\n if (error.fieldPath && fields[error.fieldPath] === undefined) {\n fields[error.fieldPath] = error.message;\n }\n if (Object.keys(fields).length > 0) {\n return { fields };\n }\n }\n return { fields: {}, general: this.messageOf(error) };\n }\n\n messageOf(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return 'Error inesperado';\n }\n}\n"],"mappings":";;;AAsBA,IAAa,qBAAb,MAAa,mBAAkB;CAC7B,IAAI,OAAc;EAChB,IAAI,iBAAiB,UAAU;GAC7B,MAAM,SAAiC,CAAA;GACvC,KAAK,MAAM,SAAS,MAAM,QACxB,IAAI,MAAM,MACR,OAAO,MAAM,QAAQ,MAAM;GAI/B,IAAI,MAAM,aAAa,OAAO,MAAM,eAAe,QACjD,OAAO,MAAM,aAAa,MAAM;GAElC,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,OAAO,EAAE,OAAM;EAEnB;EACA,OAAO;GAAE,QAAQ,CAAA;GAAI,SAAS,KAAK,UAAU,KAAK;EAAC;CACrD;CAEA,UAAU,OAAc;EACtB,IAAI,iBAAiB,OACnB,OAAO,MAAM;EAEf,OAAO;CACT;CAzBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAlB,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAkB,CAAA;;AAAlB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAkB,YAAA,CAAA,EAD9B,MAAA,QAAA,CAAA"}
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
- import { v as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
2
+ import { y as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-DU_6_CtP.mjs";
3
3
  import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
4
4
  import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
5
- import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs";
5
+ import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-DRmKFrSH.mjs";
6
6
  import * as i0 from "@angular/core";
7
7
  import { Component, DestroyRef, computed, inject, input, model, output, signal, viewChild } from "@angular/core";
8
8
  import { HttpEventType } from "@angular/common/http";
@@ -405,4 +405,4 @@ function acceptMatches(accept, file) {
405
405
  }
406
406
  export { file_exports as n, FieldFile as t };
407
407
 
408
- //# sourceMappingURL=tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map
408
+ //# sourceMappingURL=tinya-code-gas-admin-ui-file-DhLi3eO0.mjs.map