@asteby/metacore-runtime-react 45.2.0 → 45.4.0

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 (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/action-modal-dispatcher.d.ts +29 -0
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +115 -6
  5. package/dist/activity-diff.d.ts +2 -0
  6. package/dist/activity-diff.d.ts.map +1 -1
  7. package/dist/activity-diff.js +2 -2
  8. package/dist/business/catalog.d.ts.map +1 -1
  9. package/dist/business/catalog.js +8 -0
  10. package/dist/business/index.d.ts +2 -0
  11. package/dist/business/index.d.ts.map +1 -1
  12. package/dist/business/index.js +2 -0
  13. package/dist/business/refund-destination.js +1 -1
  14. package/dist/business/refund.d.ts +1 -1
  15. package/dist/business/refund.d.ts.map +1 -1
  16. package/dist/business/refund.js +1 -0
  17. package/dist/business/return-wizard.d.ts +23 -0
  18. package/dist/business/return-wizard.d.ts.map +1 -0
  19. package/dist/business/return-wizard.js +59 -0
  20. package/dist/business/return.d.ts +90 -0
  21. package/dist/business/return.d.ts.map +1 -0
  22. package/dist/business/return.js +112 -0
  23. package/dist/dynamic-columns.d.ts.map +1 -1
  24. package/dist/dynamic-columns.js +3 -1
  25. package/dist/dynamic-kanban.d.ts +62 -4
  26. package/dist/dynamic-kanban.d.ts.map +1 -1
  27. package/dist/dynamic-kanban.js +158 -27
  28. package/dist/dynamic-relation.d.ts +4 -0
  29. package/dist/dynamic-relation.d.ts.map +1 -1
  30. package/dist/dynamic-relation.js +10 -1
  31. package/dist/dynamic-row-actions.d.ts.map +1 -1
  32. package/dist/dynamic-row-actions.js +14 -2
  33. package/dist/dynamic-table.d.ts.map +1 -1
  34. package/dist/dynamic-table.js +28 -3
  35. package/dist/index.d.ts +2 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/reason-prompt.d.ts +59 -0
  39. package/dist/reason-prompt.d.ts.map +1 -0
  40. package/dist/reason-prompt.js +111 -0
  41. package/dist/types.d.ts +6 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/use-addon-settings.d.ts +17 -3
  44. package/dist/use-addon-settings.d.ts.map +1 -1
  45. package/dist/use-addon-settings.js +20 -8
  46. package/package.json +1 -1
  47. package/src/__tests__/dynamic-kanban.test.tsx +141 -0
  48. package/src/__tests__/prefill-from-record.test.ts +69 -0
  49. package/src/__tests__/reason-prompt.test.tsx +121 -0
  50. package/src/__tests__/use-addon-settings.test.ts +16 -1
  51. package/src/action-modal-dispatcher.tsx +145 -3
  52. package/src/activity-diff.tsx +10 -1
  53. package/src/business/business.test.ts +46 -0
  54. package/src/business/catalog.ts +8 -0
  55. package/src/business/index.ts +2 -0
  56. package/src/business/refund-destination.tsx +2 -2
  57. package/src/business/refund.ts +2 -1
  58. package/src/business/return-wizard.tsx +223 -0
  59. package/src/business/return.ts +181 -0
  60. package/src/dynamic-columns.tsx +5 -1
  61. package/src/dynamic-kanban.tsx +197 -19
  62. package/src/dynamic-relation.tsx +14 -1
  63. package/src/dynamic-row-actions.tsx +13 -1
  64. package/src/dynamic-table.tsx +22 -2
  65. package/src/index.ts +14 -0
  66. package/src/reason-prompt.tsx +247 -0
  67. package/src/types.ts +6 -0
  68. package/src/use-addon-settings.ts +26 -7
@@ -0,0 +1,112 @@
1
+ // Devolución de mercancía (ReturnWizard: POS, Pedido, Factura, RMA, OT, devolución
2
+ // a proveedor). Lógica pura, sin endpoints: cada consumidor cablea el envío.
3
+ // Flujo (benchmark §8.2): renglones → condición y destino → recepción → reembolso o NC.
4
+ import { roundMoney, toAmount } from './format';
5
+ import { validateRefund } from './refund';
6
+ export const RETURN_CONDITION_DESTINATIONS = {
7
+ sellable: ['stock'],
8
+ defective: ['scrap', 'warranty'],
9
+ };
10
+ /** Destino por defecto de una condición (vendible → stock, defectuoso → merma). */
11
+ export function defaultStockDestination(condition) {
12
+ return RETURN_CONDITION_DESTINATIONS[condition][0];
13
+ }
14
+ export function returnableQty(l) {
15
+ return Math.max(0, roundMoney(l.sold - (l.alreadyReturned ?? 0), 4));
16
+ }
17
+ export function computeReturnTotals(lines, choices) {
18
+ let base = 0;
19
+ let tax = 0;
20
+ let goodsLines = 0;
21
+ for (const l of lines) {
22
+ const c = choices[l.key];
23
+ const q = c ? toAmount(c.quantity) : 0;
24
+ if (q <= 0)
25
+ continue;
26
+ goodsLines += 1;
27
+ const b = roundMoney(q * l.unit_price);
28
+ base += b;
29
+ tax += roundMoney(b * (l.tax_rate ?? 0));
30
+ }
31
+ base = roundMoney(base);
32
+ tax = roundMoney(tax);
33
+ return { base, tax, total: roundMoney(base + tax), goodsLines };
34
+ }
35
+ /**
36
+ * Relación SAT (c_TipoRelacion) de la NC: 03 «devolución de mercancía» si regresan
37
+ * renglones con producto; 01 «nota de crédito de los documentos relacionados» si es
38
+ * solo ajuste de importe. Misma regla que aplica el backend al proyectar la NC.
39
+ */
40
+ export function creditNoteRelation(lines, choices) {
41
+ return lines.some((l) => !!l.product_id && toAmount(choices[l.key]?.quantity) > 0) ? '03' : '01';
42
+ }
43
+ export function validateReturnChoices(lines, choices, reason, requireReason = true) {
44
+ const errors = {};
45
+ let any = false;
46
+ for (const l of lines) {
47
+ const c = choices[l.key];
48
+ const q = c ? toAmount(c.quantity) : 0;
49
+ if (q === 0)
50
+ continue;
51
+ if (q < 0)
52
+ errors[`${l.key}.quantity`] = 'La cantidad no puede ser negativa';
53
+ else if (q > returnableQty(l))
54
+ errors[`${l.key}.quantity`] = `Máximo ${returnableQty(l)} por devolver`;
55
+ else
56
+ any = true;
57
+ if (c && !RETURN_CONDITION_DESTINATIONS[c.condition]?.includes(c.destination)) {
58
+ errors[`${l.key}.destination`] = 'Ese destino no aplica a la condición elegida';
59
+ }
60
+ }
61
+ let form;
62
+ if (!any && Object.keys(errors).length === 0)
63
+ form = 'Elige al menos un renglón a devolver';
64
+ else if (requireReason && !(reason ?? '').trim())
65
+ form = 'Indica el motivo de la devolución';
66
+ return { errors, form, valid: !form && Object.keys(errors).length === 0 };
67
+ }
68
+ export function returnSteps(opts) {
69
+ // Con almacén, la recepción la confirma almacén fuera del asistente: no es paso.
70
+ return opts.warehouseConnected ? ['lines', 'condition', 'refund'] : ['lines', 'condition', 'receive', 'refund'];
71
+ }
72
+ /** Única puerta de salida hacia el backend: solo renglones con cantidad > 0. */
73
+ export function serializeReturn(args) {
74
+ const lines = args.lines
75
+ .filter((l) => toAmount(args.choices[l.key]?.quantity) > 0)
76
+ .map((l) => {
77
+ const c = args.choices[l.key];
78
+ return { key: l.key, ...(l.product_id ? { product_id: l.product_id } : {}), quantity: toAmount(c.quantity), condition: c.condition, destination: c.destination };
79
+ });
80
+ const notes = (args.notes ?? '').trim();
81
+ return {
82
+ reason: args.reason.trim(),
83
+ ...(notes ? { notes } : {}),
84
+ received: args.received,
85
+ lines,
86
+ refund: args.refund.map((a) => ({ ...a, amount: roundMoney(a.amount) })),
87
+ ...(args.refund.some((a) => a.kind === 'credit_note') ? { credit_note: { relation: creditNoteRelation(args.lines, args.choices) } } : {}),
88
+ totals: computeReturnTotals(args.lines, args.choices),
89
+ };
90
+ }
91
+ /** Valida el paso de reembolso contra el total de la devolución. */
92
+ export function validateReturnRefund(total, refund, options) {
93
+ return validateRefund(total, refund, options);
94
+ }
95
+ export const RMA_TRANSITIONS = {
96
+ draft: ['authorized', 'rejected', 'cancelled'],
97
+ authorized: ['received', 'rejected', 'cancelled'],
98
+ received: ['settled', 'cancelled'],
99
+ settled: ['closed'],
100
+ closed: [],
101
+ rejected: [],
102
+ cancelled: [],
103
+ };
104
+ /** Estados a los que se llega con motivo obligatorio. */
105
+ export const RMA_REASON_REQUIRED = ['rejected', 'cancelled'];
106
+ export function canTransitionRma(from, to, reason) {
107
+ if (!RMA_TRANSITIONS[from]?.includes(to))
108
+ return { ok: false, error: `No se puede pasar de ${from} a ${to}` };
109
+ if (RMA_REASON_REQUIRED.includes(to) && !(reason ?? '').trim())
110
+ return { ok: false, error: 'Indica el motivo' };
111
+ return { ok: true };
112
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"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"}
@@ -891,7 +891,9 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
891
891
  }
892
892
  case 'tags': {
893
893
  const list = Array.isArray(value)
894
- ? value.map(String)
894
+ ? value.map((v) => v && typeof v === 'object'
895
+ ? String(v.label ?? v.name ?? v.key ?? '')
896
+ : String(v)).filter(Boolean)
895
897
  : value
896
898
  ? String(value)
897
899
  .split(',')
@@ -60,15 +60,63 @@ export declare function applyLaneTotalsOnMove<T extends {
60
60
  */
61
61
  export declare function formatLaneCount(shown: number, loaded: number, serverTotal: number | null, laneActive: boolean): string;
62
62
  /**
63
- * Picks the columns shown on a card: a `title` column (first searchable column,
64
- * else first text-ish column) and up to `maxFields` secondary columns. Excludes
65
- * the group_by column (it's the lane itself) and any column hidden from the
66
- * table view (visibility modal/list, or `hidden`).
63
+ * Picks the columns shown on a card: a `title` column and the secondary fields.
64
+ * Excludes the group_by column (it's the lane itself) and any column hidden
65
+ * from the table view (visibility modal/list, or `hidden`).
66
+ *
67
+ * A model can lay its card out explicitly through the columns' manifest
68
+ * `display_config`: `card_title: true` picks the title, `card: true` marks each
69
+ * field the card shows, in column order (`explicit` is then true and ALL the
70
+ * flagged fields are shown — the card is no longer capped at `maxFields`).
71
+ * Without any flag the legacy heuristic applies: the first searchable / text
72
+ * column is the title and the next `maxFields` visible columns are the fields.
67
73
  */
68
74
  export declare function selectCardColumns(metadata: TableMetadata, maxFields?: number): {
69
75
  title: ColumnDefinition | null;
70
76
  fields: ColumnDefinition[];
77
+ explicit: boolean;
71
78
  };
79
+ /**
80
+ * Whether a card's date field is past due — the SDK paints it as a warning.
81
+ * A column opts in with `display_config.overdue: true`; the optional
82
+ * `display_config.overdue_unless` (`{ <field>: [values…] }`) exempts cards whose
83
+ * row matches (a work order already delivered is never late). Pure — exported
84
+ * for unit tests.
85
+ */
86
+ export declare function isCardOverdue(card: any, col: ColumnDefinition, now?: number): boolean;
87
+ /**
88
+ * The metadata regrouped by another column (a nav entry's `group_by`). A model
89
+ * can serve several boards over the same records — by stage, by technician, by
90
+ * bay — but the host projects ONE `group_by` per model, so the entry's own wins.
91
+ * The stage machine (stages, transitions, smart lanes) belongs to the model's
92
+ * stage column (`stage_field`; older hosts that do not serve it: the served
93
+ * group_by): grouping by it keeps the machine, grouping by anything else drops
94
+ * it and the lanes come from the new column. Returns the SAME object when there
95
+ * is nothing to change (no override, the served group_by already, or a column the
96
+ * model does not have). Pure — exported for unit tests.
97
+ */
98
+ export declare function withGroupBy(metadata: TableMetadata | null, groupBy: string | undefined): TableMetadata | null;
99
+ /**
100
+ * Whether the board's lanes must come from the group_by column's REFERENCE
101
+ * (one lane per technician, per bay, per owner…): the metadata declares no
102
+ * stages, the column has no inline options, but it points at a record list.
103
+ * Pure — exported for unit tests.
104
+ */
105
+ export declare function refLaneSource(metadata: TableMetadata | null): {
106
+ ref?: string;
107
+ endpoint?: string;
108
+ } | null;
109
+ /**
110
+ * The metadata with one lane per resolved option of a reference group_by column
111
+ * (labels ordered alphabetically). No transitions are declared, so a card can
112
+ * be dragged to any lane — i.e. re-assigned to another technician / bay.
113
+ * Returns the metadata untouched when there are no options. Pure.
114
+ */
115
+ export declare function withRefLanes(metadata: TableMetadata, options: {
116
+ value: string | number;
117
+ label: string;
118
+ color?: string | null;
119
+ }[]): TableMetadata;
72
120
  /**
73
121
  * The value a card cell should render for a column — same resolution the
74
122
  * table's cells apply. An FK column (`<rel>_id`) prefers the backend-resolved
@@ -208,6 +256,16 @@ export interface DynamicKanbanProps {
208
256
  timeZone?: string;
209
257
  /** ISO 4217 currency for money card fields (org config). */
210
258
  currency?: string;
259
+ /**
260
+ * Column to group the board by, when the host's nav entry asks for one
261
+ * (`?group_by=`): a model can offer several boards over the same records —
262
+ * by stage, by technician, by bay. Absent or equal to the served
263
+ * `group_by` → the model's own stage machine. Otherwise the lanes come from
264
+ * that column (its options, or its referenced records) with no transitions,
265
+ * and the per-org stage customisations (custom lanes, overrides, lane order,
266
+ * automations) are off: they belong to the stage column.
267
+ */
268
+ groupBy?: string;
211
269
  /**
212
270
  * Static equality filters always applied to the board (never shown as a
213
271
  * removable chip). Same contract as DynamicTable's `defaultFilters`.
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
@@ -26,6 +26,7 @@ import { useFlipAnimation } from './use-flip-animation';
26
26
  import { PortalDragOverlay } from './portal-drag-overlay';
27
27
  import { objectLabel } from './dynamic-relation-helpers';
28
28
  import { useMetadataCache } from './metadata-cache';
29
+ import { useOptionsResolver } from './use-options-resolver';
29
30
  import { ActivityValueRenderer } from './activity-value-renderer';
30
31
  import { isColumnVisibleInTable } from './column-visibility';
31
32
  import { aggregateOf, formatAggregateTotal, isRowActionVisible, relationKeyFor, RowActionMenuItem, } from './dynamic-columns';
@@ -173,11 +174,22 @@ export function formatLaneCount(shown, loaded, serverTotal, laneActive) {
173
174
  }
174
175
  return String(shown);
175
176
  }
177
+ /** Reads a boolean-ish opt-in from a column's `styleConfig` (`display_config`). */
178
+ function cardFlag(col, key) {
179
+ const v = col.styleConfig?.[key];
180
+ return v === true || v === 'true' || v === 1;
181
+ }
176
182
  /**
177
- * Picks the columns shown on a card: a `title` column (first searchable column,
178
- * else first text-ish column) and up to `maxFields` secondary columns. Excludes
179
- * the group_by column (it's the lane itself) and any column hidden from the
180
- * table view (visibility modal/list, or `hidden`).
183
+ * Picks the columns shown on a card: a `title` column and the secondary fields.
184
+ * Excludes the group_by column (it's the lane itself) and any column hidden
185
+ * from the table view (visibility modal/list, or `hidden`).
186
+ *
187
+ * A model can lay its card out explicitly through the columns' manifest
188
+ * `display_config`: `card_title: true` picks the title, `card: true` marks each
189
+ * field the card shows, in column order (`explicit` is then true and ALL the
190
+ * flagged fields are shown — the card is no longer capped at `maxFields`).
191
+ * Without any flag the legacy heuristic applies: the first searchable / text
192
+ * column is the title and the next `maxFields` visible columns are the fields.
181
193
  */
182
194
  export function selectCardColumns(metadata, maxFields = 3) {
183
195
  const groupBy = metadata.group_by;
@@ -185,14 +197,110 @@ export function selectCardColumns(metadata, maxFields = 3) {
185
197
  !c.hidden &&
186
198
  isColumnVisibleInTable(c) &&
187
199
  c.key !== 'id');
188
- const title = visible.find((c) => c.searchable) ??
200
+ const flagged = visible.filter((c) => cardFlag(c, 'card'));
201
+ const flaggedTitle = visible.find((c) => cardFlag(c, 'card_title'));
202
+ const explicit = flagged.length > 0 || !!flaggedTitle;
203
+ const title = flaggedTitle ??
204
+ visible.find((c) => c.searchable) ??
189
205
  visible.find((c) => c.type === 'text' || c.cellStyle === 'truncate-text') ??
190
206
  visible[0] ??
191
207
  null;
192
- const fields = visible
193
- .filter((c) => c.key !== title?.key)
194
- .slice(0, maxFields);
195
- return { title, fields };
208
+ const fields = explicit
209
+ ? flagged.filter((c) => c.key !== title?.key)
210
+ : visible.filter((c) => c.key !== title?.key).slice(0, maxFields);
211
+ return { title, fields, explicit };
212
+ }
213
+ /**
214
+ * Whether a card's date field is past due — the SDK paints it as a warning.
215
+ * A column opts in with `display_config.overdue: true`; the optional
216
+ * `display_config.overdue_unless` (`{ <field>: [values…] }`) exempts cards whose
217
+ * row matches (a work order already delivered is never late). Pure — exported
218
+ * for unit tests.
219
+ */
220
+ export function isCardOverdue(card, col, now = Date.now()) {
221
+ if (!cardFlag(col, 'overdue'))
222
+ return false;
223
+ const raw = card?.[col.key];
224
+ if (raw === null || raw === undefined || raw === '')
225
+ return false;
226
+ const at = typeof raw === 'number' ? raw : Date.parse(String(raw));
227
+ if (!Number.isFinite(at) || at >= now)
228
+ return false;
229
+ const unless = col.styleConfig?.overdue_unless;
230
+ if (unless && typeof unless === 'object') {
231
+ for (const [field, values] of Object.entries(unless)) {
232
+ const list = Array.isArray(values) ? values : [values];
233
+ if (list.map(String).includes(String(card?.[field] ?? '')))
234
+ return false;
235
+ }
236
+ }
237
+ return true;
238
+ }
239
+ /**
240
+ * The metadata regrouped by another column (a nav entry's `group_by`). A model
241
+ * can serve several boards over the same records — by stage, by technician, by
242
+ * bay — but the host projects ONE `group_by` per model, so the entry's own wins.
243
+ * The stage machine (stages, transitions, smart lanes) belongs to the model's
244
+ * stage column (`stage_field`; older hosts that do not serve it: the served
245
+ * group_by): grouping by it keeps the machine, grouping by anything else drops
246
+ * it and the lanes come from the new column. Returns the SAME object when there
247
+ * is nothing to change (no override, the served group_by already, or a column the
248
+ * model does not have). Pure — exported for unit tests.
249
+ */
250
+ export function withGroupBy(metadata, groupBy) {
251
+ if (!metadata || !groupBy || groupBy === metadata.group_by)
252
+ return metadata;
253
+ if (!metadata.columns.some((c) => c.key === groupBy))
254
+ return metadata;
255
+ const stageColumn = metadata.stage_field ?? (metadata.stages?.length ? metadata.group_by : undefined);
256
+ if (groupBy === stageColumn)
257
+ return { ...metadata, group_by: groupBy };
258
+ return {
259
+ ...metadata,
260
+ group_by: groupBy,
261
+ stages: undefined,
262
+ transitions: undefined,
263
+ smart_lanes: undefined,
264
+ };
265
+ }
266
+ /**
267
+ * Whether the board's lanes must come from the group_by column's REFERENCE
268
+ * (one lane per technician, per bay, per owner…): the metadata declares no
269
+ * stages, the column has no inline options, but it points at a record list.
270
+ * Pure — exported for unit tests.
271
+ */
272
+ export function refLaneSource(metadata) {
273
+ if (!metadata?.group_by)
274
+ return null;
275
+ if ((metadata.stages?.length ?? 0) > 0)
276
+ return null;
277
+ const col = metadata.columns.find((c) => c.key === metadata.group_by);
278
+ if (!col || (col.options?.length ?? 0) > 0)
279
+ return null;
280
+ if (col.searchEndpoint)
281
+ return { endpoint: col.searchEndpoint };
282
+ if (col.ref)
283
+ return { ref: col.ref };
284
+ return null;
285
+ }
286
+ /**
287
+ * The metadata with one lane per resolved option of a reference group_by column
288
+ * (labels ordered alphabetically). No transitions are declared, so a card can
289
+ * be dragged to any lane — i.e. re-assigned to another technician / bay.
290
+ * Returns the metadata untouched when there are no options. Pure.
291
+ */
292
+ export function withRefLanes(metadata, options) {
293
+ if (options.length === 0)
294
+ return metadata;
295
+ const stages = [...options]
296
+ .sort((a, b) => String(a.label).localeCompare(String(b.label)))
297
+ .map((o, i) => ({
298
+ key: String(o.value),
299
+ label: String(o.label),
300
+ color: o.color ?? undefined,
301
+ order: i,
302
+ }));
303
+ return { ...metadata, stages };
196
304
  }
197
305
  /** The all-zeros UUID — a Go zero-value FK serialized as "set" when it isn't. */
198
306
  const ZERO_UUID = /^0{8}-0{4}-0{4}-0{4}-0{12}$/;
@@ -461,7 +569,7 @@ const CARD_FIELD_CANDIDATES = 8;
461
569
  const CARD_FIELDS_SHOWN = 3;
462
570
  /** No card move in flight (a stable empty snapshot). */
463
571
  const NO_MOVES = Object.freeze({});
464
- function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtimeProp, onCardClick, onAction, pageSize = 50, lanePageSize = 25, timeZone: timeZoneProp, currency: currencyProp, defaultFilters, }) {
572
+ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtimeProp, onCardClick, onAction, pageSize = 50, lanePageSize = 25, timeZone: timeZoneProp, currency: currencyProp, defaultFilters, groupBy: groupByProp, }) {
465
573
  const { t, i18n } = useTranslation();
466
574
  // The org's timezone/currency: an explicit prop wins, else the app-wide
467
575
  // OrgRuntimeProvider (without it, money fell back to USD).
@@ -480,15 +588,16 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
480
588
  });
481
589
  // Stage automations (Bitrix-style per-lane rules). Degrades to no-op when
482
590
  // the host has no `/stage-automations` endpoint — the ⚡ affordance hides.
483
- const automations = useStageAutomations(model);
591
+ const automationsHook = useStageAutomations(model);
484
592
  // Custom stages (Bitrix-style user-defined columns). Degrades to no-op when
485
593
  // the host has no `/custom-stages` endpoint — the "+ Agregar etapa" column
486
594
  // and lane menus simply don't render.
487
- const customStages = useCustomStages(model);
595
+ const customStagesHook = useCustomStages(model);
488
596
  // Per-org overrides for DECLARED lanes (rename/recolor/conditions). Degrades
489
597
  // to no-op when the host has no `/stage-overrides` endpoint — the ⚙ gear then
490
598
  // hides on declared lanes (custom lanes keep it via /custom-stages).
491
- const stageOverrides = useStageOverrides(model);
599
+ const stageOverridesHook = useStageOverrides(model);
600
+ const stageLayoutHook = useStageLayout(model);
492
601
  // Dialog state: create/edit a stage, and the delete confirmation.
493
602
  const [stageDialogOpen, setStageDialogOpen] = useState(false);
494
603
  const [editingStage, setEditingStage] = useState(null);
@@ -508,7 +617,28 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
508
617
  }, []);
509
618
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
510
619
  const cachedMeta = getMetadata(model);
511
- const [metadata, setMetadata] = useState(cachedMeta || null);
620
+ const [serverMetadata, setMetadata] = useState(cachedMeta || null);
621
+ // A reference group_by (technician, bay…) has no declared stages: its lanes
622
+ // are the referenced records, resolved through the same options endpoint the
623
+ // form selects use.
624
+ const routedMetadata = useMemo(() => withGroupBy(serverMetadata, groupByProp), [serverMetadata, groupByProp]);
625
+ // Grouped by something other than the stage column: the stage machine is gone.
626
+ const regrouped = (serverMetadata?.stages?.length ?? 0) > 0 && (routedMetadata?.stages?.length ?? 0) === 0;
627
+ const laneSource = useMemo(() => refLaneSource(routedMetadata), [routedMetadata]);
628
+ const { options: laneOptions } = useOptionsResolver({
629
+ modelKey: '',
630
+ fieldKey: 'id',
631
+ ref: laneSource?.ref,
632
+ endpoint: laneSource?.endpoint,
633
+ limit: 200,
634
+ enabled: !!laneSource,
635
+ });
636
+ const metadata = useMemo(() => (routedMetadata && laneSource ? withRefLanes(routedMetadata, laneOptions) : routedMetadata), [routedMetadata, laneSource, laneOptions]);
637
+ // The stage customisations belong to the stage column: off on another grouping.
638
+ const automations = useMemo(() => (regrouped ? { ...automationsHook, available: false } : automationsHook), [regrouped, automationsHook]);
639
+ const customStages = useMemo(() => (regrouped ? { ...customStagesHook, available: false } : customStagesHook), [regrouped, customStagesHook]);
640
+ const stageOverrides = useMemo(() => (regrouped ? { ...stageOverridesHook, available: false } : stageOverridesHook), [regrouped, stageOverridesHook]);
641
+ const stageLayout = useMemo(() => (regrouped ? { ...stageLayoutHook, available: false } : stageLayoutHook), [regrouped, stageLayoutHook]);
512
642
  const [records, setRecords] = useState([]);
513
643
  const [loading, setLoading] = useState(!cachedMeta);
514
644
  const [loadingData, setLoadingData] = useState(true);
@@ -532,7 +662,6 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
532
662
  // the metadata (the backend also stamps `stages[]/smart_lanes[].order`, so the
533
663
  // board already paints ordered on load — this only backs the live drag + the
534
664
  // revert-on-failure). Null → follow the metadata order.
535
- const stageLayout = useStageLayout(model);
536
665
  const [laneOrderOverride, setLaneOrderOverride] = useState(null);
537
666
  const laneReorderEnabled = stageLayout.available;
538
667
  // Monotonic token for the current board load. Bumped on every fetchData so a
@@ -969,9 +1098,9 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
969
1098
  const grouped = useMemo(() => groupByStage(displayRecords, groupByKey, stages), [displayRecords, groupByKey, stages]);
970
1099
  // Card fields: a few more candidates than a card shows, so a card whose
971
1100
  // first fields are empty shows the next ones that have a value.
972
- const { title: titleCol, fields: fieldCols } = useMemo(() => metadata
1101
+ const { title: titleCol, fields: fieldCols, explicit: explicitCard, } = useMemo(() => metadata
973
1102
  ? selectCardColumns(metadata, CARD_FIELD_CANDIDATES)
974
- : { title: null, fields: [] }, [metadata]);
1103
+ : { title: null, fields: [], explicit: false }, [metadata]);
975
1104
  // Columns the lane search scans: the card title + its visible field cells.
976
1105
  const searchCols = useMemo(() => [titleCol, ...fieldCols].filter(Boolean), [titleCol, fieldCols]);
977
1106
  // Row-placement actions resolved EXACTLY like DynamicTable's action column:
@@ -1412,7 +1541,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
1412
1541
  extraFilters,
1413
1542
  configurable: isConfigurable(stage),
1414
1543
  onConfigure: () => openConfigStage(stage),
1415
- children: loadingData && cards.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : cards.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('kanban.emptyLane', { defaultValue: 'Sin tarjetas' }) })) : (cards.map((card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, pending: card.id != null && String(card.id) in moveOverrides }, String(card.id))))),
1544
+ children: loadingData && cards.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] })) : cards.length === 0 ? (_jsx("p", { className: "px-1 py-6 text-center text-xs text-muted-foreground", children: t('kanban.emptyLane', { defaultValue: 'Sin tarjetas' }) })) : (cards.map((card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, pending: card.id != null && String(card.id) in moveOverrides }, String(card.id))))),
1416
1545
  };
1417
1546
  };
1418
1547
  // Props for one smart (virtual) lane — a read-only, filter-defined column.
@@ -1426,7 +1555,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
1426
1555
  refreshTrigger,
1427
1556
  onEdit: openEditStage,
1428
1557
  onDelete: openDeleteStage,
1429
- renderCard: (card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, draggable: false })),
1558
+ renderCard: (card) => (_jsx(KanbanCard, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, actions: rowActions, locale: i18n.language, timeZone: timeZone, currency: currency, onClick: onCardClick, onAction: handleInternalAction, draggable: false })),
1430
1559
  });
1431
1560
  const showResetOrder = laneReorderEnabled && (stageLayout.hasCustomLayout || !!laneOrderOverride);
1432
1561
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", "data-testid": "kanban-filters", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: t('kanban.searchPlaceholder', { defaultValue: 'Buscar...' }), className: "h-8 w-52 pl-8 text-sm" })] }), filterFields.length > 0 && (_jsxs(Sheet, { open: filtersOpen, onOpenChange: setFiltersOpen, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5", children: [_jsx(ListFilter, { className: "h-3.5 w-3.5" }), t('kanban.filters', { defaultValue: 'Filtros' }), activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-0.5 h-4 min-w-4 justify-center rounded-full px-1 text-[10px] tabular-nums", children: activeFilterCount }))] }) }), _jsxs(SheetContent, { side: "right", className: "flex w-80 flex-col gap-0 p-0 sm:max-w-sm", children: [_jsx(SheetHeader, { className: "space-y-0 border-b px-4 py-3", children: _jsx(SheetTitle, { className: "text-sm", children: t('kanban.filters', { defaultValue: 'Filtros' }) }) }), _jsxs("div", { className: "flex flex-1 flex-col gap-1 overflow-y-auto px-4 py-3", children: [activeFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { className: "px-0.5 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: t('kanban.activeFilters', {
@@ -1449,7 +1578,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
1449
1578
  // drop itself is refused in onDragEnd.
1450
1579
  droppableDisabled: false, laneProps: buildLaneProps(lane.stage) }, lane.stage.key)) : laneReorderEnabled ? (_jsx(SortableSmartLane, { droppableDisabled: !!activeId, smartProps: buildSmartProps(lane.stage) }, `smart-${lane.stage.key}`)) : (_jsx(SmartLane, { ...buildSmartProps(lane.stage) }, `smart-${lane.stage.key}`))), unassignedStage && (_jsx(DroppableStageLane, {
1451
1580
  // "Sin etapa" is not a value a card can be given.
1452
- droppableDisabled: !!activeId, laneProps: buildLaneProps(unassignedStage) }, UNASSIGNED_LANE)), customStages.available && (_jsx(AddStageColumn, { onClick: openCreateStage }))] }) }), _jsx(PortalDragOverlay, { dropAnimation: DROP_ANIMATION, zIndex: 60, children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, locale: i18n.language, timeZone: timeZone, currency: currency, blocked: overBlocked })) : null }), rowActionDialogs] }), customStages.available && (_jsxs(_Fragment, { children: [_jsx(CustomStageDialog, { open: stageDialogOpen, onOpenChange: setStageDialogOpen, model: model, columns: metadata?.columns ?? [], initial: editingStage, nextPosition: nextStagePosition, onCreate: customStages.create, onUpdate: customStages.update }), _jsx(CustomStageDeleteDialog, { open: !!deletingStage, onOpenChange: (o) => {
1581
+ droppableDisabled: !!activeId, laneProps: buildLaneProps(unassignedStage) }, UNASSIGNED_LANE)), customStages.available && (_jsx(AddStageColumn, { onClick: openCreateStage }))] }) }), _jsx(PortalDragOverlay, { dropAnimation: DROP_ANIMATION, zIndex: 60, children: activeCard ? (_jsx(CardPreview, { card: activeCard, titleCol: titleCol, fieldCols: fieldCols, showAllFields: explicitCard, locale: i18n.language, timeZone: timeZone, currency: currency, blocked: overBlocked })) : null }), rowActionDialogs] }), customStages.available && (_jsxs(_Fragment, { children: [_jsx(CustomStageDialog, { open: stageDialogOpen, onOpenChange: setStageDialogOpen, model: model, columns: metadata?.columns ?? [], initial: editingStage, nextPosition: nextStagePosition, onCreate: customStages.create, onUpdate: customStages.update }), _jsx(CustomStageDeleteDialog, { open: !!deletingStage, onOpenChange: (o) => {
1453
1582
  if (!o)
1454
1583
  setDeletingStage(null);
1455
1584
  }, stage: deletingStage, reassignTargets: stages.map((s) => ({
@@ -1738,11 +1867,13 @@ function LaneFilterButton({ fields, value, onChange, }) {
1738
1867
  }
1739
1868
  // The title + the card's non-empty fields. Shared by the card and the drag
1740
1869
  // preview so the lifted card looks exactly like the one on the board.
1741
- function CardBody({ card, titleCol, fieldCols, locale, timeZone, currency, menu, }) {
1742
- const fields = visibleCardFields(card, fieldCols, CARD_FIELDS_SHOWN);
1743
- return (_jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol && !isEmptyCardValue(card, titleCol) ? (_jsx(ActivityValueRenderer, { value: cardCellValue(card, titleCol), col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsxs("span", { className: "truncate text-muted-foreground", children: ["#", String(card.id)] })) }), menu] }), fields.map((col) => (_jsxs("div", { className: "flex min-w-0 items-start gap-1.5 text-xs text-muted-foreground", children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 break-words", children: _jsx(ActivityValueRenderer, { value: cardCellValue(card, col), col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }));
1870
+ function CardBody({ card, titleCol, fieldCols, showAllFields, locale, timeZone, currency, menu, }) {
1871
+ const fields = visibleCardFields(card, fieldCols, showAllFields ? fieldCols.length : CARD_FIELDS_SHOWN);
1872
+ return (_jsxs(CardContent, { className: "space-y-1.5 p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("div", { className: "min-w-0 flex-1 break-words text-sm font-medium leading-snug", children: titleCol && !isEmptyCardValue(card, titleCol) ? (_jsx(ActivityValueRenderer, { value: cardCellValue(card, titleCol), col: titleCol, locale: locale, timeZone: timeZone, currency: currency })) : (_jsxs("span", { className: "truncate text-muted-foreground", children: ["#", String(card.id)] })) }), menu] }), fields.map((col) => (_jsxs("div", { className: `flex min-w-0 items-start gap-1.5 text-xs ${isCardOverdue(card, col)
1873
+ ? 'font-medium text-destructive'
1874
+ : 'text-muted-foreground'}`, "data-overdue": isCardOverdue(card, col) || undefined, children: [_jsxs("span", { className: "shrink-0 opacity-70", children: [col.label, ":"] }), _jsx("span", { className: "min-w-0 break-words", children: _jsx(ActivityValueRenderer, { value: cardCellValue(card, col), col: col, locale: locale, timeZone: timeZone, currency: currency }) })] }, col.key)))] }));
1744
1875
  }
1745
- function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, currency, onClick, onAction, draggable = true, pending = false, }) {
1876
+ function KanbanCard({ card, titleCol, fieldCols, showAllFields, actions, locale, timeZone, currency, onClick, onAction, draggable = true, pending = false, }) {
1746
1877
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
1747
1878
  id: String(card.id),
1748
1879
  data: { type: 'card' },
@@ -1755,10 +1886,10 @@ function KanbanCard({ card, titleCol, fieldCols, actions, locale, timeZone, curr
1755
1886
  e.stopPropagation();
1756
1887
  onAction(a.key, card);
1757
1888
  } }, a.key))) })] })) : null;
1758
- return (_jsx(Card, { ref: draggable ? setNodeRef : undefined, ...(draggable ? attributes : {}), ...(draggable ? listeners : {}), className: `w-full min-w-0 shrink-0 border-border/70 shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring ${draggable ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`, style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), "data-flip-key": draggable ? String(card.id) : undefined, "aria-busy": pending || undefined, children: _jsx(CardBody, { card: card, titleCol: titleCol, fieldCols: fieldCols, locale: locale, timeZone: timeZone, currency: currency, menu: menu }) }));
1889
+ return (_jsx(Card, { ref: draggable ? setNodeRef : undefined, ...(draggable ? attributes : {}), ...(draggable ? listeners : {}), className: `w-full min-w-0 shrink-0 border-border/70 shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring ${draggable ? 'cursor-grab active:cursor-grabbing' : 'cursor-default'}`, style: { opacity: isDragging ? 0.4 : 1 }, onClick: () => onClick?.(card), "data-card-id": String(card.id), "data-flip-key": draggable ? String(card.id) : undefined, "aria-busy": pending || undefined, children: _jsx(CardBody, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: showAllFields, locale: locale, timeZone: timeZone, currency: currency, menu: menu }) }));
1759
1890
  }
1760
1891
  // Static preview rendered inside the DragOverlay (no dnd hooks, no menu). It
1761
1892
  // fills the overlay box, which dnd-kit sizes to the card being dragged.
1762
- function CardPreview({ card, titleCol, fieldCols, locale, timeZone, currency, blocked = false, }) {
1763
- return (_jsx(Card, { className: `h-full w-full shadow-xl transition-[border-color] ${blocked ? 'border-destructive/60' : 'border-primary/40'}`, style: { cursor: blocked ? 'not-allowed' : 'grabbing' }, "data-testid": "kanban-drag-preview", "data-blocked": blocked || undefined, children: _jsx(CardBody, { card: card, titleCol: titleCol, fieldCols: fieldCols, locale: locale, timeZone: timeZone, currency: currency }) }));
1893
+ function CardPreview({ card, titleCol, fieldCols, showAllFields, locale, timeZone, currency, blocked = false, }) {
1894
+ return (_jsx(Card, { className: `h-full w-full shadow-xl transition-[border-color] ${blocked ? 'border-destructive/60' : 'border-primary/40'}`, style: { cursor: blocked ? 'not-allowed' : 'grabbing' }, "data-testid": "kanban-drag-preview", "data-blocked": blocked || undefined, children: _jsx(CardBody, { card: card, titleCol: titleCol, fieldCols: fieldCols, showAllFields: showAllFields, locale: locale, timeZone: timeZone, currency: currency }) }));
1764
1895
  }
@@ -3,6 +3,10 @@ export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildR
3
3
  export interface DynamicRelationStrings {
4
4
  title: string;
5
5
  emptyState: string;
6
+ /** Sub-tabla sin permiso de lectura (HTTP 403). Default: "Sin permiso para ver estos registros." */
7
+ forbiddenState?: string;
8
+ /** Falla al cargar la sub-tabla (no 403). Default: "No se pudieron cargar los registros relacionados." */
9
+ errorState?: string;
6
10
  addLabel: string;
7
11
  editLabel: string;
8
12
  removeLabel: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AAmEA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CACrB;AAwBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAK1D"}
1
+ {"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AAmEA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0GAA0G;IAC1G,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CACrB;AAwBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAK1D"}
@@ -73,6 +73,9 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
73
73
  const [optionsMap, setOptionsMap] = useState(new Map());
74
74
  const [rows, setRows] = useState([]);
75
75
  const [loading, setLoading] = useState(true);
76
+ // HTTP status of the last failed fetch (null = ok). Without it a 403/500
77
+ // rendered the empty state and read as "no related rows" (PIT-045).
78
+ const [fetchErrorStatus, setFetchErrorStatus] = useState(null);
76
79
  // Paginación server-side + scroll infinito, igual que <DynamicTable>.
77
80
  const [total, setTotal] = useState(0);
78
81
  const [page, setPage] = useState(1);
@@ -158,6 +161,7 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
158
161
  cacheMetadata(model, fresh);
159
162
  }
160
163
  const list = dataRes.data;
164
+ setFetchErrorStatus(null);
161
165
  if (list.success) {
162
166
  const fetched = list.data || [];
163
167
  setRows((prev) => (append ? dedupeById(prev, fetched) : fetched));
@@ -171,6 +175,7 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
171
175
  }
172
176
  catch (err) {
173
177
  console.error('DynamicRelation fetch error', err);
178
+ setFetchErrorStatus(err?.response?.status ?? 0);
174
179
  }
175
180
  finally {
176
181
  if (append)
@@ -300,7 +305,11 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
300
305
  setSubmitting(false);
301
306
  }
302
307
  }, [api, dataEndpoint, fetchAll, onChange, rowToDelete]);
303
- return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate || showSearch) && (_jsxs("div", { className: "flex items-center justify-between gap-2 pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [showSearch && (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: labels.searchPlaceholder, className: "h-8 w-40 ps-7 sm:w-56" })] })), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx(OptionsContext.Provider, { value: optionsCtx, children: _jsxs("div", { ref: rootRef, className: "overflow-auto max-h-[60vh] border rounded-md bg-card", children: [_jsxs(Table, { noWrapper: true, className: "w-full", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: "border-b-0 hover:bg-transparent", children: headerGroup.headers.map((header) => {
308
+ return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate || showSearch) && (_jsxs("div", { className: "flex items-center justify-between gap-2 pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [showSearch && (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: labels.searchPlaceholder, className: "h-8 w-40 ps-7 sm:w-56" })] })), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: fetchErrorStatus === 403
309
+ ? (labels.forbiddenState ?? 'Sin permiso para ver estos registros.')
310
+ : fetchErrorStatus !== null
311
+ ? (labels.errorState ?? 'No se pudieron cargar los registros relacionados.')
312
+ : labels.emptyState })) : (_jsx(OptionsContext.Provider, { value: optionsCtx, children: _jsxs("div", { ref: rootRef, className: "overflow-auto max-h-[60vh] border rounded-md bg-card", children: [_jsxs(Table, { noWrapper: true, className: "w-full", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: "border-b-0 hover:bg-transparent", children: headerGroup.headers.map((header) => {
304
313
  const isActions = header.id === 'actions';
305
314
  return (_jsx(TableHead, { colSpan: header.colSpan, style: header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined, className: cn('bg-card border-b h-10', isActions && 'text-right'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
306
315
  }) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, idx) => (_jsx(TableRow, { children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: "py-2", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, relationRowKey(row.original, idx, foreignKey)))) })] }), loadingMore && (_jsx("div", { className: "p-2", children: _jsx(Skeleton, { className: "h-8 w-full" }) })), _jsx("div", { ref: sentinelRef, "aria-hidden": true, className: "h-px w-full" })] }) })), !loading && rows.length > 0 && total > rows.length && (_jsx("div", { className: "pt-2 text-end text-xs text-muted-foreground", children: labels.countLabel
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,SAAS,CAAA;AAE3E,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CAkIhD"}
1
+ {"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,SAAS,CAAA;AAE3E,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CA6IhD"}