@asteby/metacore-runtime-react 49.7.0 → 49.9.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 (74) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/business/document-editor-model.d.ts +27 -2
  3. package/dist/business/document-editor-model.d.ts.map +1 -1
  4. package/dist/business/document-editor-model.js +53 -3
  5. package/dist/business/document-editor.d.ts +7 -2
  6. package/dist/business/document-editor.d.ts.map +1 -1
  7. package/dist/business/document-editor.js +121 -22
  8. package/dist/business/document-lines.d.ts +21 -1
  9. package/dist/business/document-lines.d.ts.map +1 -1
  10. package/dist/business/document-lines.js +69 -7
  11. package/dist/business/index.d.ts +1 -0
  12. package/dist/business/index.d.ts.map +1 -1
  13. package/dist/business/index.js +1 -0
  14. package/dist/business/line-items-editor.d.ts +21 -1
  15. package/dist/business/line-items-editor.d.ts.map +1 -1
  16. package/dist/business/line-items-editor.js +74 -15
  17. package/dist/business/line-items.d.ts +28 -0
  18. package/dist/business/line-items.d.ts.map +1 -1
  19. package/dist/business/line-items.js +1 -0
  20. package/dist/business/line-product-cell.d.ts +6 -0
  21. package/dist/business/line-product-cell.d.ts.map +1 -1
  22. package/dist/business/line-product-cell.js +9 -3
  23. package/dist/business/product-display.d.ts +18 -0
  24. package/dist/business/product-display.d.ts.map +1 -0
  25. package/dist/business/product-display.js +40 -0
  26. package/dist/business/product-options.d.ts +6 -1
  27. package/dist/business/product-options.d.ts.map +1 -1
  28. package/dist/business/product-options.js +11 -2
  29. package/dist/business/product-search.d.ts +8 -0
  30. package/dist/business/product-search.d.ts.map +1 -1
  31. package/dist/dialogs/dynamic-record.js +1 -1
  32. package/dist/dynamic-select-field.d.ts +7 -1
  33. package/dist/dynamic-select-field.d.ts.map +1 -1
  34. package/dist/dynamic-select-field.js +18 -4
  35. package/dist/field-grid.d.ts +4 -3
  36. package/dist/field-grid.d.ts.map +1 -1
  37. package/dist/field-grid.js +1 -1
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/option-display.d.ts +68 -0
  42. package/dist/option-display.d.ts.map +1 -0
  43. package/dist/option-display.js +135 -0
  44. package/dist/query-batch.d.ts +1 -1
  45. package/dist/query-batch.d.ts.map +1 -1
  46. package/dist/query-batch.js +3 -1
  47. package/dist/record-picker.d.ts +2 -0
  48. package/dist/record-picker.d.ts.map +1 -1
  49. package/dist/record-picker.js +11 -4
  50. package/dist/use-options-resolver.d.ts +16 -1
  51. package/dist/use-options-resolver.d.ts.map +1 -1
  52. package/dist/use-options-resolver.js +38 -6
  53. package/package.json +1 -1
  54. package/src/__tests__/document-editor-inline-selects.test.tsx +11 -4
  55. package/src/__tests__/line-row-create-edit.test.tsx +312 -0
  56. package/src/__tests__/option-display.test.tsx +241 -0
  57. package/src/business/document-editor-model.ts +60 -2
  58. package/src/business/document-editor.tsx +143 -19
  59. package/src/business/document-lines.ts +79 -7
  60. package/src/business/index.ts +1 -0
  61. package/src/business/line-items-editor.tsx +158 -30
  62. package/src/business/line-items.ts +31 -0
  63. package/src/business/line-product-cell.tsx +32 -5
  64. package/src/business/product-display.ts +62 -0
  65. package/src/business/product-options.tsx +11 -2
  66. package/src/business/product-search.ts +8 -0
  67. package/src/dialogs/dynamic-record.tsx +1 -1
  68. package/src/dynamic-select-field.tsx +45 -1
  69. package/src/field-grid.tsx +5 -4
  70. package/src/index.ts +10 -0
  71. package/src/option-display.tsx +255 -0
  72. package/src/query-batch.ts +2 -0
  73. package/src/record-picker.tsx +16 -3
  74. package/src/use-options-resolver.ts +49 -4
@@ -4,15 +4,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // Controlado: `value`/`onChange` con `LineItem[]`. Guardar = `serializeLineItems`.
5
5
  import { useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { Heading, Minus, Plus, StickyNote, Trash2 } from 'lucide-react';
7
+ import { Heading, Minus, Pencil, Plus, StickyNote, Trash2 } from 'lucide-react';
8
8
  import { Button, Input } from '@asteby/metacore-ui';
9
9
  import { useCan } from '../permissions-context';
10
10
  import { useFormatter } from './format';
11
- import { addProductLine, applyProductToLine, lineGridKeyCommand, } from './document-lines';
11
+ import { addProductLine, applyCatalogPending, applyProductToLine, refreshLineFromProduct, lineGridKeyCommand, } from './document-lines';
12
12
  import { computeLine, computeTotals, makeLine, validateLineItems, } from './line-items';
13
13
  import { LineProductCell } from './line-product-cell';
14
14
  const num = (s) => (s.trim() === '' ? 0 : Number(s.replace(',', '.')) || 0);
15
- export function DocumentLinesGrid({ value, onChange, columns = ['discount'], policy, serverErrors, onValidate, onRequestProduct, search, priceSource = 'sale', mode = 'free', discountMode = 'percent', quantityStepper, mergeSameProduct = true, warehouseId, allowSections = true, readOnly = false, editPermission, currency, showTotals = true, }) {
15
+ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], policy, serverErrors, onValidate, onRequestProduct, search, priceSource = 'sale', mode = 'free', discountMode = 'percent', quantityStepper, mergeSameProduct = true, warehouseId, allowSections = true, readOnly = false, editPermission, currency, showTotals = true, onCreateProduct, onEditProduct, productLabel, unitLabel, }) {
16
16
  const { t } = useTranslation();
17
17
  const can = useCan();
18
18
  const fmt = useFormatter({ currency });
@@ -29,6 +29,11 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
29
29
  const stepper = quantityStepper ?? !!search;
30
30
  const canAdd = !locked && !fromSource;
31
31
  const [undo, setUndo] = useState(null);
32
+ // Crear/editar desde la celda resuelven DESPUÉS de un modal: se trabaja
33
+ // sobre los renglones de ese momento (no los del render que abrió el modal).
34
+ const latest = useRef(value);
35
+ latest.current = value;
36
+ const entity = productLabel ?? t('lineItems.productEntity', { defaultValue: 'producto' });
32
37
  const emit = (next) => {
33
38
  onChange(next);
34
39
  const priced = discountMode === 'amount' ? next.map((l) => (l.kind === 'item' ? { ...l, discount_kind: 'amount' } : l)) : next;
@@ -37,7 +42,7 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
37
42
  const patch = (idx, p) => {
38
43
  if (discountMode === 'amount')
39
44
  p = { ...p, discount_kind: 'amount' };
40
- emit(value.map((l, i) => (i === idx ? { ...l, ...p } : l)));
45
+ emit(value.map((l, i) => (i === idx ? withoutAnsweredPending({ ...l, ...p }, p) : l)));
41
46
  };
42
47
  const remove = (idx) => {
43
48
  setUndo(value);
@@ -61,23 +66,55 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
61
66
  }, [focusQty, value]);
62
67
  const blankLine = () => makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {});
63
68
  /** Renglón vacío del final: el producto entra como renglón nuevo (o suma al mismo). */
64
- const pickProduct = (product, variant) => {
65
- const next = addProductLine(value, product, variant, { priceSource, warehouseId, mergeSameProduct });
69
+ const pickProduct = (product, variant, base = value) => {
70
+ const next = addProductLine(base, product, variant, { priceSource, warehouseId, mergeSameProduct });
66
71
  emit(next);
67
72
  const id = variant?.id ?? product.id;
68
- const target = next.find((l) => l.kind === 'item' && l.product_id === id && !value.includes(l)) ?? next[next.length - 1];
73
+ const target = next.find((l) => l.kind === 'item' && l.product_id === id && !base.includes(l)) ?? next[next.length - 1];
69
74
  if (target)
70
75
  setFocusQty(target.key);
71
76
  };
72
77
  /** Renglón libre existente: el producto elegido en su celda llena ESE renglón. */
73
- const fillLine = (idx, product, variant) => {
74
- const cur = value[idx];
78
+ const fillLine = (idx, product, variant, base = value) => {
79
+ const cur = base[idx];
75
80
  if (!cur)
76
81
  return;
77
82
  const filled = applyProductToLine(cur, product, variant, { priceSource, warehouseId });
78
- emit(value.map((l, i) => (i === idx ? (discountMode === 'amount' ? { ...filled, discount_kind: 'amount' } : filled) : l)));
83
+ emit(base.map((l, i) => (i === idx ? (discountMode === 'amount' ? { ...filled, discount_kind: 'amount' } : filled) : l)));
79
84
  setFocusQty(cur.key);
80
85
  };
86
+ /**
87
+ * «Crear producto «texto»» / «+»: el alta del modelo de catálogo; al
88
+ * guardarse, el producto nuevo entra en ESE renglón (o en uno nuevo desde
89
+ * el renglón del final) y lo llena igual que al elegir uno existente.
90
+ */
91
+ const createProduct = onCreateProduct
92
+ ? async (query, lineKey) => {
93
+ const product = await onCreateProduct(query);
94
+ if (!product)
95
+ return;
96
+ const base = latest.current;
97
+ const idx = lineKey ? base.findIndex((l) => l.key === lineKey) : -1;
98
+ if (idx >= 0 && !base[idx].product_id)
99
+ fillLine(idx, product, undefined, base);
100
+ else
101
+ pickProduct(product, undefined, base);
102
+ }
103
+ : undefined;
104
+ /**
105
+ * Lápiz: edita el producto del renglón; al guardar, el renglón toma los
106
+ * valores nuevos salvo lo que el usuario sobrescribió (queda el aviso).
107
+ */
108
+ const editProduct = onEditProduct
109
+ ? async (line) => {
110
+ const product = await onEditProduct(line);
111
+ if (!product)
112
+ return;
113
+ const base = latest.current;
114
+ emit(base.map((l) => (l.key === line.key ? refreshLineFromProduct(l, product, { priceSource, warehouseId }) : l)));
115
+ }
116
+ : undefined;
117
+ const unitText = (u) => (!u ? undefined : unitLabel ? unitLabel(u) || undefined : u);
81
118
  /** Texto escrito en el renglón vacío sin elegir producto → renglón libre con esa descripción. */
82
119
  const addFreeFromDraft = (description) => {
83
120
  const line = { ...blankLine(), description };
@@ -129,7 +166,12 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
129
166
  const a = computeLine(shown);
130
167
  const kind = shown.discount_kind ?? 'percent';
131
168
  const qtyMax = l.max_quantity;
132
- return (_jsxs("tr", { className: "group border-t align-top transition-colors hover:bg-muted/30 focus-within:bg-muted/20", "data-line-key": l.key, children: [_jsxs("td", { className: CELL, children: [search && canAdd && !l.product_id ? (_jsx(LineProductCell, { search: search, text: l.description, onTextChange: (text) => patch(i, { description: text }), onPick: (p, v) => fillLine(i, p, v), onKeyDownClosed: (e) => descKeys(e, i, l), warehouseId: warehouseId, currency: currency, ariaLabel: t('lineItems.description', { defaultValue: 'Descripción' }), invalid: !!cell(i, 'description'), dataCell: "description" })) : (_jsx(Input, { value: l.description, disabled: locked, className: INPUT, "data-cell": "description", "aria-label": t('lineItems.description', { defaultValue: 'Descripción' }), "aria-invalid": !!cell(i, 'description'), onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => descKeys(e, i, l) })), (l.sku || l.unit) && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [l.sku, !show('unit') ? l.unit : ''].filter(Boolean).join(' · ') })), cell(i, 'description') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'description') })] }), show('unit') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.unit ?? '', disabled: locked, onChange: (e) => patch(i, { unit: e.target.value }) }) })), _jsxs("td", { className: CELL, children: [stepper ? (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { type: "button", size: "icon", variant: "outline", className: "size-7", disabled: locked || toNum(l.quantity) <= (fromSource ? 0 : 1), "aria-label": t('lineItems.decreaseQty', { defaultValue: 'Disminuir cantidad' }), onClick: () => patch(i, { quantity: Math.max(fromSource ? 0 : 1, toNum(l.quantity) - 1) }), children: _jsx(Minus, { className: "size-3" }) }), _jsx(Input, { inputMode: "decimal", className: "h-8 w-14 text-right tabular-nums", "data-cell": "quantity", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => {
169
+ return (_jsxs("tr", { className: "group border-t align-top transition-colors hover:bg-muted/30 focus-within:bg-muted/20", "data-line-key": l.key, children: [_jsxs("td", { className: CELL, children: [search && canAdd && !l.product_id ? (_jsx(LineProductCell, { search: search, text: l.description, onTextChange: (text) => patch(i, { description: text }), onPick: (p, v) => fillLine(i, p, v), onKeyDownClosed: (e) => descKeys(e, i, l), warehouseId: warehouseId, currency: currency, ariaLabel: t('lineItems.description', { defaultValue: 'Descripción' }), invalid: !!cell(i, 'description'), dataCell: "description", onCreate: createProduct ? (q) => void createProduct(q, l.key) : undefined, entityLabel: entity })) : (_jsxs("div", { className: "relative", "data-slot": "line-description", children: [_jsx(Input, { value: l.description, disabled: locked, className: INPUT + (editProduct && l.product_id && !locked ? ' pr-9' : ''), "data-cell": "description", "aria-label": t('lineItems.description', { defaultValue: 'Descripción' }), "aria-invalid": !!cell(i, 'description'), onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => descKeys(e, i, l) }), editProduct && l.product_id && !locked && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", className: EDIT, "data-slot": "line-edit-product", "aria-label": t('lineItems.editProduct', { defaultValue: 'Editar {{entity}}', entity }), title: t('lineItems.editProduct', { defaultValue: 'Editar {{entity}}', entity }), onClick: () => void editProduct(l), children: _jsx(Pencil, { className: "size-3.5" }) }))] })), (l.sku || unitText(l.unit)) && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [l.sku, !show('unit') ? unitText(l.unit) : ''].filter(Boolean).join(' · ') })), l.catalog_pending && !locked && (_jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", "data-slot": "catalog-pending", role: "status", children: [l.catalog_pending.unit_price != null
170
+ ? t('lineItems.catalogPriceChanged', {
171
+ defaultValue: 'Precio de catálogo cambió a {{price}}',
172
+ price: fmt.money(l.catalog_pending.unit_price),
173
+ })
174
+ : t('lineItems.catalogChanged', { defaultValue: 'El producto cambió en el catálogo' }), ' — ', _jsx("button", { type: "button", className: "font-medium text-foreground underline-offset-2 hover:underline focus-visible:underline focus-visible:outline-none", onClick: () => emit(latest.current.map((x) => (x.key === l.key ? applyCatalogPending(x) : x))), children: t('lineItems.catalogApply', { defaultValue: 'aplicar' }) })] })), cell(i, 'description') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'description') })] }), show('unit') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.unit ?? '', disabled: locked, onChange: (e) => patch(i, { unit: e.target.value }) }) })), _jsxs("td", { className: CELL, children: [stepper ? (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { type: "button", size: "icon", variant: "outline", className: "size-7", disabled: locked || toNum(l.quantity) <= (fromSource ? 0 : 1), "aria-label": t('lineItems.decreaseQty', { defaultValue: 'Disminuir cantidad' }), onClick: () => patch(i, { quantity: Math.max(fromSource ? 0 : 1, toNum(l.quantity) - 1) }), children: _jsx(Minus, { className: "size-3" }) }), _jsx(Input, { inputMode: "decimal", className: "h-8 w-14 text-right tabular-nums", "data-cell": "quantity", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => {
133
175
  let q = num(e.target.value);
134
176
  if (qtyMax != null && q > qtyMax)
135
177
  q = qtyMax;
@@ -140,11 +182,21 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
140
182
  const next = toNum(l.quantity) + 1;
141
183
  patch(i, { quantity: qtyMax != null ? Math.min(next, qtyMax) : next });
142
184
  }, children: _jsx(Plus, { className: "size-3" }) })] })) : (_jsx(Input, { inputMode: "decimal", className: NUM, "data-cell": "quantity", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => patch(i, { quantity: num(e.target.value) }) })), cell(i, 'quantity') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'quantity') }), !cell(i, 'quantity') && warn(i, 'quantity') && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: warn(i, 'quantity') })] }), _jsxs("td", { className: CELL, children: [_jsx(Input, { inputMode: "decimal", className: NUM, value: String(l.unit_price), disabled: locked, "aria-label": priceSource === 'cost' ? t('lineItems.unitCost', { defaultValue: 'Costo unitario' }) : t('lineItems.unitPrice', { defaultValue: 'Precio' }), "aria-invalid": !!cell(i, 'unit_price'), onChange: (e) => patch(i, { unit_price: num(e.target.value) }) }), l.catalog_price != null && (_jsxs("p", { className: "mt-0.5 truncate whitespace-nowrap text-right text-xs text-muted-foreground", children: [t('lineItems.catalogPrice', { defaultValue: 'Catálogo' }), " ", fmt.money(l.catalog_price)] })), cell(i, 'unit_price') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'unit_price') }), !cell(i, 'unit_price') && warn(i, 'unit_price') && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: warn(i, 'unit_price') })] }), show('discount') && (_jsxs("td", { className: CELL, children: [_jsxs("div", { className: "flex items-center gap-1", children: [discountMode === 'both' && (_jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "h-8 px-1.5", "aria-label": t('lineItems.discountKind', { defaultValue: 'Tipo de descuento' }), onClick: () => patch(i, { discount_kind: kind === 'amount' ? 'percent' : 'amount', discount: 0 }), children: kind === 'amount' ? '$' : '%' })), _jsx(Input, { inputMode: "decimal", className: NUM, value: String(l.discount), disabled: locked, "aria-invalid": !!cell(i, 'discount'), onChange: (e) => patch(i, { discount: num(e.target.value), discount_kind: kind }) })] }), cell(i, 'discount') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'discount') })] })), show('tax') && (_jsx("td", { className: CELL, children: _jsx(Input, { inputMode: "decimal", className: NUM, value: String(Math.round(l.tax_rate * 10000) / 100), disabled: locked, onChange: (e) => patch(i, { tax_rate: num(e.target.value) / 100 }) }) })), show('technician') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.technician_id ?? '', disabled: locked, onChange: (e) => patch(i, { technician_id: e.target.value }) }) })), show('lot') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.lot ?? '', disabled: locked, onChange: (e) => patch(i, { lot: e.target.value }) }) })), show('dot') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.dot ?? '', disabled: locked, onChange: (e) => patch(i, { dot: e.target.value }) }) })), _jsx("td", { className: "px-3 py-1.5 text-right align-top font-medium tabular-nums", "data-slot": "line-amount", children: _jsx("span", { className: "inline-flex h-8 items-center", children: fmt.money(a.net) }) }), _jsx("td", { className: "px-1 py-1.5 align-top", children: canAdd && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", className: DELETE, onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) })) })] }, l.key));
143
- }), value.length === 0 && !search && (_jsx("tr", { children: _jsx("td", { colSpan: 99, className: "px-3 py-6 text-center text-muted-foreground", children: t('lineItems.empty', { defaultValue: 'Aún no hay renglones. Agrega un producto o un renglón libre.' }) }) })), canAdd && search && (_jsx(DraftRow, { search: search, warehouseId: warehouseId, currency: currency, onPick: pickProduct, onFree: addFreeFromDraft, onAddBlank: () => emit([...value, blankLine()]), trailingCells: colCount - 1 }))] }), showTotals && (_jsxs("tfoot", { className: "border-t bg-muted/20 text-sm tabular-nums", "data-slot": "line-totals", children: [_jsx(TotalRow, { span: colCount - 2, label: t('lineItems.subtotal', { defaultValue: 'Subtotal' }), value: fmt.money(totals.subtotal) }), totals.discount > 0 && _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.discountTotal', { defaultValue: 'Descuento' }), value: `-${fmt.money(totals.discount)}` }), totals.tax > 0 && _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.taxTotal', { defaultValue: 'Impuestos' }), value: fmt.money(totals.tax), slot: "tax-total" }), _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.total', { defaultValue: 'Total' }), value: fmt.money(totals.total), slot: "grand-total", strong: true })] }))] }) }), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form }), undo && canAdd && (_jsxs("p", { className: "text-sm text-muted-foreground", children: [t('lineItems.removed', { defaultValue: 'Renglón eliminado.' }), ' ', _jsx("button", { type: "button", className: "underline", onClick: () => {
185
+ }), value.length === 0 && !search && (_jsx("tr", { children: _jsx("td", { colSpan: 99, className: "px-3 py-6 text-center text-muted-foreground", children: t('lineItems.empty', { defaultValue: 'Aún no hay renglones. Agrega un producto o un renglón libre.' }) }) })), canAdd && search && (_jsx(DraftRow, { search: search, warehouseId: warehouseId, currency: currency, onPick: (p, v) => pickProduct(p, v), onFree: addFreeFromDraft, onAddBlank: () => emit([...value, blankLine()]), onCreate: createProduct ? (q) => void createProduct(q) : undefined, entityLabel: entity, span: colCount - 2 }))] }), showTotals && (_jsxs("tfoot", { className: "border-t bg-muted/20 text-sm tabular-nums", "data-slot": "line-totals", children: [_jsx(TotalRow, { span: colCount - 2, label: t('lineItems.subtotal', { defaultValue: 'Subtotal' }), value: fmt.money(totals.subtotal) }), totals.discount > 0 && _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.discountTotal', { defaultValue: 'Descuento' }), value: `-${fmt.money(totals.discount)}` }), totals.tax > 0 && _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.taxTotal', { defaultValue: 'Impuestos' }), value: fmt.money(totals.tax), slot: "tax-total" }), _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.total', { defaultValue: 'Total' }), value: fmt.money(totals.total), slot: "grand-total", strong: true })] }))] }) }), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form }), undo && canAdd && (_jsxs("p", { className: "text-sm text-muted-foreground", children: [t('lineItems.removed', { defaultValue: 'Renglón eliminado.' }), ' ', _jsx("button", { type: "button", className: "underline", onClick: () => {
144
186
  emit(undo);
145
187
  setUndo(null);
146
188
  }, children: t('lineItems.undo', { defaultValue: 'Deshacer' }) })] })), _jsx("div", { className: "flex flex-wrap items-center gap-3", children: canAdd ? (_jsxs("div", { className: "flex flex-wrap gap-1", children: [onRequestProduct && !search && (_jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: onRequestProduct, children: [_jsx(Plus, { className: "mr-1 size-4" }), t('lineItems.addProduct', { defaultValue: 'Agregar producto' })] })), _jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, blankLine()]), children: [_jsx(Plus, { className: "mr-1 size-4" }), t('lineItems.addLine', { defaultValue: 'Renglón libre' })] }), allowSections && (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, makeLine({ kind: 'section' })]), children: [_jsx(Heading, { className: "mr-1 size-4" }), t('lineItems.addSection', { defaultValue: 'Sección' })] }), _jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, makeLine({ kind: 'note' })]), children: [_jsx(StickyNote, { className: "mr-1 size-4" }), t('lineItems.addNote', { defaultValue: 'Nota' })] })] }))] })) : null })] }));
147
189
  }
190
+ /** Capturar a mano un campo con aviso de catálogo responde el aviso: deja de ofrecer «aplicar» para ese campo. */
191
+ function withoutAnsweredPending(line, p) {
192
+ if (!line.catalog_pending)
193
+ return line;
194
+ const pending = { ...line.catalog_pending };
195
+ for (const k of Object.keys(p))
196
+ delete pending[k];
197
+ const { catalog_pending: _drop, ...rest } = line;
198
+ return Object.keys(pending).length > 0 ? { ...rest, catalog_pending: pending } : rest;
199
+ }
148
200
  /** Mismo componente. El nombre anterior sigue exportado. */
149
201
  export const LineItemsEditor = DocumentLinesGrid;
150
202
  function toNum(n) {
@@ -155,10 +207,10 @@ function toNum(n) {
155
207
  * celda «Descripción» (no en una barra a todo lo ancho). Elegir agrega el
156
208
  * producto; Enter con texto sin coincidencias lo agrega como renglón libre.
157
209
  */
158
- function DraftRow({ search, warehouseId, currency, onPick, onFree, onAddBlank, trailingCells, }) {
210
+ function DraftRow({ search, warehouseId, currency, onPick, onFree, onAddBlank, onCreate, entityLabel, span, }) {
159
211
  const { t } = useTranslation();
160
212
  const [text, setText] = useState('');
161
- return (_jsxs("tr", { className: "border-t border-dashed", "data-slot": "line-draft-row", children: [_jsx("td", { className: CELL, children: _jsx(LineProductCell, { search: search, text: text, onTextChange: setText, onPick: (p, v) => {
213
+ return (_jsxs("tr", { className: "border-t border-dashed", "data-slot": "line-draft-row", children: [_jsx("td", { className: CELL, colSpan: span, children: _jsx(LineProductCell, { search: search, text: text, onTextChange: setText, onPick: (p, v) => {
162
214
  setText('');
163
215
  onPick(p, v);
164
216
  }, onKeyDownClosed: (e) => {
@@ -171,7 +223,12 @@ function DraftRow({ search, warehouseId, currency, onPick, onFree, onAddBlank, t
171
223
  onFree(typed);
172
224
  else
173
225
  onAddBlank();
174
- }, warehouseId: warehouseId, currency: currency, ariaLabel: t('lineItems.searchProduct', { defaultValue: 'Buscar producto' }), placeholder: t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), className: "border-dashed bg-transparent shadow-none", dataCell: "search" }) }), Array.from({ length: trailingCells }, (_, i) => (_jsx("td", { "aria-hidden": true }, i)))] }));
226
+ }, warehouseId: warehouseId, currency: currency, ariaLabel: t('lineItems.searchProduct', { defaultValue: 'Buscar producto' }), className: "border-dashed bg-transparent shadow-none", dataCell: "search", onCreate: onCreate
227
+ ? (q) => {
228
+ setText('');
229
+ onCreate(q);
230
+ }
231
+ : undefined, entityLabel: entityLabel }) }), _jsx("td", { "aria-hidden": true }), _jsx("td", { "aria-hidden": true })] }));
175
232
  }
176
233
  /** Fila de totales alineada a la columna «Importe». */
177
234
  function TotalRow({ span, label, value, slot, strong }) {
@@ -179,6 +236,8 @@ function TotalRow({ span, label, value, slot, strong }) {
179
236
  }
180
237
  /** Borrar renglón: aparece al pasar o enfocar la fila (siempre visible en móvil). */
181
238
  const DELETE = 'size-8 text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 max-sm:opacity-100';
239
+ /** Lápiz «Editar producto» dentro de la celda: al pasar o enfocar la fila; siempre con teclado y en móvil. */
240
+ const EDIT = 'absolute right-0.5 top-1/2 size-7 -translate-y-1/2 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 max-sm:opacity-100';
182
241
  /** Celda densa: misma altura en todos los renglones. */
183
242
  const CELL = 'px-2 py-1.5 align-top';
184
243
  /** Inputs de celda: un poco más bajos que los del formulario (densidad de editor). */
@@ -1,3 +1,4 @@
1
+ import type { ProductVariant } from './product-search';
1
2
  export type LineItemKind = 'item' | 'section' | 'note';
2
3
  export interface LineItem {
3
4
  /** Clave estable de UI (no se envía al backend). */
@@ -43,6 +44,33 @@ export interface LineItem {
43
44
  * del host) — el SDK no conoce sus claves.
44
45
  */
45
46
  extensions?: Record<string, string>;
47
+ /**
48
+ * Valores que el catálogo escribió en el renglón la última vez que se
49
+ * aplicó el producto (elegirlo, crearlo o editarlo desde la celda). Un campo
50
+ * cuyo valor actual difiere de su foto es un override del usuario («dirty»)
51
+ * y una edición del producto no lo pisa. No se serializa.
52
+ */
53
+ catalog?: LineCatalogSnapshot;
54
+ /**
55
+ * Valores nuevos del catálogo que NO se aplicaron porque el usuario ya
56
+ * había cambiado ese campo a mano: la celda ofrece «aplicar». No se serializa.
57
+ */
58
+ catalog_pending?: Partial<Pick<LineItem, CatalogLineField>>;
59
+ }
60
+ /** Campos del renglón que salen del producto y el usuario puede sobrescribir. */
61
+ export type CatalogLineField = 'description' | 'sku' | 'unit_price' | 'tax_rate' | 'unit';
62
+ export declare const CATALOG_LINE_FIELDS: readonly CatalogLineField[];
63
+ /** Foto de lo que el catálogo escribió en el renglón (por campo) y de qué producto/variante. */
64
+ export interface LineCatalogSnapshot {
65
+ description?: string;
66
+ sku?: string;
67
+ unit_price?: number;
68
+ tax_rate?: number;
69
+ unit?: string;
70
+ /** Id del producto del catálogo (el registro que se edita), aunque el renglón lleve una variante. */
71
+ product_ref?: string;
72
+ /** Variante elegida: al refrescar se vuelve a aplicar la misma. */
73
+ variant?: ProductVariant;
46
74
  }
47
75
  /** Qué hacer si `quantity > available`. */
48
76
  export type StockPolicy = 'allow' | 'warn' | 'block';
@@ -1 +1 @@
1
- {"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@
3
3
  // guardado de renglones fallaba (PIT-018) por mandar al API cadenas vacías y
4
4
  // claves de UI; `serializeLineItems` es la única puerta de salida hacia el backend.
5
5
  import { roundMoney, toAmount } from './format';
6
+ export const CATALOG_LINE_FIELDS = ['description', 'sku', 'unit_price', 'tax_rate', 'unit'];
6
7
  let seq = 0;
7
8
  /** Clave de UI única en el proceso. */
8
9
  export function newLineKey() {
@@ -25,6 +25,12 @@ export interface LineProductCellProps {
25
25
  className?: string;
26
26
  /** Atributos de datos para que el grid ubique la celda (foco por teclado). */
27
27
  dataCell?: string;
28
+ /** Alta del producto desde la celda, prellenada con lo buscado. */
29
+ onCreate?: (query: string) => void;
30
+ /** Nombre del modelo para «Crear …» (default «producto»). */
31
+ entityLabel?: string;
32
+ /** Detalle de qué se puede buscar: tooltip del buscador y estado sin coincidencias. */
33
+ hint?: string;
28
34
  }
29
35
  export declare const LineProductCell: import("react").ForwardRefExoticComponent<LineProductCellProps & import("react").RefAttributes<HTMLInputElement>>;
30
36
  //# sourceMappingURL=line-product-cell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"line-product-cell.d.ts","sourceRoot":"","sources":["../../src/business/line-product-cell.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAGhE,OAAO,EAKH,KAAK,UAAU,EACf,KAAK,eAAe,EACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAErE,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAE/C,+CAA+C;AAC/C,MAAM,MAAM,cAAc,GAAG,UAAU,CAAA;AAEvC,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,iBAAiB,CAAA;IACzB,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IAClE;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,eAAe,mHA0C1B,CAAA"}
1
+ {"version":3,"file":"line-product-cell.d.ts","sourceRoot":"","sources":["../../src/business/line-product-cell.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAGhE,OAAO,EAKH,KAAK,UAAU,EACf,KAAK,eAAe,EACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAErE,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAE/C,+CAA+C;AAC/C,MAAM,MAAM,cAAc,GAAG,UAAU,CAAA;AAEvC,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,iBAAiB,CAAA;IACzB,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IAClE;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,eAAe,mHA2D1B,CAAA"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // LineProductCell — buscador de producto DENTRO de la celda «Descripción» de un
3
3
  // renglón (patrón de editor de facturas pro: Odoo/Holded/Linear). Es una
4
4
  // configuración del <RecordPicker> compartido (trigger «input», texto libre,
@@ -9,15 +9,21 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  // Teclado: ↑/↓ recorren, Enter elige (el grid pasa el foco a «Cant.»), Esc
10
10
  // cierra, Tab cierra y avanza normal. El texto escrito sin elegir queda como
11
11
  // descripción libre (el renglón libre sigue funcionando igual).
12
+ //
13
+ // Con `onCreate`, la celda ofrece crear el producto como las apps top: «+» unido
14
+ // al buscador y «Crear producto «texto»» al pie de la lista, prellenado con lo
15
+ // buscado. El grid llena el renglón con el producto nuevo al guardarlo.
12
16
  import { forwardRef, useState } from 'react';
13
17
  import { useTranslation } from 'react-i18next';
14
18
  import { RecordPicker } from '../record-picker';
15
19
  import { ProductHitRow, productHitKey, productHitLabel, useProductSearch, } from './product-options';
16
- export const LineProductCell = forwardRef(function LineProductCell({ search, text, onTextChange, onPick, onKeyDownClosed, warehouseId, currency, placeholder, ariaLabel, disabled, invalid, className, dataCell }, ref) {
20
+ export const LineProductCell = forwardRef(function LineProductCell({ search, text, onTextChange, onPick, onKeyDownClosed, warehouseId, currency, placeholder, ariaLabel, disabled, invalid, className, dataCell, onCreate, entityLabel, hint }, ref) {
17
21
  const { t } = useTranslation();
18
22
  // Solo se busca/abre mientras se escribe: enfocar un renglón libre ya
19
23
  // capturado no despliega «sin resultados» encima de la tabla.
20
24
  const [typing, setTyping] = useState(false);
21
25
  const { hits, loading, minChars } = useProductSearch(typing ? text : '', search, typing);
22
- return (_jsx(RecordPicker, { trigger: "input", variant: "cell", freeText: true, anchorSlot: "line-product-cell", contentSlot: "line-product-results", minListWidth: "22rem", items: hits, loading: loading, minChars: minChars, query: text, onQueryChange: onTextChange, open: typing, onOpenChange: setTyping, getKey: productHitKey, getLabel: productHitLabel, renderItem: (hit) => _jsx(ProductHitRow, { hit: hit, warehouseId: warehouseId, currency: currency, compact: true }), onSelect: (hit) => onPick(hit.product, hit.variant), onUnhandledKeyDown: onKeyDownClosed, inputRef: ref, ariaLabel: ariaLabel, listLabel: ariaLabel, disabled: disabled, invalid: invalid, triggerClassName: className, triggerProps: { 'data-cell': dataCell }, placeholder: placeholder ?? t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), emptyText: t('lineItems.noProduct', { defaultValue: 'Sin coincidencias. Se usará como descripción libre.' }) }));
26
+ const entity = entityLabel ?? t('lineItems.productEntity', { defaultValue: 'producto' });
27
+ const searchHint = hint ?? t('lineItems.searchHint', { defaultValue: 'Busca por nombre, SKU o código de barras' });
28
+ return (_jsx(RecordPicker, { trigger: "input", variant: "cell", freeText: true, anchorSlot: "line-product-cell", contentSlot: "line-product-results", minListWidth: "26.25rem", maxListWidth: "35rem", items: hits, loading: loading, minChars: minChars, query: text, onQueryChange: onTextChange, open: typing, onOpenChange: setTyping, getKey: productHitKey, getLabel: productHitLabel, renderItem: (hit) => _jsx(ProductHitRow, { hit: hit, warehouseId: warehouseId, currency: currency, compact: true }), onSelect: (hit) => onPick(hit.product, hit.variant), onUnhandledKeyDown: onKeyDownClosed, inputRef: ref, ariaLabel: ariaLabel, listLabel: ariaLabel, disabled: disabled, invalid: invalid, triggerClassName: className, triggerProps: { 'data-cell': dataCell, title: searchHint }, placeholder: placeholder ?? t('lineItems.searchPlaceholderShort', { defaultValue: 'Buscar producto…' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), emptyText: _jsxs("span", { className: "block space-y-1", children: [_jsx("span", { className: "block", children: t('lineItems.noProduct', { defaultValue: 'Sin coincidencias. Se usará como descripción libre.' }) }), _jsx("span", { className: "block text-xs", children: searchHint })] }), onCreate: onCreate, entityLabel: entity, createLabel: t('lineItems.createProduct', { defaultValue: 'Crear {{entity}}', entity }), createFooterLabel: (q) => (_jsxs(_Fragment, { children: [t('lineItems.createProduct', { defaultValue: 'Crear {{entity}}', entity }), q ? _jsxs("span", { className: "text-muted-foreground", children: [" \u00AB", q, "\u00BB"] }) : null] })) }));
23
29
  });
@@ -0,0 +1,18 @@
1
+ import type { ProductResult } from './product-search';
2
+ interface ApiLike {
3
+ get: (url: string, cfg?: {
4
+ params?: Record<string, unknown>;
5
+ signal?: AbortSignal;
6
+ }) => Promise<{
7
+ data: any;
8
+ }>;
9
+ }
10
+ export interface OptionDisplaysOptions {
11
+ /** Catalog model the options endpoint resolves (e.g. `products.Product`). */
12
+ model: string;
13
+ /** Picker context (`ctx.<key>`), e.g. `{ warehouse_id }`. Empty values are dropped. */
14
+ context?: Record<string, string | null | undefined>;
15
+ }
16
+ export declare function withOptionDisplays<Q>(search: (query: Q, signal: AbortSignal) => Promise<ProductResult[]>, api: ApiLike, opts: OptionDisplaysOptions): (query: Q, signal: AbortSignal) => Promise<ProductResult[]>;
17
+ export {};
18
+ //# sourceMappingURL=product-display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-display.d.ts","sourceRoot":"","sources":["../../src/business/product-display.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,UAAU,OAAO;IACb,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;CACjH;AAED,MAAM,WAAW,qBAAqB;IAClC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAA;IACb,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CACtD;AAKD,wBAAgB,kBAAkB,CAAC,CAAC,EAChC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,EACnE,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,qBAAqB,GAC5B,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CA+B7D"}
@@ -0,0 +1,40 @@
1
+ /** Max ids per lookup (kernel MaxOptionsIDs). */
2
+ const MAX_IDS = 100;
3
+ export function withOptionDisplays(search, api, opts) {
4
+ const ctx = {};
5
+ for (const [k, v] of Object.entries(opts.context ?? {})) {
6
+ if (v != null && String(v).trim())
7
+ ctx[`ctx.${k}`] = String(v).trim();
8
+ }
9
+ return async (query, signal) => {
10
+ const rows = await search(query, signal);
11
+ const ids = [...new Set(rows.filter((r) => !r.display && r.id).map((r) => r.id))].slice(0, MAX_IDS);
12
+ if (ids.length === 0)
13
+ return rows;
14
+ let byId;
15
+ try {
16
+ const res = await api.get(`/options/${opts.model}`, { params: { field: 'id', ids: ids.join(','), ...ctx }, signal });
17
+ const data = res?.data?.data;
18
+ byId = new Map();
19
+ if (Array.isArray(data)) {
20
+ for (const o of data) {
21
+ const d = o && typeof o === 'object' ? o.display : null;
22
+ const id = o && typeof o === 'object' ? o.id ?? o.value : null;
23
+ if (d && typeof d === 'object' && id != null)
24
+ byId.set(String(id).toLowerCase(), d);
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ if (signal.aborted)
30
+ throw new DOMException('Aborted', 'AbortError');
31
+ return rows;
32
+ }
33
+ if (byId.size === 0)
34
+ return rows;
35
+ return rows.map((r) => {
36
+ const d = byId.get(String(r.id).toLowerCase());
37
+ return d && !r.display ? { ...r, display: d } : r;
38
+ });
39
+ };
40
+ }
@@ -31,7 +31,12 @@ export declare function ProductHitTrailing({ hit, warehouseId, currency, compact
31
31
  currency?: string;
32
32
  compact?: boolean;
33
33
  }): import("react").JSX.Element;
34
- /** Option body: bold name (· variant), muted meta line, stock/price right. */
34
+ /**
35
+ * Option body: bold name (· variant), muted meta line, stock/price right. A
36
+ * product whose catalog declares an `option_display` (the server resolved it,
37
+ * see `withOptionDisplays`) paints that row instead: avatar, two-line title,
38
+ * subtitle and toned metrics (price + stock contributed by inventory).
39
+ */
35
40
  export declare function ProductHitRow({ hit, warehouseId, currency, compact }: {
36
41
  hit: ProductHit;
37
42
  warehouseId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"product-options.d.ts","sourceRoot":"","sources":["../../src/business/product-options.tsx"],"names":[],"mappings":"AAUA,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACtB,MAAM,kBAAkB,CAAA;AAEzB,gEAAgE;AAChE,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,CAAC,EAAE,cAAc,CAAA;CAC3B;AAED,MAAM,MAAM,eAAe,GAAG,CAC1B,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAC/C,MAAM,EAAE,WAAW,KAClB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AAE7B,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,UAAU,EAAE,CAIlF;AAED,eAAO,MAAM,aAAa,GAAI,GAAG,UAAU,KAAG,MAAkD,CAAA;AAChG,eAAO,MAAM,eAAe,GAAI,GAAG,UAAU,KAAG,MACyB,CAAA;AACzE,eAAO,MAAM,cAAc,GAAI,GAAG,UAAU,KAAG,MAO3B,CAAA;AAEpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,UAAO;;;;;;;EAerF;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,EAC/B,GAAG,EACH,WAAW,EACX,QAAQ,EACR,OAAe,GAClB,EAAE;IACC,GAAG,EAAE,UAAU,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,+BAsBA;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,+BAarJ"}
1
+ {"version":3,"file":"product-options.d.ts","sourceRoot":"","sources":["../../src/business/product-options.tsx"],"names":[],"mappings":"AAWA,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACtB,MAAM,kBAAkB,CAAA;AAEzB,gEAAgE;AAChE,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,CAAC,EAAE,cAAc,CAAA;CAC3B;AAED,MAAM,MAAM,eAAe,GAAG,CAC1B,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAC/C,MAAM,EAAE,WAAW,KAClB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AAE7B,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,UAAU,EAAE,CAIlF;AAED,eAAO,MAAM,aAAa,GAAI,GAAG,UAAU,KAAG,MAAkD,CAAA;AAChG,eAAO,MAAM,eAAe,GAAI,GAAG,UAAU,KAAG,MACyB,CAAA;AACzE,eAAO,MAAM,cAAc,GAAI,GAAG,UAAU,KAAG,MAO3B,CAAA;AAEpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,UAAO;;;;;;;EAerF;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,EAC/B,GAAG,EACH,WAAW,EACX,QAAQ,EACR,OAAe,GAClB,EAAE;IACC,GAAG,EAAE,UAAU,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,+BAsBA;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,+BAgBrJ"}
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
9
9
  import { Badge } from '@asteby/metacore-ui/primitives';
10
10
  import { useLatestSearch, useRecordSearch } from '../record-picker';
11
11
  import { useFormatter } from './format';
12
+ import { OptionDisplayRow } from '../option-display';
12
13
  import { availableStock, parseProductQuery, } from './product-search';
13
14
  /** Products with variants expand to one row per variant. */
14
15
  export function flattenProductHits(results) {
@@ -51,8 +52,16 @@ export function ProductHitTrailing({ hit, warehouseId, currency, compact = false
51
52
  ? t('productPicker.inStock', { defaultValue: '{{n}} disp.', n: stock })
52
53
  : t('productPicker.outOfStock', { defaultValue: 'Sin existencia' }) }))), price != null && _jsx("span", { className: "tabular-nums", children: fmt.money(price) })] }));
53
54
  }
54
- /** Option body: bold name (· variant), muted meta line, stock/price right. */
55
+ /**
56
+ * Option body: bold name (· variant), muted meta line, stock/price right. A
57
+ * product whose catalog declares an `option_display` (the server resolved it,
58
+ * see `withOptionDisplays`) paints that row instead: avatar, two-line title,
59
+ * subtitle and toned metrics (price + stock contributed by inventory).
60
+ */
55
61
  export function ProductHitRow({ hit, warehouseId, currency, compact }) {
62
+ if (hit.product.display && !hit.variant) {
63
+ return _jsx(OptionDisplayRow, { display: hit.product.display, label: productHitLabel(hit), currency: currency });
64
+ }
56
65
  const meta = productHitMeta(hit);
57
- return (_jsxs("span", { className: "flex w-full min-w-0 items-center justify-between gap-3", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-medium", children: productHitLabel(hit) }), meta && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), _jsx("span", { className: "flex shrink-0 items-center gap-2", children: _jsx(ProductHitTrailing, { hit: hit, warehouseId: warehouseId, currency: currency, compact: compact }) })] }));
66
+ return (_jsxs("span", { className: "flex w-full min-w-0 items-center justify-between gap-3", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "line-clamp-2 break-words font-medium", title: productHitLabel(hit), children: productHitLabel(hit) }), meta && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), _jsx("span", { className: "flex shrink-0 items-center gap-2", children: _jsx(ProductHitTrailing, { hit: hit, warehouseId: warehouseId, currency: currency, compact: compact }) })] }));
58
67
  }
@@ -1,4 +1,5 @@
1
1
  import type { LineItem } from './line-items';
2
+ import type { OptionDisplayData } from '../option-display';
2
3
  export interface TireSize {
3
4
  width: number;
4
5
  ratio: number;
@@ -65,6 +66,13 @@ export interface ProductResult {
65
66
  unit?: string;
66
67
  /** Campos de extensión del producto (fiscal_data.*) que se copian al renglón. */
67
68
  extensions?: Record<string, string>;
69
+ /**
70
+ * Presentación declarativa del catálogo (manifest `option_display` del
71
+ * modelo de producto): la fila del buscador la pinta tal cual — precio,
72
+ * existencia aportada por inventory con su tono, badges. Ver
73
+ * `withOptionDisplays`.
74
+ */
75
+ display?: OptionDisplayData | null;
68
76
  }
69
77
  /** Existencia disponible: de la variante o del producto, en un almacén o en total. */
70
78
  export declare function availableStock(source: {
@@ -1 +1 @@
1
- {"version":3,"file":"product-search.d.ts","sourceRoot":"","sources":["../../src/business/product-search.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"product-search.d.ts","sourceRoot":"","sources":["../../src/business/product-search.ts"],"names":[],"mappings":"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"}
@@ -770,7 +770,7 @@ function FieldRow({ field, record, value, mode, onChange, error, locked }) {
770
770
  // CREATE for a caller-specified field (see `lockedFields`).
771
771
  const isEditReadonly = (mode === 'edit' && !!field.readonly) || !!locked;
772
772
  const { t } = useTranslation();
773
- return (_jsxs("div", { className: "flex flex-col gap-1.5", "data-aby-field": field.key, children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [localizeFieldLabel(field.label, t), field.required && mode !== 'view' && !isEditReadonly && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), mode === 'view' ? (_jsx(ViewValue, { field: field, value: value, record: record })) : isEditReadonly ? (_jsx(ReadonlyEditField, { field: field, value: value })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record, invalid: !!error })), error && mode !== 'view' && (_jsx("p", { className: "text-destructive text-xs mt-1", children: error }))] }));
773
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", "data-aby-field": field.key, children: [_jsxs(Label, { className: "text-sm font-medium text-foreground/80", children: [localizeFieldLabel(field.label, t), field.required && mode !== 'view' && !isEditReadonly && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), mode === 'view' ? (_jsx(ViewValue, { field: field, value: value, record: record })) : isEditReadonly ? (_jsx(ReadonlyEditField, { field: field, value: value })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record, invalid: !!error })), error && mode !== 'view' && (_jsx("p", { className: "text-destructive text-xs mt-1", children: error }))] }));
774
774
  }
775
775
  // ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
776
776
  // field: a disabled, muted input that shows the current value without allowing
@@ -73,6 +73,12 @@ export interface DynamicSelectFieldProps {
73
73
  createLockedFields?: string[];
74
74
  /** Paint trigger with destructive border when validation failed. */
75
75
  invalid?: boolean;
76
+ /**
77
+ * Picker context for the declarative option display's contributed
78
+ * metrics (`?ctx.<key>=`), e.g. `{ warehouse_id }` of the document being
79
+ * edited so the product stock is the one of that warehouse.
80
+ */
81
+ optionsContext?: Record<string, string | null | undefined>;
76
82
  }
77
83
  /**
78
84
  * Declarative `dynamic_select` (manifest `ref` / `optionsConfig.source`) on top
@@ -80,6 +86,6 @@ export interface DynamicSelectFieldProps {
80
86
  * endpoint (`useOptionsResolver`: option_filter, options[].when keep-value,
81
87
  * cascade `filter_value`), inline create/edit through the host modal events.
82
88
  */
83
- export declare function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, }: DynamicSelectFieldProps): import("react").JSX.Element;
89
+ export declare function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, optionsContext, }: DynamicSelectFieldProps): import("react").JSX.Element;
84
90
  export default DynamicSelectField;
85
91
  //# 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":"AAkCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAIhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEhE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,yBAAyB,CAAA;AAE1D,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;;;;;OAKG;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;;;;;GAKG;AACH,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,+BAqPzB;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAmCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAIhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEhE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,yBAAyB,CAAA;AAE1D,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;;;;;OAKG;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;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CAC7D;AAED;;;;;GAKG;AACH,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,EACf,cAAc,GACjB,EAAE,uBAAuB,+BAyRzB;AAED,eAAe,kBAAkB,CAAA"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // DynamicSelectField — async, searchable single-select for declarative forms.
3
3
  // A configuration of the shared <RecordPicker> (record-picker.tsx).
4
4
  //
@@ -28,10 +28,11 @@ import { useEffect, useRef, useState } from 'react';
28
28
  import { getOptionFilter } from './option-filter';
29
29
  import { useTranslation } from 'react-i18next';
30
30
  import { Badge, Button } from '@asteby/metacore-ui/primitives';
31
- import { ScanLine } from 'lucide-react';
31
+ import { Check, ScanLine } from 'lucide-react';
32
32
  import { BarcodeScanner } from './barcode-scanner';
33
33
  import { RecordPicker } from './record-picker';
34
34
  import { OptionLead } from './record-picker-option';
35
+ import { OptionDisplayRow, OptionDisplayValue, getOptionDisplay, hasOptionDisplays } from './option-display';
35
36
  import { recordLabel, requestRecordCreate, requestRecordEdit, withSearchPrefill } from './record-picker-actions';
36
37
  import { useOptionsResolver } from './use-options-resolver';
37
38
  import { useResolveOptionIds } from './use-option-ids';
@@ -52,7 +53,7 @@ export const DELETED_RECORD_LABEL = '(registro eliminado)';
52
53
  * endpoint (`useOptionsResolver`: option_filter, options[].when keep-value,
53
54
  * cascade `filter_value`), inline create/edit through the host modal events.
54
55
  */
55
- export function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
56
+ export function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, optionsContext, }) {
56
57
  const { t } = useTranslation();
57
58
  const ph = (fallback) => field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback;
58
59
  const [open, setOpen] = useState(false);
@@ -95,6 +96,7 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
95
96
  filterValue: dependsOn ? scope : undefined,
96
97
  optionFilter,
97
98
  keepValue: value,
99
+ context: optionsContext,
98
100
  // Fetch only while open (a held value's label comes from the `?ids=`
99
101
  // lookup below, readonly included). Blocked cascades and static lists
100
102
  // never fetch.
@@ -188,16 +190,28 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
188
190
  };
189
191
  const canMutate = !!fieldRef && !hideCreate && !useStatic && !blockedByDependency;
190
192
  const fieldName = field.label ? t(field.label, { defaultValue: field.label }) : fieldRef ?? '';
193
+ // Declarative option display (manifest `option_display`): rich rows with
194
+ // avatar, two-line title, subtitle and toned metrics (price, stock…).
195
+ const richRows = hasOptionDisplays(options);
196
+ const selectedDisplay = getOptionDisplay(selectedOption);
191
197
  const badge = (opt) => opt.description ? (_jsx(Badge, { variant: "secondary", className: "shrink-0 font-normal tabular-nums", children: opt.description })) : null;
192
198
  return (_jsx(RecordPicker, { slot: "dynamic-select", id: field.key, items: options, loading: loading, getKey: (o) => String(o.id), getLabel: (o) => o.label, value: value, selected: selectedOption, onSelect: handlePick, query: search, onQueryChange: setSearch, open: open && !blockedByDependency, onOpenChange: (o) => {
193
199
  if (!blockedByDependency)
194
200
  setOpen(o);
195
- }, disabled: blockedByDependency, readOnly: readonly, invalid: invalid, renderLead: (o, where) => _jsx(OptionLead, { option: o, size: where === 'option' ? 24 : 20 }), getDescription: descriptionAsBadge ? undefined : (o) => o.description, renderTrailing: descriptionAsBadge ? (o) => badge(o) : undefined, renderValue: () => {
201
+ }, disabled: blockedByDependency, readOnly: readonly, invalid: invalid, renderLead: (o, where) => _jsx(OptionLead, { option: o, size: where === 'option' ? 24 : 20 }), renderItem: richRows
202
+ ? (o, st) => {
203
+ const d = getOptionDisplay(o);
204
+ return (_jsxs(_Fragment, { children: [d ? (_jsx(OptionDisplayRow, { display: d, label: o.label, active: st.active, selected: st.selected })) : (_jsxs("span", { className: "flex w-full min-w-0 items-center gap-3", children: [_jsx(OptionLead, { option: o, size: 32 }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "line-clamp-2 text-sm font-medium", children: o.label }), o.description ? _jsx("span", { className: "truncate text-xs text-muted-foreground", children: o.description }) : null] })] })), _jsx(Check, { className: 'size-4 shrink-0 text-primary ' + (st.selected ? 'opacity-100' : 'opacity-0'), "aria-hidden": true })] }));
205
+ }
206
+ : undefined, isItemDisabled: richRows ? (o) => !!getOptionDisplay(o)?.blocked : undefined, minListWidth: richRows ? '26rem' : undefined, getDescription: descriptionAsBadge ? undefined : (o) => o.description, renderTrailing: descriptionAsBadge ? (o) => badge(o) : undefined, renderValue: () => {
196
207
  // Never flash the raw id while the label resolves.
197
208
  if (valueResolving)
198
209
  return resolvingNode;
199
210
  if (valueMissing)
200
211
  return missingNode;
212
+ if (selectedDisplay && value && !blockedByDependency) {
213
+ return _jsx(OptionDisplayValue, { display: selectedDisplay, label: selectedOption?.label });
214
+ }
201
215
  if (readonly) {
202
216
  return (_jsxs(_Fragment, { children: [selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedOption ? '' : 'text-muted-foreground'), children: selectedOption?.label ?? ph('—') })] }));
203
217
  }