@asteby/metacore-runtime-react 49.7.0 → 49.8.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 (40) hide show
  1. package/CHANGELOG.md +6 -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 +116 -21
  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/line-items-editor.d.ts +21 -1
  12. package/dist/business/line-items-editor.d.ts.map +1 -1
  13. package/dist/business/line-items-editor.js +74 -15
  14. package/dist/business/line-items.d.ts +28 -0
  15. package/dist/business/line-items.d.ts.map +1 -1
  16. package/dist/business/line-items.js +1 -0
  17. package/dist/business/line-product-cell.d.ts +6 -0
  18. package/dist/business/line-product-cell.d.ts.map +1 -1
  19. package/dist/business/line-product-cell.js +9 -3
  20. package/dist/business/product-options.js +1 -1
  21. package/dist/dialogs/dynamic-record.js +1 -1
  22. package/dist/field-grid.d.ts +4 -3
  23. package/dist/field-grid.d.ts.map +1 -1
  24. package/dist/field-grid.js +1 -1
  25. package/dist/record-picker.d.ts +2 -0
  26. package/dist/record-picker.d.ts.map +1 -1
  27. package/dist/record-picker.js +11 -4
  28. package/package.json +1 -1
  29. package/src/__tests__/document-editor-inline-selects.test.tsx +11 -4
  30. package/src/__tests__/line-row-create-edit.test.tsx +312 -0
  31. package/src/business/document-editor-model.ts +60 -2
  32. package/src/business/document-editor.tsx +132 -17
  33. package/src/business/document-lines.ts +79 -7
  34. package/src/business/line-items-editor.tsx +158 -30
  35. package/src/business/line-items.ts +31 -0
  36. package/src/business/line-product-cell.tsx +32 -5
  37. package/src/business/product-options.tsx +1 -1
  38. package/src/dialogs/dynamic-record.tsx +1 -1
  39. package/src/field-grid.tsx +5 -4
  40. package/src/record-picker.tsx +16 -3
@@ -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
  });
@@ -54,5 +54,5 @@ export function ProductHitTrailing({ hit, warehouseId, currency, compact = false
54
54
  /** Option body: bold name (· variant), muted meta line, stock/price right. */
55
55
  export function ProductHitRow({ hit, warehouseId, currency, compact }) {
56
56
  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 }) })] }));
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: "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
58
  }
@@ -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
@@ -13,9 +13,10 @@ export declare function FieldLabel({ htmlFor, required, tone, children, }: {
13
13
  htmlFor?: string;
14
14
  required?: boolean;
15
15
  /**
16
- * `caps` (default): small uppercase eyebrow of the CRUD modals. `sentence`:
17
- * sentence-case label for dense editors (DocumentEditor) where a column of
18
- * uppercase labels reads as shouting.
16
+ * `sentence` (default): sentence-case label — every form of the runtime
17
+ * (CRUD modal, action modal, DocumentEditor) reads the same and a column of
18
+ * uppercase labels no longer shouts. `caps`: small uppercase eyebrow, for
19
+ * callers that still want it.
19
20
  */
20
21
  tone?: 'caps' | 'sentence';
21
22
  children: ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"field-grid.d.ts","sourceRoot":"","sources":["../src/field-grid.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,+BAM7F;AAED,wBAAgB,SAAS,CAAC,EACtB,QAAQ,EACR,SAAS,EACT,SAAS,GACZ,EAAE;IACC,QAAQ,EAAE,SAAS,CAAA;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,+BAMA;AAED,wBAAgB,UAAU,CAAC,EACvB,OAAO,EACP,QAAQ,EACR,IAAa,EACb,QAAQ,GACX,EAAE;IACC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;CACtB,+BAcA"}
1
+ {"version":3,"file":"field-grid.d.ts","sourceRoot":"","sources":["../src/field-grid.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,+BAM7F;AAED,wBAAgB,SAAS,CAAC,EACtB,QAAQ,EACR,SAAS,EACT,SAAS,GACZ,EAAE;IACC,QAAQ,EAAE,SAAS,CAAA;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,+BAMA;AAED,wBAAgB,UAAU,CAAC,EACvB,OAAO,EACP,QAAQ,EACR,IAAiB,EACjB,QAAQ,GACX,EAAE;IACC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;CACtB,+BAcA"}
@@ -7,7 +7,7 @@ export function FieldGrid({ children, className }) {
7
7
  export function FieldCell({ children, fullWidth, className, }) {
8
8
  return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-1.5', fullWidth && 'sm:col-span-2', className), children: children }));
9
9
  }
10
- export function FieldLabel({ htmlFor, required, tone = 'caps', children, }) {
10
+ export function FieldLabel({ htmlFor, required, tone = 'sentence', children, }) {
11
11
  return (_jsxs(Label, { htmlFor: htmlFor, className: tone === 'sentence'
12
12
  ? 'text-sm font-medium text-foreground/80'
13
13
  : 'text-xs font-medium uppercase tracking-wide text-muted-foreground', children: [children, required && _jsx("span", { className: "ml-0.5 text-destructive", children: "*" })] }));
@@ -134,6 +134,8 @@ export interface RecordPickerProps<T> {
134
134
  inputRef?: Ref<HTMLInputElement>;
135
135
  /** Floor for the list width (CSS length). Default 14rem. */
136
136
  minListWidth?: string;
137
+ /** Ceiling for the list width (CSS length), e.g. under a very wide trigger. Default: none. */
138
+ maxListWidth?: string;
137
139
  /** Rendered after the joined group (e.g. a scan button). */
138
140
  after?: ReactNode;
139
141
  /** Rendered under the control (badges, hints). */
@@ -1 +1 @@
1
- {"version":3,"file":"record-picker.d.ts","sourceRoot":"","sources":["../src/record-picker.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"record-picker.d.ts","sourceRoot":"","sources":["../src/record-picker.tsx"],"names":[],"mappings":"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"}
@@ -111,7 +111,7 @@ function asKeys(value, multiple) {
111
111
  return [String(value)];
112
112
  }
113
113
  export function RecordPicker(props) {
114
- const { getKey, getLabel, items, search, debounceMs = 250, multiple = false, value, selected, onSelect, onClear, onRemove, renderItem, renderLead, getDescription, renderTrailing, renderValue, isItemDisabled, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', loadingText = 'Buscando…', errorText = 'No se pudo buscar. Inténtalo de nuevo.', minCharsText, clearLabel = 'Quitar selección', listLabel, entityLabel = '', onCreate, onEdit, createLabel, editLabel, createFooter = 'always', createFooterLabel, hideJoinedAction = false, trigger = 'button', variant = 'field', freeText = false, icon: Icon, id, ariaLabel, disabled = false, readOnly = false, invalid = false, slot, anchorSlot, contentSlot = 'record-picker-content', className, triggerClassName, triggerProps, inputRef, minListWidth = '14rem', after, below, beforeList, onInputKeyDown, onUnhandledKeyDown, } = props;
114
+ const { getKey, getLabel, items, search, debounceMs = 250, multiple = false, value, selected, onSelect, onClear, onRemove, renderItem, renderLead, getDescription, renderTrailing, renderValue, isItemDisabled, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', loadingText = 'Buscando…', errorText = 'No se pudo buscar. Inténtalo de nuevo.', minCharsText, clearLabel = 'Quitar selección', listLabel, entityLabel = '', onCreate, onEdit, createLabel, editLabel, createFooter = 'always', createFooterLabel, hideJoinedAction = false, trigger = 'button', variant = 'field', freeText = false, icon: Icon, id, ariaLabel, disabled = false, readOnly = false, invalid = false, slot, anchorSlot, contentSlot = 'record-picker-content', className, triggerClassName, triggerProps, inputRef, minListWidth = '14rem', maxListWidth, after, below, beforeList, onInputKeyDown, onUnhandledKeyDown, } = props;
115
115
  const isInput = trigger === 'input';
116
116
  const minChars = props.minChars ?? (isInput ? 2 : 0);
117
117
  const showCheck = props.showCheck ?? !isInput;
@@ -278,7 +278,12 @@ export function RecordPicker(props) {
278
278
  return (_jsx("div", { className: "px-2 py-6 text-center text-sm text-muted-foreground", "data-slot": "picker-empty", children: emptyText }));
279
279
  })();
280
280
  const optionClass = (on, dis) => cx('relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden', on && 'bg-accent text-accent-foreground', dis && 'pointer-events-none opacity-50');
281
- const list = (_jsxs("div", { className: "max-h-72 overflow-y-auto overflow-x-hidden overscroll-contain", "data-slot": "record-picker-list", children: [beforeList, status, _jsxs("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? ariaLabel ?? entityLabel ?? undefined, "aria-multiselectable": multiple || undefined, className: cx('p-1', count === 0 && 'hidden'), children: [rows.map((item, i) => {
281
+ const list = (_jsxs("div", { className: "overflow-y-auto overflow-x-hidden overscroll-contain", "data-slot": "record-picker-list",
282
+ // Never taller than the room Radix measured on the side it opened
283
+ // (flipped above the field near the bottom of a modal it used to
284
+ // spill past the top of the viewport): 18rem at most, minus the
285
+ // search box in button mode; the list scrolls inside.
286
+ style: { maxHeight: `min(18rem, calc(var(--radix-popover-content-available-height, 18rem) - ${isInput ? '2px' : '2.75rem'}))` }, children: [beforeList, status, _jsxs("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? ariaLabel ?? entityLabel ?? undefined, "aria-multiselectable": multiple || undefined, className: cx('p-1', count === 0 && 'hidden'), children: [rows.map((item, i) => {
282
287
  const sel = isSelected(item);
283
288
  const dis = !!isItemDisabled?.(item);
284
289
  const on = i === active;
@@ -286,10 +291,12 @@ export function RecordPicker(props) {
286
291
  return (_jsx("li", { id: `${listId}-o${i}`, role: "option", "aria-selected": multiple ? sel : on, "aria-disabled": dis || undefined, "data-active": on || undefined, "data-selected": sel || undefined, "data-slot": "record-picker-option", className: optionClass(on, dis), onMouseDown: (e) => e.preventDefault(), onMouseMove: () => {
287
292
  if (!on && !dis)
288
293
  setActive(i);
289
- }, onClick: () => choose(i), children: renderItem ? (renderItem(item, { active: on, selected: sel, disabled: dis })) : (_jsxs(_Fragment, { children: [showCheck ? (_jsx(Check, { className: cx('size-4 shrink-0', sel ? 'opacity-100' : 'opacity-0'), "aria-hidden": true })) : null, renderLead?.(item, 'option'), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate", children: getLabel(item) }), desc != null && desc !== '' ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: desc })) : null] }), renderTrailing ? (_jsx("span", { className: "ml-auto flex shrink-0 items-center gap-2", children: renderTrailing(item, 'option') })) : null] })) }, getKey(item)));
294
+ }, onClick: () => choose(i), children: renderItem ? (renderItem(item, { active: on, selected: sel, disabled: dis })) : (_jsxs(_Fragment, { children: [showCheck ? (_jsx(Check, { className: cx('size-4 shrink-0', sel ? 'opacity-100' : 'opacity-0'), "aria-hidden": true })) : null, renderLead?.(item, 'option'), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "line-clamp-2 break-words", children: getLabel(item) }), desc != null && desc !== '' ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: desc })) : null] }), renderTrailing ? (_jsx("span", { className: "ml-auto flex shrink-0 items-center gap-2", children: renderTrailing(item, 'option') })) : null] })) }, getKey(item)));
290
295
  }), showFooter ? (_jsxs(_Fragment, { children: [rows.length > 0 ? _jsx("li", { role: "presentation", className: "-mx-1 my-1 h-px bg-border" }) : null, _jsxs("li", { id: `${listId}-o${rows.length}`, role: "option", "aria-selected": active === rows.length, "data-active": active === rows.length || undefined, "data-slot": "picker-create", className: cx(optionClass(active === rows.length, false), 'text-primary'), onMouseDown: (e) => e.preventDefault(), onMouseMove: () => setActive(rows.length), onClick: runCreate, children: [_jsx(Plus, { className: "size-4 shrink-0", "aria-hidden": true }), _jsxs("span", { className: "truncate", children: [createFooterLabel ? createFooterLabel(trimmed) : `Crear ${entityLabel}`.trim(), !createFooterLabel && trimmed ? _jsxs("span", { className: "text-muted-foreground", children: [" \u00AB", trimmed, "\u00BB"] }) : null] })] })] })) : null] })] }));
291
296
  const contentStyle = {
292
- width: `max(var(--radix-popover-trigger-width), ${minListWidth})`,
297
+ width: maxListWidth
298
+ ? `min(max(var(--radix-popover-trigger-width), ${minListWidth}), ${maxListWidth})`
299
+ : `max(var(--radix-popover-trigger-width), ${minListWidth})`,
293
300
  maxWidth: 'calc(100vw - 1rem)',
294
301
  zIndex: RECORD_PICKER_Z_INDEX,
295
302
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "49.7.0",
3
+ "version": "49.8.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,10 +47,17 @@ describe('DocumentLinesGrid — select de producto en la celda', () => {
47
47
  const input = screen.getByRole('combobox', { name: 'Buscar producto' })
48
48
  const td = input.closest('td')!
49
49
  expect(td).toBeTruthy()
50
- // Ocupa solo su celda: no hay colSpan sobre toda la fila.
51
- expect(td.colSpan).toBe(1)
52
- expect(td.closest('tr')!.getAttribute('data-slot')).toBe('line-draft-row')
53
- expect(td.closest('tr')!.children.length).toBeGreaterThan(1)
50
+ // No es una barra a todo lo ancho: el renglón vacío conserva las celdas
51
+ // de «Importe» y acciones; el buscador usa el ancho de las columnas que
52
+ // ese renglón aún no llena (cantidad, precio…), así no se corta.
53
+ const row = td.closest('tr')!
54
+ expect(row.getAttribute('data-slot')).toBe('line-draft-row')
55
+ const columns = document.querySelectorAll('[data-slot="line-items-editor"] thead th').length
56
+ expect(td.colSpan).toBe(columns - 2)
57
+ expect(row.children.length).toBe(3)
58
+ // Placeholder corto que cabe; el detalle va como ayuda.
59
+ expect(input.getAttribute('placeholder')).toBe('Buscar producto…')
60
+ expect(input.getAttribute('title')).toMatch(/SKU o código de barras/)
54
61
  })
55
62
 
56
63
  it('la lista sale en un portal fuera del contenedor con overflow y Enter elige y pasa a «Cant.»', async () => {