@asteby/metacore-runtime-react 49.6.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.
- package/CHANGELOG.md +12 -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 +116 -21
- 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/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-options.js +1 -1
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +22 -4
- package/dist/dynamic-select-field.d.ts +2 -0
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +42 -15
- 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 +2 -0
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +4 -0
- 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-option-ids.d.ts +34 -0
- package/dist/use-option-ids.d.ts.map +1 -0
- package/dist/use-option-ids.js +138 -0
- package/dist/use-options-resolver.d.ts +4 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +10 -1
- package/package.json +1 -1
- package/src/__tests__/document-editor-inline-selects.test.tsx +11 -4
- package/src/__tests__/edit-field-polish.test.tsx +6 -3
- package/src/__tests__/line-row-create-edit.test.tsx +312 -0
- package/src/__tests__/option-ids.test.tsx +181 -0
- package/src/business/document-editor-model.ts +60 -2
- package/src/business/document-editor.tsx +132 -17
- package/src/business/document-lines.ts +79 -7
- 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-options.tsx +1 -1
- package/src/dialogs/dynamic-record.tsx +1 -1
- package/src/dynamic-multi-select-field.tsx +20 -8
- package/src/dynamic-select-field.tsx +51 -17
- package/src/field-grid.tsx +5 -4
- package/src/index.ts +8 -0
- package/src/query-batch.ts +3 -0
- package/src/record-picker.tsx +16 -3
- package/src/use-option-ids.ts +168 -0
- package/src/use-options-resolver.ts +11 -1
|
@@ -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
|
});
|
|
@@ -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: "
|
|
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-
|
|
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-multi-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-multi-select-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-multi-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-multi-select-field.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,4BAA4B;IACzC,KAAK,EAAE,cAAc,CAAA;IACrB,qFAAqF;IACrF,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,uBAAuB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,4BAA4B,+BA0D/F"}
|
|
@@ -18,16 +18,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
18
18
|
// (segments, tags, categories — tens, not thousands of rows); a field with a
|
|
19
19
|
// genuinely large option set should keep using a single dynamic_select per
|
|
20
20
|
// value instead.
|
|
21
|
+
//
|
|
22
|
+
// Selected ids outside that page (or hidden by an option_filter) are labelled
|
|
23
|
+
// with ONE `?ids=` lookup (useResolveOptionIds): a chip reads "Cargando…" while
|
|
24
|
+
// it resolves and "(registro eliminado)" when the record is gone — never the id.
|
|
21
25
|
import { useMemo, useState } from 'react';
|
|
22
26
|
import { getOptionFilter } from './option-filter';
|
|
23
27
|
import { RecordPicker } from './record-picker';
|
|
24
28
|
import { OptionLead } from './record-picker-option';
|
|
25
29
|
import { useOptionsResolver } from './use-options-resolver';
|
|
30
|
+
import { useResolveOptionIds } from './use-option-ids';
|
|
31
|
+
import { DELETED_RECORD_LABEL } from './dynamic-select-field';
|
|
26
32
|
import { getFieldRef } from './dynamic-form-schema';
|
|
27
33
|
export function DynamicMultiSelectField({ field, value, onChange }) {
|
|
28
34
|
const ref = getFieldRef(field);
|
|
29
35
|
const [query, setQuery] = useState('');
|
|
30
|
-
const { options, loading } = useOptionsResolver({
|
|
36
|
+
const { options, loading, meta, error } = useOptionsResolver({
|
|
31
37
|
modelKey: '',
|
|
32
38
|
fieldKey: 'id',
|
|
33
39
|
ref,
|
|
@@ -36,9 +42,21 @@ export function DynamicMultiSelectField({ field, value, onChange }) {
|
|
|
36
42
|
optionFilter: getOptionFilter(field),
|
|
37
43
|
});
|
|
38
44
|
const selected = useMemo(() => (Array.isArray(value) ? value.map(String) : []), [value]);
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const endpoint = !ref && field.searchEndpoint ? field.searchEndpoint : undefined;
|
|
46
|
+
// Ask for the ids the loaded page does not cover — only once it is in.
|
|
47
|
+
const pageSettled = !loading && (meta !== null || error !== null);
|
|
48
|
+
const unresolved = pageSettled ? selected.filter((id) => !options.some((o) => String(o.id) === id)) : [];
|
|
49
|
+
const { resolved } = useResolveOptionIds({ ref, endpoint, field: 'id', ids: unresolved, enabled: unresolved.length > 0 });
|
|
50
|
+
const selectedItems = selected.map((id) => {
|
|
51
|
+
const loaded = options.find((o) => String(o.id) === id);
|
|
52
|
+
if (loaded)
|
|
53
|
+
return loaded;
|
|
54
|
+
const r = pageSettled ? resolved.get(id) : undefined;
|
|
55
|
+
if (r?.status === 'found')
|
|
56
|
+
return r.option;
|
|
57
|
+
const label = !r ? 'Cargando…' : r.status === 'missing' ? DELETED_RECORD_LABEL : id;
|
|
58
|
+
return { id, value: id, label, name: label };
|
|
59
|
+
});
|
|
42
60
|
const shown = useMemo(() => {
|
|
43
61
|
const q = query.trim().toLowerCase();
|
|
44
62
|
return q ? options.filter((o) => String(o.label ?? '').toLowerCase().includes(q)) : options;
|
|
@@ -7,6 +7,8 @@ export { OptionLead, OptionThumb } from './record-picker-option';
|
|
|
7
7
|
* `dependsHint`.
|
|
8
8
|
*/
|
|
9
9
|
export declare const DEFAULT_DEPENDS_HINT = "Selecciona primero el campo del que depende";
|
|
10
|
+
/** Label of a held value whose record no longer exists (or is not visible). */
|
|
11
|
+
export declare const DELETED_RECORD_LABEL = "(registro eliminado)";
|
|
10
12
|
export interface DynamicSelectFieldProps {
|
|
11
13
|
field: ActionFieldDef;
|
|
12
14
|
value: any;
|
|
@@ -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":"AAkCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAIhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEhE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,yBAAyB,CAAA;AAE1D,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;IACtC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI,CAAA;IAChD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAAM,EACN,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,aAAoB,EACpB,kBAA0B,EAC1B,UAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,OAAe,GAClB,EAAE,uBAAuB,+BAqPzB;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -19,10 +19,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
19
19
|
// 1. field.ref → /options/<ref>?field=id (canonical, preferred)
|
|
20
20
|
// 2. field.searchEndpoint→ used verbatim as the options endpoint (escape hatch)
|
|
21
21
|
//
|
|
22
|
-
// Edit
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// Edit mode: an EXISTING value is labelled from (in order) the option the user
|
|
23
|
+
// just picked, the loaded page, the caller's `seedOption`, and otherwise one
|
|
24
|
+
// `?ids=` lookup (useResolveOptionIds — batched and shared with every picker of
|
|
25
|
+
// the same ref on screen). While it resolves the trigger reads "Cargando…",
|
|
26
|
+
// never the raw id; an id that no longer exists reads "(registro eliminado)".
|
|
26
27
|
import { useEffect, useRef, useState } from 'react';
|
|
27
28
|
import { getOptionFilter } from './option-filter';
|
|
28
29
|
import { useTranslation } from 'react-i18next';
|
|
@@ -33,6 +34,7 @@ import { RecordPicker } from './record-picker';
|
|
|
33
34
|
import { OptionLead } from './record-picker-option';
|
|
34
35
|
import { recordLabel, requestRecordCreate, requestRecordEdit, withSearchPrefill } from './record-picker-actions';
|
|
35
36
|
import { useOptionsResolver } from './use-options-resolver';
|
|
37
|
+
import { useResolveOptionIds } from './use-option-ids';
|
|
36
38
|
import { useDebouncedValue } from './use-debounced-value';
|
|
37
39
|
import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
|
|
38
40
|
export { OptionLead, OptionThumb } from './record-picker-option';
|
|
@@ -42,6 +44,8 @@ export { OptionLead, OptionThumb } from './record-picker-option';
|
|
|
42
44
|
* `dependsHint`.
|
|
43
45
|
*/
|
|
44
46
|
export const DEFAULT_DEPENDS_HINT = 'Selecciona primero el campo del que depende';
|
|
47
|
+
/** Label of a held value whose record no longer exists (or is not visible). */
|
|
48
|
+
export const DELETED_RECORD_LABEL = '(registro eliminado)';
|
|
45
49
|
/**
|
|
46
50
|
* Declarative `dynamic_select` (manifest `ref` / `optionsConfig.source`) on top
|
|
47
51
|
* of the shared {@link RecordPicker}: options from the canonical options
|
|
@@ -78,21 +82,23 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
78
82
|
const blockedByDependency = !!dependsOn && scope === '';
|
|
79
83
|
const useStatic = Array.isArray(staticOptions);
|
|
80
84
|
const optionFilter = getOptionFilter(field);
|
|
85
|
+
// optionsConfig.source → `/options/<source>`. Else searchEndpoint only
|
|
86
|
+
// drives the URL when there's no ref (ref is canonical and wins).
|
|
87
|
+
const optionsEndpoint = source.endpoint ?? (source.ref ? undefined : field.searchEndpoint);
|
|
81
88
|
const { options: fetchedOptions, loading: fetchLoading } = useOptionsResolver({
|
|
82
89
|
modelKey: '',
|
|
83
90
|
fieldKey: source.fieldKey,
|
|
84
91
|
ref: source.ref,
|
|
85
|
-
|
|
86
|
-
// drives the URL when there's no ref (ref is canonical and wins).
|
|
87
|
-
endpoint: source.endpoint ?? (source.ref ? undefined : field.searchEndpoint),
|
|
92
|
+
endpoint: optionsEndpoint,
|
|
88
93
|
query: debounced,
|
|
89
94
|
limit: 20,
|
|
90
95
|
filterValue: dependsOn ? scope : undefined,
|
|
91
96
|
optionFilter,
|
|
92
97
|
keepValue: value,
|
|
93
|
-
// Fetch only while open
|
|
94
|
-
//
|
|
95
|
-
|
|
98
|
+
// Fetch only while open (a held value's label comes from the `?ids=`
|
|
99
|
+
// lookup below, readonly included). Blocked cascades and static lists
|
|
100
|
+
// never fetch.
|
|
101
|
+
enabled: !useStatic && open && !blockedByDependency,
|
|
96
102
|
});
|
|
97
103
|
const options = useStatic
|
|
98
104
|
? staticOptions.filter((o) => {
|
|
@@ -116,11 +122,27 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
116
122
|
onChange('');
|
|
117
123
|
}
|
|
118
124
|
}, [dependsOn, scope, value, onChange]);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
(
|
|
125
|
+
const valueKey = value != null && value !== '' ? String(value) : '';
|
|
126
|
+
const knownOption = (picked && String(picked.id) === valueKey ? picked : null) ??
|
|
127
|
+
options.find((o) => String(o.id) === valueKey) ??
|
|
128
|
+
(seedOption && String(seedOption.id) === valueKey ? seedOption : null) ??
|
|
122
129
|
null;
|
|
123
|
-
|
|
130
|
+
// A held value nobody has labelled yet: one `?ids=` lookup (no popover).
|
|
131
|
+
const needsLookup = !!valueKey && !knownOption && !useStatic;
|
|
132
|
+
const { resolved, loading: idsLoading } = useResolveOptionIds({
|
|
133
|
+
ref: source.ref,
|
|
134
|
+
endpoint: optionsEndpoint,
|
|
135
|
+
field: source.fieldKey,
|
|
136
|
+
ids: needsLookup ? [valueKey] : [],
|
|
137
|
+
enabled: needsLookup,
|
|
138
|
+
});
|
|
139
|
+
const resolution = needsLookup ? resolved.get(valueKey) : undefined;
|
|
140
|
+
const selectedOption = knownOption ?? (resolution?.status === 'found' ? resolution.option : null);
|
|
141
|
+
const valueResolving = needsLookup && !selectedOption && idsLoading;
|
|
142
|
+
const valueMissing = needsLookup && !selectedOption && resolution?.status === 'missing';
|
|
143
|
+
const selectedLabel = selectedOption?.label ?? (valueResolving ? '' : valueMissing ? DELETED_RECORD_LABEL : valueKey);
|
|
144
|
+
const resolvingNode = (_jsx("span", { className: "min-w-0 flex-1 truncate animate-pulse text-muted-foreground", "data-slot": "record-picker-value-loading", children: "Cargando\u2026" }));
|
|
145
|
+
const missingNode = (_jsx("span", { className: "min-w-0 flex-1 truncate italic text-muted-foreground", "data-slot": "record-picker-value-missing", children: DELETED_RECORD_LABEL }));
|
|
124
146
|
const handlePick = (opt) => {
|
|
125
147
|
setPicked(opt);
|
|
126
148
|
onChange(String(opt.id));
|
|
@@ -171,8 +193,13 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
171
193
|
if (!blockedByDependency)
|
|
172
194
|
setOpen(o);
|
|
173
195
|
}, disabled: blockedByDependency, readOnly: readonly, invalid: invalid, renderLead: (o, where) => _jsx(OptionLead, { option: o, size: where === 'option' ? 24 : 20 }), getDescription: descriptionAsBadge ? undefined : (o) => o.description, renderTrailing: descriptionAsBadge ? (o) => badge(o) : undefined, renderValue: () => {
|
|
196
|
+
// Never flash the raw id while the label resolves.
|
|
197
|
+
if (valueResolving)
|
|
198
|
+
return resolvingNode;
|
|
199
|
+
if (valueMissing)
|
|
200
|
+
return missingNode;
|
|
174
201
|
if (readonly) {
|
|
175
|
-
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 ??
|
|
202
|
+
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('—') })] }));
|
|
176
203
|
}
|
|
177
204
|
return (_jsxs(_Fragment, { children: [value && selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: blockedByDependency ? dependsHint || DEFAULT_DEPENDS_HINT : selectedLabel || ph('Buscar…') }), descriptionAsBadge && selectedOption ? badge(selectedOption) : null] }));
|
|
178
205
|
}, placeholder: ph('Buscar…'), searchPlaceholder: ph('Buscar…'), emptyText: debounced ? 'Sin resultados' : useStatic ? 'Sin opciones' : 'Sin resultados — escribe para filtrar', entityLabel: fieldName, onCreate: canMutate ? openCreate : undefined, onEdit: canMutate ? openEdit : undefined, triggerProps: {
|
package/dist/field-grid.d.ts
CHANGED
|
@@ -13,9 +13,10 @@ export declare function FieldLabel({ htmlFor, required, tone, children, }: {
|
|
|
13
13
|
htmlFor?: string;
|
|
14
14
|
required?: boolean;
|
|
15
15
|
/**
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
* uppercase labels
|
|
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;
|
package/dist/field-grid.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/field-grid.js
CHANGED
|
@@ -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 = '
|
|
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: "*" })] }));
|
package/dist/index.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export { registerAgentResultRenderer, resolveAgentResultRenderer, listAgentResul
|
|
|
104
104
|
export { isColumnVisibleInTable, isColumnVisibleInModal, isColumnVisibleInLineSubtable, AUDIT_SUBTABLE_COLUMN_KEYS, getSearchableColumnKeys, } from './column-visibility';
|
|
105
105
|
export { batchGet, loadQueryPart, nameBatchTokens, narrowInToken, optionsBatchToken, optionsModelFromUrl, rememberInRows, invalidateQueryBatchData, resetQueryBatchCache, splitInToken, tokenForGet, type InListToken, type NarrowedIn, type QueryPart, } from './query-batch';
|
|
106
106
|
export { useOptionsResolver, invalidateOptionsCache, projectOption, type ResolvedOption, type OptionsMeta, type UseOptionsResolverArgs, type UseOptionsResolverResult, } from './use-options-resolver';
|
|
107
|
+
export { useResolveOptionIds, OPTION_IDS_CHUNK, type OptionIdResolution, type UseResolveOptionIdsArgs, type UseResolveOptionIdsResult, } from './use-option-ids';
|
|
108
|
+
export { DELETED_RECORD_LABEL } from './dynamic-select-field';
|
|
107
109
|
export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, type OrgConfigBridge, } from './use-org-config-bridge';
|
|
108
110
|
export { registerValidator, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, getVisibleWhen, evaluateVisibleWhen, ATTRIBUTE_CLASSES_KEY, scopeValueFromFilterToken, buildListScopeValues, evaluateVisibleWhenForListScope, applyLineItemRowFormulas, computeLineItemTotals, toNumber, } from './dynamic-form-schema';
|
|
109
111
|
export { BranchCreateGateProvider, useBranchCreateGate, type BranchCreateGateApi, } from './branch-create-gate';
|