@asteby/metacore-runtime-react 46.2.2 → 46.3.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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/action-modal-dispatcher.js +1 -1
  3. package/dist/business/catalog.d.ts.map +1 -1
  4. package/dist/business/catalog.js +16 -4
  5. package/dist/business/document-lines.d.ts +30 -0
  6. package/dist/business/document-lines.d.ts.map +1 -0
  7. package/dist/business/document-lines.js +65 -0
  8. package/dist/business/index.d.ts +1 -0
  9. package/dist/business/index.d.ts.map +1 -1
  10. package/dist/business/index.js +1 -0
  11. package/dist/business/line-items-editor.d.ts +28 -4
  12. package/dist/business/line-items-editor.d.ts.map +1 -1
  13. package/dist/business/line-items-editor.js +139 -14
  14. package/dist/business/line-items.d.ts +24 -2
  15. package/dist/business/line-items.d.ts.map +1 -1
  16. package/dist/business/line-items.js +50 -5
  17. package/dist/business/product-search.d.ts +4 -0
  18. package/dist/business/product-search.d.ts.map +1 -1
  19. package/dist/business/product-search.js +5 -1
  20. package/dist/dynamic-date-field.d.ts +3 -1
  21. package/dist/dynamic-date-field.d.ts.map +1 -1
  22. package/dist/dynamic-date-field.js +3 -2
  23. package/dist/dynamic-form-schema.d.ts +9 -0
  24. package/dist/dynamic-form-schema.d.ts.map +1 -1
  25. package/dist/dynamic-form-schema.js +61 -0
  26. package/dist/dynamic-line-items.d.ts.map +1 -1
  27. package/dist/dynamic-line-items.js +18 -8
  28. package/dist/dynamic-select-field.d.ts +3 -1
  29. package/dist/dynamic-select-field.d.ts.map +1 -1
  30. package/dist/dynamic-select-field.js +2 -1
  31. package/dist/use-options-resolver.d.ts +5 -5
  32. package/dist/use-options-resolver.d.ts.map +1 -1
  33. package/dist/use-options-resolver.js +11 -0
  34. package/package.json +1 -1
  35. package/src/__tests__/document-lines-grid.test.tsx +79 -0
  36. package/src/__tests__/line-item-formulas.test.ts +18 -1
  37. package/src/__tests__/use-options-resolver.test.ts +5 -0
  38. package/src/action-modal-dispatcher.tsx +1 -1
  39. package/src/business/business.test.ts +43 -0
  40. package/src/business/catalog.ts +16 -4
  41. package/src/business/document-lines.ts +91 -0
  42. package/src/business/index.ts +1 -0
  43. package/src/business/line-items-editor.tsx +365 -46
  44. package/src/business/line-items.ts +65 -7
  45. package/src/business/product-search.ts +9 -1
  46. package/src/dynamic-date-field.tsx +8 -2
  47. package/src/dynamic-form-schema.ts +60 -0
  48. package/src/dynamic-line-items.tsx +23 -3
  49. package/src/dynamic-select-field.tsx +4 -0
  50. package/src/use-options-resolver.ts +15 -0
@@ -25,7 +25,10 @@ export function computeLine(l) {
25
25
  if (l.kind !== 'item')
26
26
  return { gross: 0, discount: 0, net: 0, tax: 0, total: 0 };
27
27
  const gross = roundMoney(toAmount(l.quantity) * toAmount(l.unit_price));
28
- const discount = roundMoney(gross * (toAmount(l.discount) / 100));
28
+ const raw = toAmount(l.discount);
29
+ const discount = l.discount_kind === 'amount'
30
+ ? roundMoney(Math.min(Math.max(raw, 0), Math.max(gross, 0)))
31
+ : roundMoney(gross * (raw / 100));
29
32
  const net = roundMoney(gross - discount);
30
33
  const tax = roundMoney(net * toAmount(l.tax_rate));
31
34
  return { gross, discount, net, tax, total: roundMoney(net + tax) };
@@ -47,8 +50,27 @@ export function computeTotals(lines) {
47
50
  t.total = roundMoney(t.subtotal - t.discount + t.tax);
48
51
  return t;
49
52
  }
53
+ /** Impuesto agrupado por tasa, sumando el impuesto ya redondeado de cada renglón. */
54
+ export function taxBreakdown(lines) {
55
+ const map = new Map();
56
+ for (const l of lines) {
57
+ if (l.kind !== 'item')
58
+ continue;
59
+ const a = computeLine(l);
60
+ const rate = toAmount(l.tax_rate);
61
+ const cur = map.get(rate) ?? { base: 0, tax: 0 };
62
+ cur.base += a.net;
63
+ cur.tax += a.tax;
64
+ map.set(rate, cur);
65
+ }
66
+ return [...map.entries()].map(([rate, v]) => ({
67
+ rate,
68
+ base: roundMoney(v.base),
69
+ tax: roundMoney(v.tax),
70
+ }));
71
+ }
50
72
  export function validateLineItems(lines, policy = {}) {
51
- const { stockPolicy = 'warn', allowNegative = false, requireItems = true } = policy;
73
+ const { stockPolicy = 'warn', allowNegative = false, requireItems = true, allowZeroQuantity = false } = policy;
52
74
  const errors = {};
53
75
  const warnings = {};
54
76
  let items = 0;
@@ -64,14 +86,28 @@ export function validateLineItems(lines, policy = {}) {
64
86
  const disc = toAmount(l.discount);
65
87
  if (!l.description.trim() && !l.product_id)
66
88
  errors[`${i}.description`] = 'Elige un producto o escribe una descripción';
67
- if (qty === 0)
89
+ if (qty === 0 && !allowZeroQuantity)
68
90
  errors[`${i}.quantity`] = 'La cantidad debe ser distinta de cero';
69
91
  else if (qty < 0 && !allowNegative)
70
92
  errors[`${i}.quantity`] = 'La cantidad no puede ser negativa';
93
+ if (l.max_quantity != null && qty > toAmount(l.max_quantity)) {
94
+ errors[`${i}.quantity`] = `El máximo es ${l.max_quantity}`;
95
+ }
71
96
  if (price < 0 && !allowNegative)
72
97
  errors[`${i}.unit_price`] = 'El precio no puede ser negativo';
73
- if (disc < 0 || disc > 100)
98
+ if (l.cost != null && price < toAmount(l.cost)) {
99
+ warnings[`${i}.unit_price`] = 'El precio está por debajo del costo';
100
+ }
101
+ if (l.discount_kind === 'amount') {
102
+ const gross = roundMoney(qty * price);
103
+ if (disc < 0)
104
+ errors[`${i}.discount`] = 'El descuento no puede ser negativo';
105
+ else if (disc > gross)
106
+ errors[`${i}.discount`] = 'El descuento no puede superar el importe';
107
+ }
108
+ else if (disc < 0 || disc > 100) {
74
109
  errors[`${i}.discount`] = 'El descuento debe estar entre 0 y 100%';
110
+ }
75
111
  if (l.available != null && qty > l.available && stockPolicy !== 'allow') {
76
112
  const msg = `Solo hay ${l.available} disponibles`;
77
113
  if (stockPolicy === 'block')
@@ -99,7 +135,11 @@ export function serializeLineItems(lines) {
99
135
  out.unit_price = toAmount(l.unit_price);
100
136
  out.discount = toAmount(l.discount);
101
137
  out.tax_rate = toAmount(l.tax_rate);
102
- for (const k of ['product_id', 'sku', 'technician_id', 'lot', 'dot']) {
138
+ if (l.discount_kind === 'amount')
139
+ out.discount_kind = 'amount';
140
+ if (l.catalog_price != null)
141
+ out.catalog_price = toAmount(l.catalog_price);
142
+ for (const k of ['product_id', 'sku', 'technician_id', 'lot', 'dot', 'unit']) {
103
143
  const v = l[k];
104
144
  if (typeof v === 'string' && v.trim())
105
145
  out[k] = v.trim();
@@ -124,7 +164,12 @@ export function parseLineItems(rows) {
124
164
  quantity: o.quantity == null ? 1 : toAmount(o.quantity),
125
165
  unit_price: toAmount(o.unit_price),
126
166
  discount: toAmount(o.discount),
167
+ discount_kind: o.discount_kind === 'amount' ? 'amount' : undefined,
127
168
  tax_rate: toAmount(o.tax_rate),
169
+ unit: str(o.unit),
170
+ catalog_price: o.catalog_price == null || o.catalog_price === '' ? undefined : toAmount(o.catalog_price),
171
+ cost: o.cost == null || o.cost === '' ? undefined : toAmount(o.cost),
172
+ max_quantity: o.max_quantity == null || o.max_quantity === '' ? undefined : toAmount(o.max_quantity),
128
173
  technician_id: str(o.technician_id),
129
174
  lot: str(o.lot),
130
175
  dot: str(o.dot),
@@ -36,6 +36,8 @@ export interface ProductVariant {
36
36
  sku?: string;
37
37
  barcode?: string;
38
38
  price?: number;
39
+ /** Costo de catálogo (compras). Opcional; el precio de lista sigue en `price`. */
40
+ cost?: number;
39
41
  stock?: StockByWarehouse[];
40
42
  }
41
43
  export interface ProductResult {
@@ -47,6 +49,8 @@ export interface ProductResult {
47
49
  supplier_sku?: string;
48
50
  kind?: 'product' | 'tire' | 'service' | 'bundle';
49
51
  price?: number;
52
+ /** Costo de catálogo (compras). */
53
+ cost?: number;
50
54
  tax_rate?: number;
51
55
  tire?: TireSize;
52
56
  variants?: ProductVariant[];
@@ -1 +1 @@
1
- {"version":3,"file":"product-search.d.ts","sourceRoot":"","sources":["../../src/business/product-search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAG5C,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAMjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAW5D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAO3D;AAED,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAC1B,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAAE,EACtC,WAAW,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAKpB;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CACzB,CAAC,EAAE,aAAa,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,QAAQ,CAWV"}
1
+ {"version":3,"file":"product-search.d.ts","sourceRoot":"","sources":["../../src/business/product-search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAG5C,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAMjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAW5D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAO3D;AAED,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAC1B,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAAE,EACtC,WAAW,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAKpB;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CACzB,CAAC,EAAE,aAAa,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,QAAQ,CAeV"}
@@ -39,12 +39,16 @@ export function availableStock(source, warehouseId) {
39
39
  /** Producto (+ variante) → renglón listo para <LineItemsEditor>. */
40
40
  export function productToLine(p, variant, opts = {}) {
41
41
  const src = variant ?? p;
42
+ const list = variant?.price ?? p.price;
43
+ const cost = variant?.cost ?? p.cost;
42
44
  return makeLine({
43
45
  product_id: variant?.id ?? p.id,
44
46
  sku: variant?.sku ?? p.sku,
45
47
  description: variant ? `${p.name} · ${variant.label}` : p.name,
46
48
  quantity: opts.quantity ?? 1,
47
- unit_price: variant?.price ?? p.price ?? 0,
49
+ unit_price: list ?? 0,
50
+ catalog_price: list,
51
+ cost,
48
52
  tax_rate: p.tax_rate ?? 0,
49
53
  available: availableStock(src, opts.warehouseId) ?? availableStock(p, opts.warehouseId),
50
54
  });
@@ -3,7 +3,9 @@ export interface DynamicDateFieldProps {
3
3
  field: ActionFieldDef;
4
4
  value: any;
5
5
  onChange: (v: any) => void;
6
+ /** Server or client validation failed for this field (PIT-046). */
7
+ invalid?: boolean;
6
8
  }
7
- export declare function DynamicDateField({ field, value, onChange }: DynamicDateFieldProps): import("react").JSX.Element;
9
+ export declare function DynamicDateField({ field, value, onChange, invalid }: DynamicDateFieldProps): import("react").JSX.Element;
8
10
  export default DynamicDateField;
9
11
  //# sourceMappingURL=dynamic-date-field.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-date-field.d.ts","sourceRoot":"","sources":["../src/dynamic-date-field.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAsBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,qBAAqB,+BAkCjF;AAED,eAAe,gBAAgB,CAAA"}
1
+ {"version":3,"file":"dynamic-date-field.d.ts","sourceRoot":"","sources":["../src/dynamic-date-field.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAsBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,qBAAqB,+BAsC1F;AAED,eAAe,gBAAgB,CAAA"}
@@ -35,10 +35,11 @@ function label(d) {
35
35
  return toISO(d);
36
36
  }
37
37
  }
38
- export function DynamicDateField({ field, value, onChange }) {
38
+ export function DynamicDateField({ field, value, onChange, invalid }) {
39
39
  const [open, setOpen] = useState(false);
40
40
  const selected = toDate(value);
41
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", id: field.key, "data-empty": !selected, className: "w-full justify-start text-start font-normal data-[empty=true]:text-muted-foreground", children: [_jsx(CalendarIcon, { className: "mr-2 size-4 shrink-0 opacity-50" }), _jsx("span", { className: "truncate", children: selected ? label(selected) : (field.placeholder || 'Elegí una fecha') })] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", captionLayout: "dropdown", selected: selected, defaultMonth: selected, onSelect: (d) => {
41
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", id: field.key, "data-empty": !selected, "aria-invalid": invalid || undefined, className: 'w-full justify-start text-start font-normal data-[empty=true]:text-muted-foreground' +
42
+ (invalid ? ' border-destructive ring-1 ring-destructive/30' : ''), children: [_jsx(CalendarIcon, { className: "mr-2 size-4 shrink-0 opacity-50" }), _jsx("span", { className: "truncate", children: selected ? label(selected) : (field.placeholder || 'Elegí una fecha') })] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", captionLayout: "dropdown", selected: selected, defaultMonth: selected, onSelect: (d) => {
42
43
  onChange(d ? toISO(d) : '');
43
44
  setOpen(false);
44
45
  } }) })] }));
@@ -47,6 +47,15 @@ export declare function computeLineItemTotals(field: ActionFieldDef, rows: any[]
47
47
  * No-op when the row has no amount column or no qty/price pair.
48
48
  */
49
49
  export declare function applyLineItemRowFormulas(itemFields: ActionFieldDef[], row: Record<string, any>): Record<string, any>;
50
+ /**
51
+ * Al elegir un producto en una grilla declarativa, copia precio/costo/SKU/tasa
52
+ * que vengan en `option.meta` a las columnas hermanas y recalcula el importe.
53
+ * No-op si el meta no trae datos útiles (el journal entry no se toca).
54
+ */
55
+ export declare function autofillLineFromOption(itemFields: ActionFieldDef[], row: Record<string, any>, option: {
56
+ label?: string;
57
+ meta?: Record<string, unknown>;
58
+ }): Record<string, any>;
50
59
  export interface BalanceState {
51
60
  debit: number;
52
61
  credit: number;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
@@ -162,6 +162,67 @@ export function applyLineItemRowFormulas(itemFields, row) {
162
162
  out[discountKey] = 0;
163
163
  return out;
164
164
  }
165
+ const AUTOFILL_PRICE_KEYS = new Set(['unit_price', 'price', 'sale_price']);
166
+ const AUTOFILL_COST_KEYS = new Set(['cost', 'unit_cost', 'catalog_cost', 'catalog_price']);
167
+ function metaNumber(meta, keys) {
168
+ for (const k of keys) {
169
+ const v = meta[k];
170
+ if (typeof v === 'number' && Number.isFinite(v))
171
+ return v;
172
+ if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
173
+ return Number(v);
174
+ }
175
+ return undefined;
176
+ }
177
+ /**
178
+ * Al elegir un producto en una grilla declarativa, copia precio/costo/SKU/tasa
179
+ * que vengan en `option.meta` a las columnas hermanas y recalcula el importe.
180
+ * No-op si el meta no trae datos útiles (el journal entry no se toca).
181
+ */
182
+ export function autofillLineFromOption(itemFields, row, option) {
183
+ const meta = option.meta;
184
+ if (!meta || Object.keys(meta).length === 0)
185
+ return row;
186
+ const next = { ...row };
187
+ let changed = false;
188
+ for (const f of itemFields) {
189
+ if (AUTOFILL_PRICE_KEYS.has(f.key)) {
190
+ const v = metaNumber(meta, ['unit_price', 'price', 'sale_price']);
191
+ if (v != null) {
192
+ next[f.key] = v;
193
+ changed = true;
194
+ }
195
+ }
196
+ else if (AUTOFILL_COST_KEYS.has(f.key)) {
197
+ const v = metaNumber(meta, ['cost', 'unit_cost', 'catalog_price', 'catalog_cost']);
198
+ if (v != null) {
199
+ next[f.key] = v;
200
+ changed = true;
201
+ }
202
+ }
203
+ else if (f.key === 'tax_rate' || f.key === 'tax') {
204
+ const v = metaNumber(meta, ['tax_rate', 'tax']);
205
+ if (v != null) {
206
+ next[f.key] = v;
207
+ changed = true;
208
+ }
209
+ }
210
+ else if (f.key === 'sku' || f.key === 'code') {
211
+ const v = meta.sku ?? meta.code;
212
+ if (typeof v === 'string' && v.trim()) {
213
+ next[f.key] = v.trim();
214
+ changed = true;
215
+ }
216
+ }
217
+ else if ((f.key === 'description' || f.key === 'name') && !next[f.key] && option.label) {
218
+ next[f.key] = option.label;
219
+ changed = true;
220
+ }
221
+ }
222
+ if (!changed)
223
+ return row;
224
+ return applyLineItemRowFormulas(itemFields, next);
225
+ }
165
226
  /**
166
227
  * Evaluates a line-items field's balance rule against its rows. Returns
167
228
  * `undefined` when the field declares no balance rule. `balanced` is true when
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAiB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAyBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,qBAAqB,+BAwQvH"}
1
+ {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAkB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAyBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,qBAAqB,+BAoRvH"}
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
12
12
  import { useEffect, useRef } from 'react';
13
13
  import { Plus, Trash2, Check } from 'lucide-react';
14
- import { resolveWidget, getItemFields, computeLineItemTotals, applyLineItemRowFormulas, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
14
+ import { resolveWidget, getItemFields, computeLineItemTotals, applyLineItemRowFormulas, autofillLineFromOption, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
15
15
  import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field';
16
16
  import { useOptionsResolver } from './use-options-resolver';
17
17
  const fmtNumber = (n) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
@@ -78,14 +78,19 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
78
78
  }
79
79
  updateCell(idx, key, cellValue);
80
80
  };
81
+ const handlePick = (idx, key, opt) => {
82
+ onChange(rows.map((r, i) => i === idx
83
+ ? autofillLineFromOption(itemFields, { ...r, [key]: String(opt.id) }, opt)
84
+ : r));
85
+ };
81
86
  const totalCols = itemFields.filter((c) => c.total);
82
87
  return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => {
83
88
  const cellErr = errMap[`${idx}.${col.key}`];
84
- return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive text-xs", children: cellErr }))] }, col.key));
89
+ return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), onPick: (opt) => handlePick(idx, col.key, opt), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive text-xs", children: cellErr }))] }, col.key));
85
90
  }) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
86
91
  (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => {
87
92
  const cellErr = errMap[`${idx}.${col.key}`];
88
- return (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive mt-1 text-xs", children: cellErr }))] }, col.key));
93
+ return (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), onPick: (opt) => handlePick(idx, col.key, opt), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive mt-1 text-xs", children: cellErr }))] }, col.key));
89
94
  }), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
90
95
  if (ci === 0) {
91
96
  return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
@@ -107,7 +112,7 @@ function BalanceBadge({ state, }) {
107
112
  // without the per-field Label (the column header is the label) and sized for a
108
113
  // table cell. Nested line-items inside a row are not supported (a row column is
109
114
  // a scalar widget).
110
- function CellRenderer({ field, value, onChange, disabled, formValues, rowValues, invalid }) {
115
+ function CellRenderer({ field, value, onChange, onPick, disabled, formValues, rowValues, invalid }) {
111
116
  const widget = resolveWidget(field);
112
117
  const invalidCls = invalid
113
118
  ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
@@ -143,10 +148,10 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
143
148
  // Async searchable picker per row cell — e.g. the account_id column of a
144
149
  // journal entry's debit/credit lines. Same widget as the flat form.
145
150
  if (widget === 'dynamic_select') {
146
- return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro, invalid: invalid }));
151
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, onPick: onPick, dependsValue: dependsValue, readonly: ro, invalid: invalid }));
147
152
  }
148
153
  if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
149
- return (_jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled, formValues: formValues, rowValues: rowValues }));
154
+ return (_jsx(RefCell, { field: field, value: value, onChange: onChange, onPick: onPick, disabled: disabled, formValues: formValues, rowValues: rowValues }));
150
155
  }
151
156
  switch (widget) {
152
157
  case 'textarea':
@@ -176,7 +181,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
176
181
  return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
177
182
  }
178
183
  }
179
- function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {
184
+ function RefCell({ field, value, onChange, onPick, disabled, formValues, rowValues }) {
180
185
  // Cascade: resolve the value of the field this cell `dependsOn` from the
181
186
  // row (sibling) first, then the header form. While empty, the picker is
182
187
  // disabled with a hint instead of listing the whole (unscoped) table.
@@ -210,5 +215,10 @@ function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {
210
215
  : loading
211
216
  ? 'Cargando…'
212
217
  : field.placeholder || 'Seleccionar...';
213
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled || loading || blockedByDependency, children: [_jsx(SelectTrigger, { className: "w-full", "data-depends-blocked": blockedByDependency ? '' : undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] }) }, String(opt.id)))) })] }));
218
+ return (_jsxs(Select, { value: value || '', onValueChange: (v) => {
219
+ onChange(v);
220
+ const opt = options.find((o) => String(o.id) === String(v));
221
+ if (opt)
222
+ onPick?.(opt);
223
+ }, disabled: disabled || loading || blockedByDependency, children: [_jsx(SelectTrigger, { className: "w-full", "data-depends-blocked": blockedByDependency ? '' : undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] }) }, String(opt.id)))) })] }));
214
224
  }
@@ -33,6 +33,8 @@ export interface DynamicSelectFieldProps {
33
33
  field: ActionFieldDef;
34
34
  value: any;
35
35
  onChange: (v: any) => void;
36
+ /** Opción completa recién elegida (incluye `meta` para autollenar columnas hermanas). */
37
+ onPick?: (opt: ResolvedOption) => void;
36
38
  /**
37
39
  * Pre-resolved option for the CURRENT value (label + image/color/icon) the
38
40
  * caller already has — e.g. the relation sibling the table served. Lets the
@@ -91,6 +93,6 @@ export interface DynamicSelectFieldProps {
91
93
  /** Paint trigger with destructive border when validation failed. */
92
94
  invalid?: boolean;
93
95
  }
94
- export declare function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, }: DynamicSelectFieldProps): import("react").JSX.Element;
96
+ export declare function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, }: DynamicSelectFieldProps): import("react").JSX.Element;
95
97
  export default DynamicSelectField;
96
98
  //# sourceMappingURL=dynamic-select-field.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AA4CA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA4BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAMD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI,CAAA;IAChD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,aAAoB,EACpB,kBAA0B,EAC1B,UAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,OAAe,GAClB,EAAE,uBAAuB,+BA0TzB;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AA4CA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA4BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAMD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;IACtC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI,CAAA;IAChD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAAM,EACN,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,aAAoB,EACpB,kBAA0B,EAC1B,UAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,OAAe,GAClB,EAAE,uBAAuB,+BA2TzB;AAED,eAAe,kBAAkB,CAAA"}
@@ -88,7 +88,7 @@ export function OptionLead({ option, size = 20, }) {
88
88
  function useDebounced(value, ms) {
89
89
  return useDebouncedValue(value, ms);
90
90
  }
91
- export function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
91
+ export function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
92
92
  const { t } = useTranslation();
93
93
  const ph = (fallback) => field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback;
94
94
  const [open, setOpen] = useState(false);
@@ -180,6 +180,7 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
180
180
  const handlePick = (opt) => {
181
181
  setPicked(opt);
182
182
  onChange(String(opt.id));
183
+ onPick?.(opt);
183
184
  setOpen(false);
184
185
  setSearch('');
185
186
  };
@@ -11,6 +11,11 @@ export interface ResolvedOption {
11
11
  image?: string | null;
12
12
  color?: string | null;
13
13
  icon?: string | null;
14
+ /**
15
+ * Campos extra del payload (precio, costo, sku, tasa) que no son decoración.
16
+ * Solo se llena cuando el option trae números o strings además de id/label.
17
+ */
18
+ meta?: Record<string, unknown>;
14
19
  }
15
20
  export interface OptionsMeta {
16
21
  /** 'static' for inline options, 'dynamic' for FK-resolved lists. */
@@ -84,10 +89,5 @@ export interface UseOptionsResolverResult {
84
89
  export declare function invalidateOptionsCache(): void;
85
90
  export declare function optionsRequestKey(scope: string, url: string, field: string, query: string, limit: number | undefined, filter: string | undefined): string;
86
91
  export declare function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsResolverResult;
87
- /**
88
- * Normalizes the wire shape into ResolvedOption. The kernel returns dual
89
- * id/value and label/name fields for legacy parity — we accept either
90
- * and surface a stable shape downstream.
91
- */
92
92
  export declare function projectOption(raw: any): ResolvedOption;
93
93
  //# sourceMappingURL=use-options-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAuBD;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C;AAED,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAER;AA8BD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CA6JzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAatD"}
1
+ {"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAuBD;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C;AAED,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAER;AA8BD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CA6JzF;AASD,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAqBtD"}
@@ -218,9 +218,19 @@ export function useOptionsResolver(args) {
218
218
  * id/value and label/name fields for legacy parity — we accept either
219
219
  * and surface a stable shape downstream.
220
220
  */
221
+ const OPTION_KNOWN_KEYS = new Set(['id', 'value', 'label', 'name', 'description', 'image', 'color', 'icon']);
221
222
  export function projectOption(raw) {
222
223
  const id = raw?.id ?? raw?.value ?? '';
223
224
  const label = String(raw?.label ?? raw?.name ?? id ?? '');
225
+ const meta = {};
226
+ if (raw && typeof raw === 'object') {
227
+ for (const [k, v] of Object.entries(raw)) {
228
+ if (OPTION_KNOWN_KEYS.has(k))
229
+ continue;
230
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
231
+ meta[k] = v;
232
+ }
233
+ }
224
234
  return {
225
235
  id,
226
236
  value: raw?.value ?? id,
@@ -230,5 +240,6 @@ export function projectOption(raw) {
230
240
  image: raw?.image ?? null,
231
241
  color: raw?.color ?? null,
232
242
  icon: raw?.icon ?? null,
243
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
233
244
  };
234
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "46.2.2",
3
+ "version": "46.3.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,79 @@
1
+ // @vitest-environment happy-dom
2
+ import { useState } from 'react'
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
5
+
6
+ vi.mock('react-i18next', () => ({
7
+ useTranslation: () => ({
8
+ t: (k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? k,
9
+ i18n: { language: 'es' },
10
+ }),
11
+ }))
12
+
13
+ import { DocumentLinesGrid, LineItemsEditor } from '../business/line-items-editor'
14
+ import { makeLine, type LineItem } from '../business/line-items'
15
+ import type { ProductResult } from '../business/product-search'
16
+
17
+ afterEach(cleanup)
18
+
19
+ function Harness({
20
+ initial = [],
21
+ search,
22
+ priceSource,
23
+ mode,
24
+ }: {
25
+ initial?: LineItem[]
26
+ search?: (q: any, signal: AbortSignal) => Promise<ProductResult[]>
27
+ priceSource?: 'sale' | 'cost' | 'origin'
28
+ mode?: 'free' | 'from_source'
29
+ }) {
30
+ const [lines, setLines] = useState(initial)
31
+ return (
32
+ <>
33
+ <DocumentLinesGrid value={lines} onChange={setLines} search={search} priceSource={priceSource} mode={mode} currency="MXN" />
34
+ <output data-testid="state">{JSON.stringify(lines)}</output>
35
+ </>
36
+ )
37
+ }
38
+
39
+ const state = () => JSON.parse(screen.getByTestId('state').textContent ?? '[]') as LineItem[]
40
+
41
+ const tire: ProductResult = { id: 'p1', name: 'Llanta X', sku: 'LX', price: 1200, cost: 800, tax_rate: 0.16 }
42
+
43
+ describe('DocumentLinesGrid', () => {
44
+ it('es el mismo componente que LineItemsEditor', () => {
45
+ expect(LineItemsEditor).toBe(DocumentLinesGrid)
46
+ })
47
+
48
+ it('al elegir un producto carga cantidad 1 y el stepper actualiza el importe', async () => {
49
+ const search = vi.fn(async () => [tire])
50
+ render(<Harness search={search} priceSource="cost" />)
51
+ fireEvent.change(screen.getByLabelText('Buscar producto'), { target: { value: 'llan' } })
52
+ const option = await waitFor(() => screen.getByRole('button', { name: /Llanta X/ }), { timeout: 2000 })
53
+ fireEvent.click(option)
54
+ expect(state()[0]).toMatchObject({ description: 'Llanta X', sku: 'LX', quantity: 1, unit_price: 800, catalog_price: 800 })
55
+ expect(screen.getByText(/Catálogo/)).toBeTruthy()
56
+ expect(document.querySelector('[data-slot="line-amount"]')?.textContent).toMatch(/800/)
57
+ fireEvent.click(screen.getByRole('button', { name: 'Aumentar cantidad' }))
58
+ expect(state()[0]!.quantity).toBe(2)
59
+ expect(document.querySelector('[data-slot="line-amount"]')?.textContent).toMatch(/1,600/)
60
+ expect(document.querySelector('[data-slot="tax-total"]')?.textContent).toMatch(/256/)
61
+ expect(document.querySelector('[data-slot="grand-total"]')?.textContent).toMatch(/1,856/)
62
+ })
63
+
64
+ it('Supr en una descripción vacía borra y se puede deshacer', () => {
65
+ render(<Harness initial={[makeLine({ description: '' })]} />)
66
+ fireEvent.keyDown(screen.getByLabelText('Descripción'), { key: 'Backspace' })
67
+ expect(state()).toHaveLength(0)
68
+ fireEvent.click(screen.getByRole('button', { name: 'Deshacer' }))
69
+ expect(state()).toHaveLength(1)
70
+ })
71
+
72
+ it('desde origen no ofrece alta ni borrado', () => {
73
+ const search = vi.fn(async () => [tire])
74
+ render(<Harness initial={[makeLine({ description: 'Llanta', quantity: 2, max_quantity: 2 })]} search={search} mode="from_source" />)
75
+ expect(screen.queryByLabelText('Buscar producto')).toBeNull()
76
+ expect(screen.queryByRole('button', { name: 'Eliminar' })).toBeNull()
77
+ expect(screen.queryByRole('button', { name: 'Renglón libre' })).toBeNull()
78
+ })
79
+ })
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { applyLineItemRowFormulas } from '../dynamic-form-schema'
2
+ import { applyLineItemRowFormulas, autofillLineFromOption } from '../dynamic-form-schema'
3
3
  import type { ActionFieldDef } from '../types'
4
4
 
5
5
  const fields: ActionFieldDef[] = [
@@ -53,3 +53,20 @@ describe('applyLineItemRowFormulas', () => {
53
53
  expect(applyLineItemRowFormulas(lean, input)).toBe(input)
54
54
  })
55
55
  })
56
+
57
+ describe('autofillLineFromOption', () => {
58
+ it('copia precio, sku y recalcula el importe', () => {
59
+ const row = autofillLineFromOption(
60
+ fields,
61
+ { product_id: '', qty: 2, unit_price: '', discount: 0, subtotal: 0 },
62
+ { label: 'Llanta', meta: { price: 800, sku: 'LX' } },
63
+ )
64
+ expect(row.unit_price).toBe(800)
65
+ expect(row.subtotal).toBe(1600)
66
+ })
67
+
68
+ it('no toca la fila si el option no trae meta', () => {
69
+ const input = { product_id: 'p1', qty: 1, unit_price: 10, discount: 0, subtotal: 10 }
70
+ expect(autofillLineFromOption(fields, input, { label: 'X' })).toBe(input)
71
+ })
72
+ })
@@ -60,6 +60,11 @@ describe('projectOption', () => {
60
60
  expect(out.icon).toBeNull()
61
61
  })
62
62
 
63
+ it('keeps extra price and sku on meta', () => {
64
+ const out = projectOption({ id: 'p1', label: 'Llanta', price: 800, sku: 'LX', nested: { a: 1 } })
65
+ expect(out.meta).toEqual({ price: 800, sku: 'LX' })
66
+ })
67
+
63
68
  it('survives empty payload (defensive)', () => {
64
69
  const out = projectOption({})
65
70
  expect(out.id).toBe('')
@@ -1501,7 +1501,7 @@ function renderField(
1501
1501
  case 'date':
1502
1502
  // Modern shadcn Calendar in a Popover (portaled, never clipped by the
1503
1503
  // modal) instead of the native, dated, easily-cut <input type=date>.
1504
- return <DynamicDateField field={field} value={value} onChange={onChange} />
1504
+ return <DynamicDateField field={field} value={value} onChange={onChange} invalid={invalid} />
1505
1505
  default:
1506
1506
  return <Input id={field.key} type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'} value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} placeholder={field.placeholder} aria-invalid={invalid || undefined} className={invalidCls || undefined} />
1507
1507
  }