@asteby/metacore-runtime-react 18.3.0 → 18.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c7fb1ad: Org-currency-aware money formatting in dynamic tables + the record dialog.
8
+
9
+ `<DynamicTable>` and `<DynamicRecordDialog>` now accept an optional `currency`
10
+ prop (the org's ISO-4217 code, e.g. `MXN`, threaded from org config like
11
+ `timeZone`). Money columns (`type:'number'` + `cellStyle:'currency'`) without an
12
+ explicit per-column currency now fall back to the org currency instead of
13
+ hardcoded `USD` — `resolveCurrency(col, orgCurrency)`. The record dialog, which
14
+ previously showed raw numbers, now formats money fields as a currency string in
15
+ the view renderer: a field is treated as money when the backend stamps
16
+ `cellStyle:'currency'`, or — as a robustness fallback mirroring the backend's
17
+ `inferDisplayCellStyle` — when it's numeric and its key matches the money
18
+ heuristic (`price`/`amount`/`total`/`cost`/`subtotal`/`balance`/`paid`, as the
19
+ whole key or a `_<m>`/`<m>_` affix). Editable inputs stay numeric.
20
+
3
21
  ## 18.3.0
4
22
 
5
23
  ### Minor Changes
@@ -45,6 +45,19 @@ export interface FieldDef {
45
45
  * its SQL type. Unknown values fall through to the `type`-based default.
46
46
  */
47
47
  widget?: string;
48
+ /**
49
+ * Declarative display hint the backend stamps on the column/modal field
50
+ * (mirrors the table column's `cellStyle`). `'currency'` makes the view
51
+ * renderer format the numeric value in the org currency. Optional —
52
+ * absent, a money-key heuristic still detects obvious money fields.
53
+ */
54
+ cellStyle?: string;
55
+ /**
56
+ * Per-field style overrides served alongside `cellStyle` (e.g.
57
+ * `{ currency: 'MXN' }`). When it carries an explicit `currency` it wins
58
+ * over the org fallback.
59
+ */
60
+ styleConfig?: Record<string, any>;
48
61
  }
49
62
  export interface DynamicRecordDialogProps {
50
63
  open: boolean;
@@ -118,9 +131,16 @@ export interface DynamicRecordDialogProps {
118
131
  * regardless of the viewer's browser timezone. Pure `date` values pin to UTC.
119
132
  */
120
133
  timeZone?: string;
134
+ /**
135
+ * Org ISO-4217 currency code (e.g. `MXN`) used as the fallback for money
136
+ * fields (`cellStyle:'currency'` or the money-key heuristic) that lack an
137
+ * explicit per-field currency. Optional — defaults to 'USD'.
138
+ */
139
+ currency?: string;
121
140
  }
122
- export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, }: DynamicRecordDialogProps): import("react").JSX.Element;
123
- export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, }: {
141
+ export declare function isMoneyField(field: FieldDef, value: any): boolean;
142
+ export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, }: DynamicRecordDialogProps): import("react").JSX.Element;
143
+ export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: {
124
144
  field: FieldDef;
125
145
  value: any;
126
146
  record: any;
@@ -128,5 +148,7 @@ export declare function ViewValue({ field, value: rawValue, record, getImageUrl:
128
148
  getImageUrl?: GetImageUrl;
129
149
  /** Optional override; when omitted falls back to the nearest provider. */
130
150
  timeZone?: string;
151
+ /** Optional override; when omitted falls back to the nearest provider. */
152
+ currency?: string;
131
153
  }): import("react").JSX.Element;
132
154
  //# sourceMappingURL=dynamic-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -158,7 +158,31 @@ const MODE_CONFIG = {
158
158
  // image leads, tz-aware dates) without prop-drilling through every renderer.
159
159
  const ModelContext = createContext('');
160
160
  const TimeZoneContext = createContext(undefined);
161
- export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, }) {
161
+ // Org ISO-4217 currency (org config, like the timezone) used as the fallback
162
+ // for money fields that don't carry an explicit per-field currency.
163
+ const CurrencyContext = createContext(undefined);
164
+ // Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
165
+ // dialog format obvious money fields as currency even when the backend hasn't
166
+ // stamped `cellStyle:'currency'` yet. Case-insensitive; matches a key that
167
+ // equals one of these, or ends with `_<m>`, or starts with `<m>_`.
168
+ const MONEY_KEY_HEURISTIC = ['price', 'amount', 'total', 'cost', 'subtotal', 'balance', 'paid'];
169
+ // isMoneyField decides whether a field should render as currency. The explicit
170
+ // `cellStyle:'currency'` stamp always wins; otherwise a numeric value whose key
171
+ // matches the money heuristic qualifies (robustness fallback).
172
+ export function isMoneyField(field, value) {
173
+ if (field.cellStyle === 'currency')
174
+ return true;
175
+ if (value === null || value === undefined || value === '')
176
+ return false;
177
+ const num = typeof value === 'number' ? value : Number(value);
178
+ if (isNaN(num))
179
+ return false;
180
+ const key = String(field.key || '').toLowerCase();
181
+ if (!key)
182
+ return false;
183
+ return MONEY_KEY_HEURISTIC.some(m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`));
184
+ }
185
+ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, }) {
162
186
  const api = useApi();
163
187
  const { t } = useTranslation();
164
188
  const [modalMeta, setModalMeta] = useState(schema ? schema : null);
@@ -387,10 +411,10 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
387
411
  return false;
388
412
  return true;
389
413
  }) ?? [];
390
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsxs(TimeZoneContext.Provider, { value: timeZone, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
391
- const isFullWidth = field.type === 'textarea';
392
- return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
393
- }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit' }) }))] }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
414
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
415
+ const isFullWidth = field.type === 'textarea';
416
+ return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
417
+ }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit' }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
394
418
  }
395
419
  function LoadingSkeleton() {
396
420
  return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
@@ -439,11 +463,14 @@ function RelationViewValue({ field, value, record }) {
439
463
  };
440
464
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(OptionLead, { option: lead, size: 24 }), _jsx("span", { className: "text-sm", children: label ?? '—' })] }));
441
465
  }
442
- export function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, }) {
466
+ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }) {
467
+ const { i18n } = useTranslation();
443
468
  const ctxImageUrl = useContext(ImageUrlContext);
444
469
  const ctxTimeZone = useContext(TimeZoneContext);
470
+ const ctxCurrency = useContext(CurrencyContext);
445
471
  const getImageUrl = getImageUrlProp ?? ctxImageUrl;
446
472
  const timeZone = timeZoneProp ?? ctxTimeZone;
473
+ const currency = currencyProp ?? ctxCurrency;
447
474
  // created_by / avatar resolver sibling → name (+ avatar) instead of "—".
448
475
  if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
449
476
  const user = createdBySibling(rawValue, record);
@@ -481,6 +508,23 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
481
508
  if (field.type === 'url' && value) {
482
509
  return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
483
510
  }
511
+ // Money → org-currency string. Detected by the backend `cellStyle:'currency'`
512
+ // stamp or a numeric value whose key matches the money heuristic (fallback
513
+ // mirroring the table cell + backend `inferDisplayCellStyle`).
514
+ if (isMoneyField(field, value)) {
515
+ const num = typeof value === 'number' ? value : Number(value);
516
+ if (!isNaN(num)) {
517
+ const resolvedCurrency = field.styleConfig?.currency || currency || 'USD';
518
+ const localeTag = i18n.language || 'es';
519
+ const formatted = new Intl.NumberFormat(localeTag, {
520
+ style: 'currency',
521
+ currency: resolvedCurrency,
522
+ minimumFractionDigits: 2,
523
+ maximumFractionDigits: 2,
524
+ }).format(num);
525
+ return _jsx("p", { className: "text-sm py-1 tabular-nums", children: formatted });
526
+ }
527
+ }
484
528
  // Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
485
529
  // day); instants render in the org timezone with a full-precision tooltip.
486
530
  if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp') {
@@ -16,7 +16,7 @@ export interface ColumnFilterConfig {
16
16
  searchEndpoint?: string;
17
17
  }
18
18
  /** Signature for the host-provided `getDynamicColumns` factory. */
19
- export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string) => ColumnDef<any>[];
19
+ export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string, currency?: string) => ColumnDef<any>[];
20
20
  /** Signature for the host-provided `DynamicIcon` renderer. */
21
21
  export type DynamicIconComponent = React.ComponentType<{
22
22
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
1
+ {"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
@@ -16,6 +16,12 @@ export interface DynamicColumnsHelpers {
16
16
  */
17
17
  apiBaseUrl?: string;
18
18
  }
19
+ /**
20
+ * Resolves the active currency for a column: the column's explicit currency
21
+ * style wins, then the org-level fallback (org config, like `timeZone`), then
22
+ * 'USD' as a last resort.
23
+ */
24
+ export declare const resolveCurrency: (col: ColumnDefinition, orgCurrency?: string) => string;
19
25
  /**
20
26
  * State-machine gate for per-row actions.
21
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAgGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AA0ED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAumBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAoErD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AA0ED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAwmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -44,8 +44,12 @@ const styleCfg = (col, ...keys) => {
44
44
  return undefined;
45
45
  };
46
46
  const EmptyCell = () => _jsx("span", { className: "text-muted-foreground", children: "-" });
47
- /** Resolves the active org currency, defaulting to USD when no override. */
48
- const resolveCurrency = (col) => styleCfg(col, 'currency') || 'USD';
47
+ /**
48
+ * Resolves the active currency for a column: the column's explicit currency
49
+ * style wins, then the org-level fallback (org config, like `timeZone`), then
50
+ * 'USD' as a last resort.
51
+ */
52
+ export const resolveCurrency = (col, orgCurrency) => styleCfg(col, 'currency') || orgCurrency || 'USD';
49
53
  const formatNumber = (value, opts, locale) => new Intl.NumberFormat(locale || undefined, opts).format(value);
50
54
  /**
51
55
  * Semantic status → badge color. Used by the `status` cell when no explicit
@@ -351,7 +355,7 @@ const AvatarCell = ({ name, desc, avatarSrc, getImageUrl }) => (_jsxs("div", { c
351
355
  export function makeDefaultGetDynamicColumns(helpers = {}) {
352
356
  const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
353
357
  const apiBaseUrl = helpers.apiBaseUrl ?? '';
354
- return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs, timeZone) {
358
+ return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs, timeZone, currency) {
355
359
  const dateLocale = currentLanguage === 'en' ? enUS : es;
356
360
  const columns = [
357
361
  {
@@ -552,7 +556,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
552
556
  const decimals = styleCfg(col, 'decimals') ?? 2;
553
557
  return (_jsx("span", { className: "block text-right font-medium tabular-nums", children: formatNumber(num, {
554
558
  style: 'currency',
555
- currency: resolveCurrency(col),
559
+ currency: resolveCurrency(col, currency),
556
560
  minimumFractionDigits: decimals,
557
561
  maximumFractionDigits: decimals,
558
562
  }, currentLanguage) }));
@@ -23,7 +23,13 @@ interface DynamicTableProps {
23
23
  * Optional — omitting it preserves the legacy browser-local formatting.
24
24
  */
25
25
  timeZone?: string;
26
+ /**
27
+ * ISO 4217 currency code (e.g. the org's `MXN`) used as the fallback for
28
+ * money cells (`type:'number'` + `cellStyle:'currency'`) that don't carry
29
+ * an explicit per-column currency. Optional — defaults to 'USD'.
30
+ */
31
+ currency?: string;
26
32
  }
27
- export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, }: DynamicTableProps): import("react").JSX.Element;
33
+ export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, currency, }: DynamicTableProps): import("react").JSX.Element;
28
34
  export {};
29
35
  //# sourceMappingURL=dynamic-table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,GACX,EAAE,iBAAiB,+BAgyBnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,GACX,EAAE,iBAAiB,+BAgyBnB"}
@@ -31,7 +31,7 @@ import { getSearchableColumnKeys } from './column-visibility';
31
31
  import { DynamicRecordDialog } from './dialogs/dynamic-record';
32
32
  import { ExportDialog } from './dialogs/export';
33
33
  import { ImportDialog } from './dialogs/import';
34
- export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColumns = [], onAction, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, }) {
34
+ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColumns = [], onAction, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, currency, }) {
35
35
  const { t, i18n } = useTranslation();
36
36
  const api = useApi();
37
37
  const currentBranch = useCurrentBranch();
@@ -555,12 +555,12 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
555
555
  const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
556
556
  ? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
557
557
  : metadata;
558
- const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone);
558
+ const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone, currency);
559
559
  const filteredBase = baseColumns.filter((col) => !hiddenColumns.includes(col.id));
560
560
  const actionsCol = filteredBase.find((c) => c.id === 'actions');
561
561
  const otherCols = filteredBase.filter((c) => c.id !== 'actions');
562
562
  return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])];
563
- }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone]);
563
+ }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency]);
564
564
  const filters = useMemo(() => [], []);
565
565
  const table = useReactTable({
566
566
  data,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.3.0",
3
+ "version": "18.4.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,62 @@
1
+ // Locks the org-currency fallback contract for money rendering: the table cell
2
+ // (`resolveCurrency`) and the record dialog (`isMoneyField`). Pure logic, no DOM.
3
+ import { describe, it, expect } from 'vitest'
4
+ import { resolveCurrency } from '../dynamic-columns'
5
+ import { isMoneyField } from '../dialogs/dynamic-record'
6
+ import type { ColumnDefinition } from '../types'
7
+ import type { FieldDef } from '../dialogs/dynamic-record'
8
+
9
+ const col = (over: Partial<ColumnDefinition>): ColumnDefinition => ({
10
+ key: 'total',
11
+ label: 'Total',
12
+ type: 'number',
13
+ sortable: true,
14
+ filterable: true,
15
+ ...over,
16
+ })
17
+
18
+ const field = (over: Partial<FieldDef>): FieldDef => ({
19
+ key: 'total',
20
+ label: 'Total',
21
+ type: 'number',
22
+ ...over,
23
+ })
24
+
25
+ describe('resolveCurrency', () => {
26
+ it("defaults to 'USD' when neither the column nor the org provide one", () => {
27
+ expect(resolveCurrency(col({}))).toBe('USD')
28
+ })
29
+
30
+ it('falls back to the org currency when the column has no explicit currency', () => {
31
+ expect(resolveCurrency(col({}), 'MXN')).toBe('MXN')
32
+ })
33
+
34
+ it('prefers the explicit per-column currency over the org fallback', () => {
35
+ expect(resolveCurrency(col({ styleConfig: { currency: 'EUR' } }), 'MXN')).toBe('EUR')
36
+ })
37
+ })
38
+
39
+ describe('isMoneyField', () => {
40
+ it("recognizes the backend cellStyle:'currency' stamp regardless of key", () => {
41
+ expect(isMoneyField(field({ key: 'foo', cellStyle: 'currency' }), 100)).toBe(true)
42
+ })
43
+
44
+ it('detects numeric money keys via the heuristic (no stamp needed)', () => {
45
+ expect(isMoneyField(field({ key: 'total' }), 100)).toBe(true)
46
+ expect(isMoneyField(field({ key: 'sub_total' }), 100)).toBe(true)
47
+ expect(isMoneyField(field({ key: 'tax_amount' }), 16)).toBe(true)
48
+ expect(isMoneyField(field({ key: 'unit_price' }), 5)).toBe(true)
49
+ expect(isMoneyField(field({ key: 'balance' }), 0)).toBe(true)
50
+ })
51
+
52
+ it('accepts numeric strings the backend serializes', () => {
53
+ expect(isMoneyField(field({ key: 'total' }), '100')).toBe(true)
54
+ })
55
+
56
+ it('ignores non-money keys and non-numeric values', () => {
57
+ expect(isMoneyField(field({ key: 'name' }), 'Acme')).toBe(false)
58
+ expect(isMoneyField(field({ key: 'quantity' }), 3)).toBe(false)
59
+ expect(isMoneyField(field({ key: 'total' }), 'n/a')).toBe(false)
60
+ expect(isMoneyField(field({ key: 'total' }), null)).toBe(false)
61
+ })
62
+ })
@@ -107,6 +107,19 @@ export interface FieldDef {
107
107
  * its SQL type. Unknown values fall through to the `type`-based default.
108
108
  */
109
109
  widget?: string
110
+ /**
111
+ * Declarative display hint the backend stamps on the column/modal field
112
+ * (mirrors the table column's `cellStyle`). `'currency'` makes the view
113
+ * renderer format the numeric value in the org currency. Optional —
114
+ * absent, a money-key heuristic still detects obvious money fields.
115
+ */
116
+ cellStyle?: string
117
+ /**
118
+ * Per-field style overrides served alongside `cellStyle` (e.g.
119
+ * `{ currency: 'MXN' }`). When it carries an explicit `currency` it wins
120
+ * over the org fallback.
121
+ */
122
+ styleConfig?: Record<string, any>
110
123
  }
111
124
 
112
125
  // Permissive shape: the wire payload may omit some fields (e.g. `title` is
@@ -208,6 +221,12 @@ export interface DynamicRecordDialogProps {
208
221
  * regardless of the viewer's browser timezone. Pure `date` values pin to UTC.
209
222
  */
210
223
  timeZone?: string
224
+ /**
225
+ * Org ISO-4217 currency code (e.g. `MXN`) used as the fallback for money
226
+ * fields (`cellStyle:'currency'` or the money-key heuristic) that lack an
227
+ * explicit per-field currency. Optional — defaults to 'USD'.
228
+ */
229
+ currency?: string
211
230
  }
212
231
 
213
232
  function resolvePath(obj: any, path: string): any {
@@ -332,6 +351,30 @@ const MODE_CONFIG = {
332
351
  // image leads, tz-aware dates) without prop-drilling through every renderer.
333
352
  const ModelContext = createContext('')
334
353
  const TimeZoneContext = createContext<string | undefined>(undefined)
354
+ // Org ISO-4217 currency (org config, like the timezone) used as the fallback
355
+ // for money fields that don't carry an explicit per-field currency.
356
+ const CurrencyContext = createContext<string | undefined>(undefined)
357
+
358
+ // Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
359
+ // dialog format obvious money fields as currency even when the backend hasn't
360
+ // stamped `cellStyle:'currency'` yet. Case-insensitive; matches a key that
361
+ // equals one of these, or ends with `_<m>`, or starts with `<m>_`.
362
+ const MONEY_KEY_HEURISTIC = ['price', 'amount', 'total', 'cost', 'subtotal', 'balance', 'paid']
363
+
364
+ // isMoneyField decides whether a field should render as currency. The explicit
365
+ // `cellStyle:'currency'` stamp always wins; otherwise a numeric value whose key
366
+ // matches the money heuristic qualifies (robustness fallback).
367
+ export function isMoneyField(field: FieldDef, value: any): boolean {
368
+ if (field.cellStyle === 'currency') return true
369
+ if (value === null || value === undefined || value === '') return false
370
+ const num = typeof value === 'number' ? value : Number(value)
371
+ if (isNaN(num)) return false
372
+ const key = String(field.key || '').toLowerCase()
373
+ if (!key) return false
374
+ return MONEY_KEY_HEURISTIC.some(
375
+ m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`),
376
+ )
377
+ }
335
378
 
336
379
  export function DynamicRecordDialog({
337
380
  open,
@@ -351,6 +394,7 @@ export function DynamicRecordDialog({
351
394
  initialRecord,
352
395
  getImageUrl = identityImageUrl,
353
396
  timeZone,
397
+ currency,
354
398
  }: DynamicRecordDialogProps) {
355
399
  const api = useApi()
356
400
  const { t } = useTranslation()
@@ -603,6 +647,7 @@ export function DynamicRecordDialog({
603
647
  <ModelContext.Provider value={model}>
604
648
  <ImageUrlContext.Provider value={getImageUrl}>
605
649
  <TimeZoneContext.Provider value={timeZone}>
650
+ <CurrencyContext.Provider value={currency}>
606
651
  <form
607
652
  id="dynamic-record-form"
608
653
  onSubmit={handleSubmit}
@@ -656,6 +701,7 @@ export function DynamicRecordDialog({
656
701
  />
657
702
  </div>
658
703
  )}
704
+ </CurrencyContext.Provider>
659
705
  </TimeZoneContext.Provider>
660
706
  </ImageUrlContext.Provider>
661
707
  </ModelContext.Provider>
@@ -810,6 +856,7 @@ export function ViewValue({
810
856
  record,
811
857
  getImageUrl: getImageUrlProp,
812
858
  timeZone: timeZoneProp,
859
+ currency: currencyProp,
813
860
  }: {
814
861
  field: FieldDef
815
862
  value: any
@@ -818,11 +865,16 @@ export function ViewValue({
818
865
  getImageUrl?: GetImageUrl
819
866
  /** Optional override; when omitted falls back to the nearest provider. */
820
867
  timeZone?: string
868
+ /** Optional override; when omitted falls back to the nearest provider. */
869
+ currency?: string
821
870
  }) {
871
+ const { i18n } = useTranslation()
822
872
  const ctxImageUrl = useContext(ImageUrlContext)
823
873
  const ctxTimeZone = useContext(TimeZoneContext)
874
+ const ctxCurrency = useContext(CurrencyContext)
824
875
  const getImageUrl = getImageUrlProp ?? ctxImageUrl
825
876
  const timeZone = timeZoneProp ?? ctxTimeZone
877
+ const currency = currencyProp ?? ctxCurrency
826
878
 
827
879
  // created_by / avatar resolver sibling → name (+ avatar) instead of "—".
828
880
  if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
@@ -904,6 +956,24 @@ export function ViewValue({
904
956
  )
905
957
  }
906
958
 
959
+ // Money → org-currency string. Detected by the backend `cellStyle:'currency'`
960
+ // stamp or a numeric value whose key matches the money heuristic (fallback
961
+ // mirroring the table cell + backend `inferDisplayCellStyle`).
962
+ if (isMoneyField(field, value)) {
963
+ const num = typeof value === 'number' ? value : Number(value)
964
+ if (!isNaN(num)) {
965
+ const resolvedCurrency = field.styleConfig?.currency || currency || 'USD'
966
+ const localeTag = i18n.language || 'es'
967
+ const formatted = new Intl.NumberFormat(localeTag, {
968
+ style: 'currency',
969
+ currency: resolvedCurrency,
970
+ minimumFractionDigits: 2,
971
+ maximumFractionDigits: 2,
972
+ }).format(num)
973
+ return <p className="text-sm py-1 tabular-nums">{formatted}</p>
974
+ }
975
+ }
976
+
907
977
  // Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
908
978
  // day); instants render in the org timezone with a full-precision tooltip.
909
979
  if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp') {
@@ -31,6 +31,7 @@ export type GetDynamicColumns = (
31
31
  language: string,
32
32
  columnFilterConfigs: Map<string, ColumnFilterConfig>,
33
33
  timeZone?: string,
34
+ currency?: string,
34
35
  ) => ColumnDef<any>[]
35
36
 
36
37
  /** Signature for the host-provided `DynamicIcon` renderer. */
@@ -93,9 +93,13 @@ const styleCfg = (
93
93
 
94
94
  const EmptyCell = () => <span className="text-muted-foreground">-</span>
95
95
 
96
- /** Resolves the active org currency, defaulting to USD when no override. */
97
- const resolveCurrency = (col: ColumnDefinition): string =>
98
- styleCfg(col, 'currency') || 'USD'
96
+ /**
97
+ * Resolves the active currency for a column: the column's explicit currency
98
+ * style wins, then the org-level fallback (org config, like `timeZone`), then
99
+ * 'USD' as a last resort.
100
+ */
101
+ export const resolveCurrency = (col: ColumnDefinition, orgCurrency?: string): string =>
102
+ styleCfg(col, 'currency') || orgCurrency || 'USD'
99
103
 
100
104
  const formatNumber = (
101
105
  value: number,
@@ -559,6 +563,7 @@ export function makeDefaultGetDynamicColumns(
559
563
  currentLanguage?: string,
560
564
  filterConfigs?: Map<string, ColumnFilterConfig>,
561
565
  timeZone?: string,
566
+ currency?: string,
562
567
  ): ColumnDef<any>[] {
563
568
  const dateLocale = currentLanguage === 'en' ? enUS : es
564
569
  const columns: ColumnDef<any>[] = [
@@ -842,7 +847,7 @@ export function makeDefaultGetDynamicColumns(
842
847
  num,
843
848
  {
844
849
  style: 'currency',
845
- currency: resolveCurrency(col),
850
+ currency: resolveCurrency(col, currency),
846
851
  minimumFractionDigits: decimals,
847
852
  maximumFractionDigits: decimals,
848
853
  },
@@ -97,6 +97,12 @@ interface DynamicTableProps {
97
97
  * Optional — omitting it preserves the legacy browser-local formatting.
98
98
  */
99
99
  timeZone?: string
100
+ /**
101
+ * ISO 4217 currency code (e.g. the org's `MXN`) used as the fallback for
102
+ * money cells (`type:'number'` + `cellStyle:'currency'`) that don't carry
103
+ * an explicit per-column currency. Optional — defaults to 'USD'.
104
+ */
105
+ currency?: string
100
106
  }
101
107
 
102
108
  export function DynamicTable({
@@ -110,6 +116,7 @@ export function DynamicTable({
110
116
  extraColumns = [],
111
117
  getDynamicColumns = defaultGetDynamicColumns,
112
118
  timeZone,
119
+ currency,
113
120
  }: DynamicTableProps) {
114
121
  const { t, i18n } = useTranslation()
115
122
  const api = useApi()
@@ -598,12 +605,12 @@ export function DynamicTable({
598
605
  const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
599
606
  ? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
600
607
  : metadata
601
- const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone)
608
+ const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone, currency)
602
609
  const filteredBase = baseColumns.filter((col: ColumnDef<any>) => !hiddenColumns.includes(col.id as string))
603
610
  const actionsCol = filteredBase.find((c: ColumnDef<any>) => c.id === 'actions')
604
611
  const otherCols = filteredBase.filter((c: ColumnDef<any>) => c.id !== 'actions')
605
612
  return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])]
606
- }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone])
613
+ }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency])
607
614
 
608
615
  const filters = useMemo(() => [], [])
609
616