@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.
- package/CHANGELOG.md +25 -0
- package/dist/business/document-editor-model.d.ts +27 -2
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +53 -3
- package/dist/business/document-editor.d.ts +7 -2
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +121 -22
- package/dist/business/document-lines.d.ts +21 -1
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +69 -7
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-items-editor.d.ts +21 -1
- package/dist/business/line-items-editor.d.ts.map +1 -1
- package/dist/business/line-items-editor.js +74 -15
- package/dist/business/line-items.d.ts +28 -0
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +1 -0
- package/dist/business/line-product-cell.d.ts +6 -0
- package/dist/business/line-product-cell.d.ts.map +1 -1
- package/dist/business/line-product-cell.js +9 -3
- package/dist/business/product-display.d.ts +18 -0
- package/dist/business/product-display.d.ts.map +1 -0
- package/dist/business/product-display.js +40 -0
- package/dist/business/product-options.d.ts +6 -1
- package/dist/business/product-options.d.ts.map +1 -1
- package/dist/business/product-options.js +11 -2
- package/dist/business/product-search.d.ts +8 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-select-field.d.ts +7 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +18 -4
- package/dist/field-grid.d.ts +4 -3
- package/dist/field-grid.d.ts.map +1 -1
- package/dist/field-grid.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/option-display.d.ts +68 -0
- package/dist/option-display.d.ts.map +1 -0
- package/dist/option-display.js +135 -0
- package/dist/query-batch.d.ts +1 -1
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +3 -1
- package/dist/record-picker.d.ts +2 -0
- package/dist/record-picker.d.ts.map +1 -1
- package/dist/record-picker.js +11 -4
- package/dist/use-options-resolver.d.ts +16 -1
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +38 -6
- package/package.json +1 -1
- package/src/__tests__/document-editor-inline-selects.test.tsx +11 -4
- package/src/__tests__/line-row-create-edit.test.tsx +312 -0
- package/src/__tests__/option-display.test.tsx +241 -0
- package/src/business/document-editor-model.ts +60 -2
- package/src/business/document-editor.tsx +143 -19
- package/src/business/document-lines.ts +79 -7
- package/src/business/index.ts +1 -0
- package/src/business/line-items-editor.tsx +158 -30
- package/src/business/line-items.ts +31 -0
- package/src/business/line-product-cell.tsx +32 -5
- package/src/business/product-display.ts +62 -0
- package/src/business/product-options.tsx +11 -2
- package/src/business/product-search.ts +8 -0
- package/src/dialogs/dynamic-record.tsx +1 -1
- package/src/dynamic-select-field.tsx +45 -1
- package/src/field-grid.tsx +5 -4
- package/src/index.ts +10 -0
- package/src/option-display.tsx +255 -0
- package/src/query-batch.ts +2 -0
- package/src/record-picker.tsx +16 -3
- 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(
|
|
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 && !
|
|
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 =
|
|
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(
|
|
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) })
|
|
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()]),
|
|
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,
|
|
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' }),
|
|
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":"
|
|
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":"
|
|
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
|
-
|
|
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
|
-
/**
|
|
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":"
|
|
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
|
-
/**
|
|
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: "
|
|
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":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;
|
|
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-
|
|
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":"
|
|
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,
|
|
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 }),
|
|
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
|
}
|