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

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 (41) hide show
  1. package/README.md +584 -116
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs → tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs} +4 -4
  3. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map → tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map} +1 -1
  4. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs → tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs} +2 -2
  5. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map} +1 -1
  6. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs → tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs} +2 -3
  7. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map → tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs → tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map} +1 -1
  10. package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs → tinya-code-gas-admin-ui-file-B6_vveO5.mjs} +3 -3
  11. package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map → tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map} +1 -1
  12. package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs → tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs} +9 -10
  13. package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map → tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs → tinya-code-gas-admin-ui-image-lUe8nrUf.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map → tinya-code-gas-admin-ui-image-lUe8nrUf.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs → tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs} +4 -4
  17. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map → tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs.map} +1 -1
  18. package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs → tinya-code-gas-admin-ui-list-CH7b7dKh.mjs} +2 -2
  19. package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map → tinya-code-gas-admin-ui-list-CH7b7dKh.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs → tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs} +3 -3
  21. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map → tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs → tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs} +3 -3
  23. package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map → tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs → tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs} +10 -10
  25. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map → tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs.map} +1 -1
  26. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs +494 -0
  27. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs.map +1 -0
  28. package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs → tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs} +2 -2
  29. package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map → tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs.map} +1 -1
  30. package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs → tinya-code-gas-admin-ui-select-CL2kj7pM.mjs} +3 -3
  31. package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map → tinya-code-gas-admin-ui-select-CL2kj7pM.mjs.map} +1 -1
  32. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs → tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs} +2 -2
  33. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map → tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs.map} +1 -1
  34. package/fesm2022/tinya-code-gas-admin-ui.mjs +65 -11
  35. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
  36. package/package.json +8 -2
  37. package/types/tinya-code-gas-admin-ui.d.ts +119 -29
  38. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs +0 -281
  39. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +0 -1
  40. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +0 -131
  41. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.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-LD8AM3Rn.mjs";
4
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.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";
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-r-RXDE37.mjs.map
1022
+ //# sourceMappingURL=tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-calendar-view-r-RXDE37.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-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,4 +1,4 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.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-BBzWUuHl.mjs.map
101
+ //# sourceMappingURL=tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.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-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":";;;;AAuCA,SAAgB,mBAAmB,QAAwB;CACzD,MAAM,UAAU,UAAU,OAAO,QAAQ;CACzC,MAAM,QAAQ,SAAS,UAAU,OAAO;CACxC,IAAI,CAAC,WAAW,CAAC,OAAO,OAAO;CAC/B,MAAM,WAAW,MAAM,QAAQ;CAC/B,OAAO,CAAC,QAAQ,IAAI,GAAI,aAAa,KAAK,CAAA,IAAK,SAAS,MAAM,GAAG,CAAE;AACrE;AAOA,SAAgB,cAAc,SAA4B,KAAW;CACnE,MAAM,WAAqB,CAAA;CAC3B,KAAK,MAAM,YAAY,SACrB,IAAI,SAAS,WAAW,GAAG,GAAG;EAC5B,MAAM,SAAS,SAAS,MAAM,CAAC;EAC/B,IAAI,WAAW,OACb,MAAM,IAAI,MAAM,aAAa,OAAM,yCAA0C;EAE/E,SAAS,KAAK,GAAG;CACnB,OACE,SAAS,KAAK,QAAQ;CAG1B,OAAO;AACT;AAGA,IAAa,wBAAb,MAAa,sBAAqB;CACf,SAAS,OAAO,MAAM;CAG/B;CASR,MAAM,SAAS,SAAuC,OAAqB;EACzE,IAAI,YAAY,QAAW,OAAO;EAClC,IAAI,QAAQ,OAAO,MAAM,QAAQ,OAAO;EACxC,IAAI,QAAQ,SAAS,SAAS,OAAO;EAErC,MAAM,UAAU,mBAAmB,QAAQ,MAAM;EACjD,IAAI,YAAY,QACd,MAAM,IAAI,MACR,YAAY,QAAQ,OAAO,SAAQ,GAAI,QAAQ,OAAO,KAAI,2BAA4B;EAI1F,MAAM,YAAY,KAAK,OAAO;EAC9B,MAAM,KAAK,MAAM,KAAK,OAAO,SAAS,CAAC,KAAK,GAAG,cAAc,SAAS,MAAM,GAAG,CAAC,CAAC;EACjF,IAAI,IAAI,KAAK,SAAS;EACtB,OAAO;CACT;CAQA,MAAM,KAAK,YAAmB;EAC5B,IAAI,eAAe,QAAW,OAAO,KAAK,OAAO,SAAS,CAAC,UAAU,CAAC;EAEtE,IAAI,KAAK,WAAW,QAAW;GAC7B,MAAM,UAAU,KAAK;GACrB,KAAK,SAAS;GACd,OAAO,KAAK,OAAO,cAAc,OAAO;EAC1C;EAEA,MAAM,UAAU,KAAK,cAAa;EAClC,OAAO,YAAY,SACf,KAAK,OAAO,SAAS,CAAC,GAAG,CAAC,IAC1B,KAAK,OAAO,SAAS,CAAC,KAAK,OAAO,CAAC;CACzC;CAGQ,gBAAa;EACnB,IAAI,OAAsC,KAAK,OAAO,YAAY,SAAS;EAC3E,OAAO,SAAS,MAAM;GACpB,MAAM,KAAK,KAAK,OAAO;GACvB,IAAI,OAAO,QAAW,OAAO,OAAO,EAAE;GACtC,OAAO,KAAK;EACd;CAEF;CA7DW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAqB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAArB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAqB,YADR;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAqB,YAAA,CAAA;EADjC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACvDlC,SAAgB,kBACd,WACA,eACA,aAAsC,CAAA,GAAE;CAGxC,MAAM,UADS,UAAU,SAAS,gBAAgB,gBAAgB,WACnD,CAAO,UAAU;CAChC,MAAM,WAAW,UAAU;CAE3B,QAAQ,UAAU,UAAlB;EACE,KAAK,MACH,OAAO,WAAW;EACpB,KAAK,MACH,OAAO,WAAW;EACpB,KAAK,SACH,OAAO,QAAQ,MAAM;EACvB,KAAK,YACH,OAAO,SAAS,QAAQ,QAAQ;CACpC;AACF;AAEA,SAAS,QAAQ,OAAc;CAC7B,IAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IACrD,OAAO;CAET,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,WAAW;CAE1B,OAAO;AACT;AAEA,SAAS,SAAS,QAAiB,UAAiB;CAClD,IAAI,OAAO,WAAW,UACpB,OAAO,OAAO,SAAS,OAAO,QAAQ,CAAC;CAEzC,IAAI,MAAM,QAAQ,MAAM,GACtB,OAAO,OAAO,SAAS,QAAQ;CAEjC,OAAO;AACT"}
@@ -1,5 +1,4 @@
1
- import { t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
2
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
1
+ import { i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
3
2
  import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
4
3
  import * as i0 from "@angular/core";
5
4
  import { Service, inject } from "@angular/core";
@@ -86,4 +85,4 @@ i0.ɵɵngDeclareClassMetadata({
86
85
  });
87
86
  export { dataSourceKey as n, DataSourceService as t };
88
87
 
89
- //# sourceMappingURL=tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map
88
+ //# sourceMappingURL=tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.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-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,4 +1,4 @@
1
- import { s as ApiError } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
1
+ import { v as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.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-Ci0UpXtV.mjs.map
45
+ //# sourceMappingURL=tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.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-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,8 +1,8 @@
1
1
  import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
- import { s as ApiError } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
2
+ import { v as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.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-C1g545w5.mjs";
5
+ import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-D700Obgh.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-Dg2N3XQ2.mjs.map
408
+ //# sourceMappingURL=tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/file/file.ts"],"sourcesContent":["import { HttpEventType } from '@angular/common/http';\nimport {\n Component,\n computed,\n DestroyRef,\n ElementRef,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport type { FileField } from '../../core/models/schema.model';\nimport { ApiError } from '../../core/models/api.model';\nimport { NotificationService } from '../../core/services/notification.service';\nimport { Button } from '../../shared/components/button/button';\nimport { UploadService } from '../../core/services/upload.service';\n\n/**\n * Carga de archivo (base.md §7 · R9.5.3): el valor es SOLO la URL pública,\n * igual que `image`, pero para cualquier tipo de archivo (pdf, xlsx, zip…).\n *\n * Espejo de `FieldImage` (D27: es un control propio que no pasa por\n * `NgComponentOutlet`, porque su `value` es un `model()` que el host\n * sincroniza por `[formField]`). La ÚNICA diferencia de comportamiento: no\n * fuerza el prefijo `image/` — el catálogo de formatos lo define el backend\n * por campo (`formats` declarado) o la lista global, y el cliente sólo\n * respeta `accept`/`maxSizeMB` como validación de comodidad.\n *\n * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el\n * valor no cambia hasta la respuesta), con archivo (nombre + Quitar / Ver) y\n * error (conserva el archivo anterior).\n */\n@Component({\n selector: 'app-field-file',\n imports: [Button],\n template: ` @if (error(); as message) {\n <p\n class=\"mb-2 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-xs text-danger\"\n role=\"alert\"\n >\n {{ message }}\n </p>\n }\n @if (uploading()) {\n <div class=\"rounded-xl border border-neutral/20 bg-surface p-3\" data-testid=\"file-uploading\">\n <div class=\"h-1.5 w-full overflow-hidden rounded-full bg-neutral/20\">\n <div\n class=\"h-full rounded-full bg-primary transition-all\"\n [style.width.%]=\"progress()\"\n ></div>\n </div>\n <p class=\"mt-1 text-xs text-neutral\">Subiendo… {{ progress() }}%</p>\n </div>\n } @else if (value()) {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-3\"\n data-testid=\"file-preview\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"truncate text-sm font-medium\" [title]=\"value()\">{{ fileName() }}</p>\n <div class=\"mt-2 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Reemplazar\n </button>\n @if (!field().required) {\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"clear()\"\n >\n Quitar\n </button>\n }\n <a\n class=\"text-sm font-medium text-primary hover:underline\"\n [href]=\"value()\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Abrir\n </a>\n </div>\n </div>\n } @else {\n <div\n class=\"rounded-xl border-2 border-dashed border-neutral/30 p-6 text-center\"\n data-testid=\"file-dropzone\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"text-sm text-neutral\">Arrastrá un archivo acá</p>\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n class=\"mt-2\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Subir archivo\n </button>\n </div>\n }\n\n <input\n #fileInput\n type=\"file\"\n class=\"sr-only\"\n [accept]=\"acceptAttr()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n />`,\n})\nexport class FieldFile implements FormValueControl<string> {\n readonly field = input.required<FileField>();\n /** Recurso dueño del archivo (firma de subida: carpeta de Cloudinary, §11.2). */\n readonly resource = input('');\n\n /** Sincronizado por el directive `FormField` del host. */\n readonly value = model.required<string>();\n readonly disabled = input(false);\n /** Marca el campo como tocado (upload/clear/error), como el `blur` nativo. */\n readonly touch = output<void>();\n\n private readonly upload = inject(UploadService);\n private readonly notifications = inject(NotificationService);\n private readonly destroyRef = inject(DestroyRef);\n\n protected readonly uploading = signal(false);\n protected readonly progress = signal(0);\n protected readonly error = signal<string | null>(null);\n\n /**\n * Nombre derivado de la URL para el preview. Cloudinary guarda el\n * `public_id` al final del path (…/v123/produccion/adjuntos/archivo_xyz):\n * lo presentable es el sufijo tras el último `/`, sin extensión ruidosa.\n */\n protected readonly fileName = computed(() => {\n const url = this.value();\n const segment = url.split('/').pop() ?? url;\n return decodeURIComponent(segment);\n });\n\n /** `accept` de `<input type=file>`: el declarado o `''` (sin forzar `image/*`). */\n protected readonly acceptAttr = computed(() => this.field().accept ?? '');\n\n /**\n * Template ref `#fileInput`: Angular NO auto-asigna las template refs a\n * propiedades de clase (mismo motivo que en `FieldImage`).\n */\n private readonly fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');\n private uploadSub?: { unsubscribe(): void };\n\n protected pickFile(): void {\n this.fileInput().nativeElement.click();\n }\n\n protected clear(): void {\n this.value.set('');\n this.error.set(null);\n this.touch.emit();\n }\n\n protected onFileSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n // Permite volver a elegir el mismo archivo tras un error.\n input.value = '';\n if (file) {\n this.startUpload(file);\n }\n }\n\n protected onDrop(event: Event): void {\n event.preventDefault();\n const file = (event as DragEvent).dataTransfer?.files?.[0];\n if (file) {\n this.startUpload(file);\n }\n }\n\n /** Valida en cliente (accept + maxSizeMB) y sube con progreso. */\n private startUpload(file: File): void {\n if (this.disabled()) {\n return;\n }\n const problem = this.validate(file);\n if (problem !== null) {\n this.error.set(problem);\n this.touch.emit();\n return;\n }\n this.error.set(null);\n this.uploading.set(true);\n this.progress.set(0);\n this.touch.emit();\n\n this.uploadSub?.unsubscribe();\n // El nombre del campo viaja siempre: si declara `formats`, el backend\n // firma ESA lista; sin formatos firma la lista global (R9.5.3).\n this.uploadSub = this.upload\n .uploadWithProgress(file, this.resource(), this.field().key)\n .subscribe({\n next: (event) => {\n if (event.type === HttpEventType.UploadProgress) {\n if (event.total) {\n this.progress.set(Math.round((event.loaded / event.total) * 100));\n }\n return;\n }\n if (event.type === HttpEventType.Response) {\n const url = event.body ?? '';\n this.uploading.set(false);\n if (url === '') {\n this.error.set('No se pudo subir el archivo.');\n return;\n }\n this.value.set(url);\n this.notifications.success('Archivo subido.');\n }\n },\n error: (err: unknown) => {\n this.uploading.set(false);\n // El backend explica el motivo (firma inválida, 429 rate-limit…).\n this.error.set(\n err instanceof ApiError\n ? err.message\n : 'No se pudo subir el archivo. Revisa la conexión e inténtalo de nuevo.',\n );\n },\n });\n this.destroyRef.onDestroy(() => this.uploadSub?.unsubscribe());\n }\n\n private validate(file: File): string | null {\n const accept = this.field().accept;\n if (accept !== undefined && accept !== '' && !acceptMatches(accept, file)) {\n return 'Formato no permitido para este campo.';\n }\n const maxSizeMB = this.field().maxSizeMB;\n if (maxSizeMB !== undefined && file.size > maxSizeMB * 1024 * 1024) {\n return `El archivo supera ${maxSizeMB} MB.`;\n }\n return null;\n }\n}\n\n/** `accept` de `<input type=file>`: lista, `tipo/*` o extensión `.png`. */\nfunction acceptMatches(accept: string, file: File): boolean {\n for (const raw of accept.split(',')) {\n const entry = raw.trim().toLowerCase();\n if (entry === '') {\n continue;\n }\n if (entry.startsWith('.')) {\n if (file.name.toLowerCase().endsWith(entry)) {\n return true;\n }\n } else if (entry.endsWith('/*')) {\n if (file.type.startsWith(entry.slice(0, -1))) {\n return true;\n }\n } else if (file.type === entry) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-file-B6_vveO5.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/file/file.ts"],"sourcesContent":["import { HttpEventType } from '@angular/common/http';\nimport {\n Component,\n computed,\n DestroyRef,\n ElementRef,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport type { FileField } from '../../core/models/schema.model';\nimport { ApiError } from '../../core/models/api.model';\nimport { NotificationService } from '../../core/services/notification.service';\nimport { Button } from '../../shared/components/button/button';\nimport { UploadService } from '../../core/services/upload.service';\n\n/**\n * Carga de archivo (base.md §7 · R9.5.3): el valor es SOLO la URL pública,\n * igual que `image`, pero para cualquier tipo de archivo (pdf, xlsx, zip…).\n *\n * Espejo de `FieldImage` (D27: es un control propio que no pasa por\n * `NgComponentOutlet`, porque su `value` es un `model()` que el host\n * sincroniza por `[formField]`). La ÚNICA diferencia de comportamiento: no\n * fuerza el prefijo `image/` — el catálogo de formatos lo define el backend\n * por campo (`formats` declarado) o la lista global, y el cliente sólo\n * respeta `accept`/`maxSizeMB` como validación de comodidad.\n *\n * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el\n * valor no cambia hasta la respuesta), con archivo (nombre + Quitar / Ver) y\n * error (conserva el archivo anterior).\n */\n@Component({\n selector: 'app-field-file',\n imports: [Button],\n template: ` @if (error(); as message) {\n <p\n class=\"mb-2 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-xs text-danger\"\n role=\"alert\"\n >\n {{ message }}\n </p>\n }\n @if (uploading()) {\n <div class=\"rounded-xl border border-neutral/20 bg-surface p-3\" data-testid=\"file-uploading\">\n <div class=\"h-1.5 w-full overflow-hidden rounded-full bg-neutral/20\">\n <div\n class=\"h-full rounded-full bg-primary transition-all\"\n [style.width.%]=\"progress()\"\n ></div>\n </div>\n <p class=\"mt-1 text-xs text-neutral\">Subiendo… {{ progress() }}%</p>\n </div>\n } @else if (value()) {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-3\"\n data-testid=\"file-preview\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"truncate text-sm font-medium\" [title]=\"value()\">{{ fileName() }}</p>\n <div class=\"mt-2 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Reemplazar\n </button>\n @if (!field().required) {\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"clear()\"\n >\n Quitar\n </button>\n }\n <a\n class=\"text-sm font-medium text-primary hover:underline\"\n [href]=\"value()\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Abrir\n </a>\n </div>\n </div>\n } @else {\n <div\n class=\"rounded-xl border-2 border-dashed border-neutral/30 p-6 text-center\"\n data-testid=\"file-dropzone\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"text-sm text-neutral\">Arrastrá un archivo acá</p>\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n class=\"mt-2\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Subir archivo\n </button>\n </div>\n }\n\n <input\n #fileInput\n type=\"file\"\n class=\"sr-only\"\n [accept]=\"acceptAttr()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n />`,\n})\nexport class FieldFile implements FormValueControl<string> {\n readonly field = input.required<FileField>();\n /** Recurso dueño del archivo (firma de subida: carpeta de Cloudinary, §11.2). */\n readonly resource = input('');\n\n /** Sincronizado por el directive `FormField` del host. */\n readonly value = model.required<string>();\n readonly disabled = input(false);\n /** Marca el campo como tocado (upload/clear/error), como el `blur` nativo. */\n readonly touch = output<void>();\n\n private readonly upload = inject(UploadService);\n private readonly notifications = inject(NotificationService);\n private readonly destroyRef = inject(DestroyRef);\n\n protected readonly uploading = signal(false);\n protected readonly progress = signal(0);\n protected readonly error = signal<string | null>(null);\n\n /**\n * Nombre derivado de la URL para el preview. Cloudinary guarda el\n * `public_id` al final del path (…/v123/produccion/adjuntos/archivo_xyz):\n * lo presentable es el sufijo tras el último `/`, sin extensión ruidosa.\n */\n protected readonly fileName = computed(() => {\n const url = this.value();\n const segment = url.split('/').pop() ?? url;\n return decodeURIComponent(segment);\n });\n\n /** `accept` de `<input type=file>`: el declarado o `''` (sin forzar `image/*`). */\n protected readonly acceptAttr = computed(() => this.field().accept ?? '');\n\n /**\n * Template ref `#fileInput`: Angular NO auto-asigna las template refs a\n * propiedades de clase (mismo motivo que en `FieldImage`).\n */\n private readonly fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');\n private uploadSub?: { unsubscribe(): void };\n\n protected pickFile(): void {\n this.fileInput().nativeElement.click();\n }\n\n protected clear(): void {\n this.value.set('');\n this.error.set(null);\n this.touch.emit();\n }\n\n protected onFileSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n // Permite volver a elegir el mismo archivo tras un error.\n input.value = '';\n if (file) {\n this.startUpload(file);\n }\n }\n\n protected onDrop(event: Event): void {\n event.preventDefault();\n const file = (event as DragEvent).dataTransfer?.files?.[0];\n if (file) {\n this.startUpload(file);\n }\n }\n\n /** Valida en cliente (accept + maxSizeMB) y sube con progreso. */\n private startUpload(file: File): void {\n if (this.disabled()) {\n return;\n }\n const problem = this.validate(file);\n if (problem !== null) {\n this.error.set(problem);\n this.touch.emit();\n return;\n }\n this.error.set(null);\n this.uploading.set(true);\n this.progress.set(0);\n this.touch.emit();\n\n this.uploadSub?.unsubscribe();\n // El nombre del campo viaja siempre: si declara `formats`, el backend\n // firma ESA lista; sin formatos firma la lista global (R9.5.3).\n this.uploadSub = this.upload\n .uploadWithProgress(file, this.resource(), this.field().key)\n .subscribe({\n next: (event) => {\n if (event.type === HttpEventType.UploadProgress) {\n if (event.total) {\n this.progress.set(Math.round((event.loaded / event.total) * 100));\n }\n return;\n }\n if (event.type === HttpEventType.Response) {\n const url = event.body ?? '';\n this.uploading.set(false);\n if (url === '') {\n this.error.set('No se pudo subir el archivo.');\n return;\n }\n this.value.set(url);\n this.notifications.success('Archivo subido.');\n }\n },\n error: (err: unknown) => {\n this.uploading.set(false);\n // El backend explica el motivo (firma inválida, 429 rate-limit…).\n this.error.set(\n err instanceof ApiError\n ? err.message\n : 'No se pudo subir el archivo. Revisa la conexión e inténtalo de nuevo.',\n );\n },\n });\n this.destroyRef.onDestroy(() => this.uploadSub?.unsubscribe());\n }\n\n private validate(file: File): string | null {\n const accept = this.field().accept;\n if (accept !== undefined && accept !== '' && !acceptMatches(accept, file)) {\n return 'Formato no permitido para este campo.';\n }\n const maxSizeMB = this.field().maxSizeMB;\n if (maxSizeMB !== undefined && file.size > maxSizeMB * 1024 * 1024) {\n return `El archivo supera ${maxSizeMB} MB.`;\n }\n return null;\n }\n}\n\n/** `accept` de `<input type=file>`: lista, `tipo/*` o extensión `.png`. */\nfunction acceptMatches(accept: string, file: File): boolean {\n for (const raw of accept.split(',')) {\n const entry = raw.trim().toLowerCase();\n if (entry === '') {\n continue;\n }\n if (entry.startsWith('.')) {\n if (file.name.toLowerCase().endsWith(entry)) {\n return true;\n }\n } else if (entry.endsWith('/*')) {\n if (file.type.startsWith(entry.slice(0, -1))) {\n return true;\n }\n } else if (file.type === entry) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":"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"}
@@ -1,21 +1,20 @@
1
1
  import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
- import { n as AuthService, t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
3
- import { i as getSchema, s as ConfigService } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
4
- import { n as get, r as isSupported, s as resolve, t as RENDER_CONTEXT_VACIO } from "./tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs";
5
- import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs";
2
+ import { c as ConfigService, d as AuthService, i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs";
3
+ import { n as get, r as isSupported, s as resolve, t as RENDER_CONTEXT_VACIO } from "./tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs";
4
+ import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs";
6
5
  import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
7
6
  import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
8
7
  import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
9
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs";
8
+ import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs";
10
9
  import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
11
10
  import { t as FIELD_ARIA } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
12
11
  import { i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
13
- import { t as FieldFile } from "./tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs";
14
- import { t as FieldImage } from "./tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs";
15
- import { t as FieldMultiselect } from "./tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs";
12
+ import { t as FieldFile } from "./tinya-code-gas-admin-ui-file-B6_vveO5.mjs";
13
+ import { t as FieldImage } from "./tinya-code-gas-admin-ui-image-lUe8nrUf.mjs";
14
+ import { t as FieldMultiselect } from "./tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs";
16
15
  import { t as esGrupoRadio } from "./tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs";
17
16
  import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
18
- import { a as colSpanClass, i as seedFields, n as mergeRecord, r as seedFieldValue, t as isRecord } from "./tinya-code-gas-admin-ui-seed-CNbQtREA.mjs";
17
+ import { a as colSpanClass, i as seedFields, n as mergeRecord, r as seedFieldValue, t as isRecord } from "./tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs";
19
18
  import { t as RelationDrawerService } from "./tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs";
20
19
  import * as i0 from "@angular/core";
21
20
  import { ApplicationRef, Component, DestroyRef, InjectionToken, Injector, PendingTasks, afterRenderEffect, computed, effect, forwardRef, inject, input, isDevMode, output, resource, signal, viewChild } from "@angular/core";
@@ -4511,4 +4510,4 @@ i0.ɵɵngDeclareClassMetadata({
4511
4510
  });
4512
4511
  export { resolveShare as _, DrawerForm as a, buildFormSchema as c, actionTypes as d, actionsFor as f, isTriggerAction as g, isShareAction as h, CopyShare as i, FieldHost as l, registerAction as m, form_view_exports as n, buildPayload as o, getAction as p, Modal as r, seedModel as s, FormView as t, actionType as u, RenderHost as v };
4513
4512
 
4514
- //# sourceMappingURL=tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map
4513
+ //# sourceMappingURL=tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs.map