@asteby/metacore-runtime-react 46.2.2 → 46.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/action-modal-dispatcher.js +1 -1
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +16 -4
- package/dist/business/document-lines.d.ts +30 -0
- package/dist/business/document-lines.d.ts.map +1 -0
- package/dist/business/document-lines.js +65 -0
- 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 +28 -4
- package/dist/business/line-items-editor.d.ts.map +1 -1
- package/dist/business/line-items-editor.js +139 -14
- package/dist/business/line-items.d.ts +24 -2
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +50 -5
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/business/product-search.js +5 -1
- package/dist/dynamic-date-field.d.ts +3 -1
- package/dist/dynamic-date-field.d.ts.map +1 -1
- package/dist/dynamic-date-field.js +3 -2
- package/dist/dynamic-form-schema.d.ts +9 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +61 -0
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +18 -8
- package/dist/dynamic-select-field.d.ts +3 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +2 -1
- package/dist/use-options-resolver.d.ts +5 -5
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +11 -0
- package/package.json +1 -1
- package/src/__tests__/document-lines-grid.test.tsx +79 -0
- package/src/__tests__/line-item-formulas.test.ts +18 -1
- package/src/__tests__/use-options-resolver.test.ts +5 -0
- package/src/action-modal-dispatcher.tsx +1 -1
- package/src/business/business.test.ts +43 -0
- package/src/business/catalog.ts +16 -4
- package/src/business/document-lines.ts +91 -0
- package/src/business/index.ts +1 -0
- package/src/business/line-items-editor.tsx +365 -46
- package/src/business/line-items.ts +65 -7
- package/src/business/product-search.ts +9 -1
- package/src/dynamic-date-field.tsx +8 -2
- package/src/dynamic-form-schema.ts +60 -0
- package/src/dynamic-line-items.tsx +23 -3
- package/src/dynamic-select-field.tsx +4 -0
- package/src/use-options-resolver.ts +15 -0
|
@@ -16,10 +16,22 @@ export interface LineItem {
|
|
|
16
16
|
description: string
|
|
17
17
|
quantity: number
|
|
18
18
|
unit_price: number
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Descuento. Porcentaje 0-100 salvo `discount_kind: 'amount'`, donde es importe.
|
|
21
|
+
*/
|
|
20
22
|
discount: number
|
|
23
|
+
/** `percent` (default, se omite al serializar) o `amount`. */
|
|
24
|
+
discount_kind?: 'percent' | 'amount'
|
|
21
25
|
/** Tasa de impuesto como fracción (0.16). Viene de la config fiscal de la org. */
|
|
22
26
|
tax_rate: number
|
|
27
|
+
/** Unidad de medida (pza, juego…). Solo se serializa si viene informada. */
|
|
28
|
+
unit?: string
|
|
29
|
+
/** Precio o costo de catálogo, para mostrarlo junto al precio editado. */
|
|
30
|
+
catalog_price?: number
|
|
31
|
+
/** Costo conocido. Aviso si el precio de venta queda por debajo. No se serializa. */
|
|
32
|
+
cost?: number
|
|
33
|
+
/** Tope de cantidad (NC: facturado − acreditado; devolución: vendido − devuelto). */
|
|
34
|
+
max_quantity?: number
|
|
23
35
|
technician_id?: string
|
|
24
36
|
lot?: string
|
|
25
37
|
dot?: string
|
|
@@ -37,6 +49,8 @@ export interface LineItemsPolicy {
|
|
|
37
49
|
allowNegative?: boolean
|
|
38
50
|
/** Exige al menos un renglón `item`. Default true. */
|
|
39
51
|
requireItems?: boolean
|
|
52
|
+
/** Permite cantidad 0 (renglón de origen que no se acredita/devuelve). Default false. */
|
|
53
|
+
allowZeroQuantity?: boolean
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
let seq = 0
|
|
@@ -67,10 +81,16 @@ export interface LineAmounts {
|
|
|
67
81
|
total: number
|
|
68
82
|
}
|
|
69
83
|
|
|
70
|
-
export function computeLine(
|
|
84
|
+
export function computeLine(
|
|
85
|
+
l: Pick<LineItem, 'kind' | 'quantity' | 'unit_price' | 'discount' | 'tax_rate'> & { discount_kind?: LineItem['discount_kind'] },
|
|
86
|
+
): LineAmounts {
|
|
71
87
|
if (l.kind !== 'item') return { gross: 0, discount: 0, net: 0, tax: 0, total: 0 }
|
|
72
88
|
const gross = roundMoney(toAmount(l.quantity) * toAmount(l.unit_price))
|
|
73
|
-
const
|
|
89
|
+
const raw = toAmount(l.discount)
|
|
90
|
+
const discount =
|
|
91
|
+
l.discount_kind === 'amount'
|
|
92
|
+
? roundMoney(Math.min(Math.max(raw, 0), Math.max(gross, 0)))
|
|
93
|
+
: roundMoney(gross * (raw / 100))
|
|
74
94
|
const net = roundMoney(gross - discount)
|
|
75
95
|
const tax = roundMoney(net * toAmount(l.tax_rate))
|
|
76
96
|
return { gross, discount, net, tax, total: roundMoney(net + tax) }
|
|
@@ -102,6 +122,25 @@ export function computeTotals(lines: LineItem[]): LineItemsTotals {
|
|
|
102
122
|
return t
|
|
103
123
|
}
|
|
104
124
|
|
|
125
|
+
/** Impuesto agrupado por tasa, sumando el impuesto ya redondeado de cada renglón. */
|
|
126
|
+
export function taxBreakdown(lines: LineItem[]): Array<{ rate: number; base: number; tax: number }> {
|
|
127
|
+
const map = new Map<number, { base: number; tax: number }>()
|
|
128
|
+
for (const l of lines) {
|
|
129
|
+
if (l.kind !== 'item') continue
|
|
130
|
+
const a = computeLine(l)
|
|
131
|
+
const rate = toAmount(l.tax_rate)
|
|
132
|
+
const cur = map.get(rate) ?? { base: 0, tax: 0 }
|
|
133
|
+
cur.base += a.net
|
|
134
|
+
cur.tax += a.tax
|
|
135
|
+
map.set(rate, cur)
|
|
136
|
+
}
|
|
137
|
+
return [...map.entries()].map(([rate, v]) => ({
|
|
138
|
+
rate,
|
|
139
|
+
base: roundMoney(v.base),
|
|
140
|
+
tax: roundMoney(v.tax),
|
|
141
|
+
}))
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
export interface LineItemsValidation {
|
|
106
145
|
/** Errores por celda `"<índice>.<campo>"` (mismo formato que DynamicLineItems). */
|
|
107
146
|
errors: Record<string, string>
|
|
@@ -113,7 +152,7 @@ export interface LineItemsValidation {
|
|
|
113
152
|
}
|
|
114
153
|
|
|
115
154
|
export function validateLineItems(lines: LineItem[], policy: LineItemsPolicy = {}): LineItemsValidation {
|
|
116
|
-
const { stockPolicy = 'warn', allowNegative = false, requireItems = true } = policy
|
|
155
|
+
const { stockPolicy = 'warn', allowNegative = false, requireItems = true, allowZeroQuantity = false } = policy
|
|
117
156
|
const errors: Record<string, string> = {}
|
|
118
157
|
const warnings: Record<string, string> = {}
|
|
119
158
|
let items = 0
|
|
@@ -127,10 +166,22 @@ export function validateLineItems(lines: LineItem[], policy: LineItemsPolicy = {
|
|
|
127
166
|
const price = toAmount(l.unit_price)
|
|
128
167
|
const disc = toAmount(l.discount)
|
|
129
168
|
if (!l.description.trim() && !l.product_id) errors[`${i}.description`] = 'Elige un producto o escribe una descripción'
|
|
130
|
-
if (qty === 0) errors[`${i}.quantity`] = 'La cantidad debe ser distinta de cero'
|
|
169
|
+
if (qty === 0 && !allowZeroQuantity) errors[`${i}.quantity`] = 'La cantidad debe ser distinta de cero'
|
|
131
170
|
else if (qty < 0 && !allowNegative) errors[`${i}.quantity`] = 'La cantidad no puede ser negativa'
|
|
171
|
+
if (l.max_quantity != null && qty > toAmount(l.max_quantity)) {
|
|
172
|
+
errors[`${i}.quantity`] = `El máximo es ${l.max_quantity}`
|
|
173
|
+
}
|
|
132
174
|
if (price < 0 && !allowNegative) errors[`${i}.unit_price`] = 'El precio no puede ser negativo'
|
|
133
|
-
if (
|
|
175
|
+
if (l.cost != null && price < toAmount(l.cost)) {
|
|
176
|
+
warnings[`${i}.unit_price`] = 'El precio está por debajo del costo'
|
|
177
|
+
}
|
|
178
|
+
if (l.discount_kind === 'amount') {
|
|
179
|
+
const gross = roundMoney(qty * price)
|
|
180
|
+
if (disc < 0) errors[`${i}.discount`] = 'El descuento no puede ser negativo'
|
|
181
|
+
else if (disc > gross) errors[`${i}.discount`] = 'El descuento no puede superar el importe'
|
|
182
|
+
} else if (disc < 0 || disc > 100) {
|
|
183
|
+
errors[`${i}.discount`] = 'El descuento debe estar entre 0 y 100%'
|
|
184
|
+
}
|
|
134
185
|
if (l.available != null && qty > l.available && stockPolicy !== 'allow') {
|
|
135
186
|
const msg = `Solo hay ${l.available} disponibles`
|
|
136
187
|
if (stockPolicy === 'block') errors[`${i}.quantity`] ??= msg
|
|
@@ -157,7 +208,9 @@ export function serializeLineItems(lines: LineItem[]): Array<Record<string, unkn
|
|
|
157
208
|
out.unit_price = toAmount(l.unit_price)
|
|
158
209
|
out.discount = toAmount(l.discount)
|
|
159
210
|
out.tax_rate = toAmount(l.tax_rate)
|
|
160
|
-
|
|
211
|
+
if (l.discount_kind === 'amount') out.discount_kind = 'amount'
|
|
212
|
+
if (l.catalog_price != null) out.catalog_price = toAmount(l.catalog_price)
|
|
213
|
+
for (const k of ['product_id', 'sku', 'technician_id', 'lot', 'dot', 'unit'] as const) {
|
|
161
214
|
const v = l[k]
|
|
162
215
|
if (typeof v === 'string' && v.trim()) out[k] = v.trim()
|
|
163
216
|
}
|
|
@@ -181,7 +234,12 @@ export function parseLineItems(rows: unknown): LineItem[] {
|
|
|
181
234
|
quantity: o.quantity == null ? 1 : toAmount(o.quantity),
|
|
182
235
|
unit_price: toAmount(o.unit_price),
|
|
183
236
|
discount: toAmount(o.discount),
|
|
237
|
+
discount_kind: o.discount_kind === 'amount' ? 'amount' : undefined,
|
|
184
238
|
tax_rate: toAmount(o.tax_rate),
|
|
239
|
+
unit: str(o.unit),
|
|
240
|
+
catalog_price: o.catalog_price == null || o.catalog_price === '' ? undefined : toAmount(o.catalog_price),
|
|
241
|
+
cost: o.cost == null || o.cost === '' ? undefined : toAmount(o.cost),
|
|
242
|
+
max_quantity: o.max_quantity == null || o.max_quantity === '' ? undefined : toAmount(o.max_quantity),
|
|
185
243
|
technician_id: str(o.technician_id),
|
|
186
244
|
lot: str(o.lot),
|
|
187
245
|
dot: str(o.dot),
|
|
@@ -58,6 +58,8 @@ export interface ProductVariant {
|
|
|
58
58
|
sku?: string
|
|
59
59
|
barcode?: string
|
|
60
60
|
price?: number
|
|
61
|
+
/** Costo de catálogo (compras). Opcional; el precio de lista sigue en `price`. */
|
|
62
|
+
cost?: number
|
|
61
63
|
stock?: StockByWarehouse[]
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -70,6 +72,8 @@ export interface ProductResult {
|
|
|
70
72
|
supplier_sku?: string
|
|
71
73
|
kind?: 'product' | 'tire' | 'service' | 'bundle'
|
|
72
74
|
price?: number
|
|
75
|
+
/** Costo de catálogo (compras). */
|
|
76
|
+
cost?: number
|
|
73
77
|
tax_rate?: number
|
|
74
78
|
tire?: TireSize
|
|
75
79
|
variants?: ProductVariant[]
|
|
@@ -96,12 +100,16 @@ export function productToLine(
|
|
|
96
100
|
opts: { warehouseId?: string; quantity?: number } = {},
|
|
97
101
|
): LineItem {
|
|
98
102
|
const src = variant ?? p
|
|
103
|
+
const list = variant?.price ?? p.price
|
|
104
|
+
const cost = variant?.cost ?? p.cost
|
|
99
105
|
return makeLine({
|
|
100
106
|
product_id: variant?.id ?? p.id,
|
|
101
107
|
sku: variant?.sku ?? p.sku,
|
|
102
108
|
description: variant ? `${p.name} · ${variant.label}` : p.name,
|
|
103
109
|
quantity: opts.quantity ?? 1,
|
|
104
|
-
unit_price:
|
|
110
|
+
unit_price: list ?? 0,
|
|
111
|
+
catalog_price: list,
|
|
112
|
+
cost,
|
|
105
113
|
tax_rate: p.tax_rate ?? 0,
|
|
106
114
|
available: availableStock(src, opts.warehouseId) ?? availableStock(p, opts.warehouseId),
|
|
107
115
|
})
|
|
@@ -27,6 +27,8 @@ export interface DynamicDateFieldProps {
|
|
|
27
27
|
field: ActionFieldDef
|
|
28
28
|
value: any
|
|
29
29
|
onChange: (v: any) => void
|
|
30
|
+
/** Server or client validation failed for this field (PIT-046). */
|
|
31
|
+
invalid?: boolean
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const toDate = parseCalendarDate
|
|
@@ -49,7 +51,7 @@ function label(d: Date | undefined): string {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
export function DynamicDateField({ field, value, onChange }: DynamicDateFieldProps) {
|
|
54
|
+
export function DynamicDateField({ field, value, onChange, invalid }: DynamicDateFieldProps) {
|
|
53
55
|
const [open, setOpen] = useState(false)
|
|
54
56
|
const selected = toDate(value)
|
|
55
57
|
|
|
@@ -61,7 +63,11 @@ export function DynamicDateField({ field, value, onChange }: DynamicDateFieldPro
|
|
|
61
63
|
variant="outline"
|
|
62
64
|
id={field.key}
|
|
63
65
|
data-empty={!selected}
|
|
64
|
-
|
|
66
|
+
aria-invalid={invalid || undefined}
|
|
67
|
+
className={
|
|
68
|
+
'w-full justify-start text-start font-normal data-[empty=true]:text-muted-foreground' +
|
|
69
|
+
(invalid ? ' border-destructive ring-1 ring-destructive/30' : '')
|
|
70
|
+
}
|
|
65
71
|
>
|
|
66
72
|
<CalendarIcon className="mr-2 size-4 shrink-0 opacity-50" />
|
|
67
73
|
<span className="truncate">
|
|
@@ -177,6 +177,66 @@ export function applyLineItemRowFormulas(
|
|
|
177
177
|
return out
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
const AUTOFILL_PRICE_KEYS = new Set(['unit_price', 'price', 'sale_price'])
|
|
181
|
+
const AUTOFILL_COST_KEYS = new Set(['cost', 'unit_cost', 'catalog_cost', 'catalog_price'])
|
|
182
|
+
|
|
183
|
+
function metaNumber(meta: Record<string, unknown>, keys: string[]): number | undefined {
|
|
184
|
+
for (const k of keys) {
|
|
185
|
+
const v = meta[k]
|
|
186
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v
|
|
187
|
+
if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v))) return Number(v)
|
|
188
|
+
}
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Al elegir un producto en una grilla declarativa, copia precio/costo/SKU/tasa
|
|
194
|
+
* que vengan en `option.meta` a las columnas hermanas y recalcula el importe.
|
|
195
|
+
* No-op si el meta no trae datos útiles (el journal entry no se toca).
|
|
196
|
+
*/
|
|
197
|
+
export function autofillLineFromOption(
|
|
198
|
+
itemFields: ActionFieldDef[],
|
|
199
|
+
row: Record<string, any>,
|
|
200
|
+
option: { label?: string; meta?: Record<string, unknown> },
|
|
201
|
+
): Record<string, any> {
|
|
202
|
+
const meta = option.meta
|
|
203
|
+
if (!meta || Object.keys(meta).length === 0) return row
|
|
204
|
+
const next: Record<string, any> = { ...row }
|
|
205
|
+
let changed = false
|
|
206
|
+
for (const f of itemFields) {
|
|
207
|
+
if (AUTOFILL_PRICE_KEYS.has(f.key)) {
|
|
208
|
+
const v = metaNumber(meta, ['unit_price', 'price', 'sale_price'])
|
|
209
|
+
if (v != null) {
|
|
210
|
+
next[f.key] = v
|
|
211
|
+
changed = true
|
|
212
|
+
}
|
|
213
|
+
} else if (AUTOFILL_COST_KEYS.has(f.key)) {
|
|
214
|
+
const v = metaNumber(meta, ['cost', 'unit_cost', 'catalog_price', 'catalog_cost'])
|
|
215
|
+
if (v != null) {
|
|
216
|
+
next[f.key] = v
|
|
217
|
+
changed = true
|
|
218
|
+
}
|
|
219
|
+
} else if (f.key === 'tax_rate' || f.key === 'tax') {
|
|
220
|
+
const v = metaNumber(meta, ['tax_rate', 'tax'])
|
|
221
|
+
if (v != null) {
|
|
222
|
+
next[f.key] = v
|
|
223
|
+
changed = true
|
|
224
|
+
}
|
|
225
|
+
} else if (f.key === 'sku' || f.key === 'code') {
|
|
226
|
+
const v = meta.sku ?? meta.code
|
|
227
|
+
if (typeof v === 'string' && v.trim()) {
|
|
228
|
+
next[f.key] = v.trim()
|
|
229
|
+
changed = true
|
|
230
|
+
}
|
|
231
|
+
} else if ((f.key === 'description' || f.key === 'name') && !next[f.key] && option.label) {
|
|
232
|
+
next[f.key] = option.label
|
|
233
|
+
changed = true
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!changed) return row
|
|
237
|
+
return applyLineItemRowFormulas(itemFields, next)
|
|
238
|
+
}
|
|
239
|
+
|
|
180
240
|
export interface BalanceState {
|
|
181
241
|
debit: number
|
|
182
242
|
credit: number
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getItemFields,
|
|
27
27
|
computeLineItemTotals,
|
|
28
28
|
applyLineItemRowFormulas,
|
|
29
|
+
autofillLineFromOption,
|
|
29
30
|
evaluateBalance,
|
|
30
31
|
toNumber,
|
|
31
32
|
getDependsOn,
|
|
@@ -139,6 +140,16 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
139
140
|
updateCell(idx, key, cellValue)
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
const handlePick = (idx: number, key: string, opt: ResolvedOption) => {
|
|
144
|
+
onChange(
|
|
145
|
+
rows.map((r, i) =>
|
|
146
|
+
i === idx
|
|
147
|
+
? autofillLineFromOption(itemFields, { ...r, [key]: String(opt.id) }, opt)
|
|
148
|
+
: r,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
const totalCols = itemFields.filter((c) => c.total)
|
|
143
154
|
|
|
144
155
|
return (
|
|
@@ -189,6 +200,7 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
189
200
|
field={col}
|
|
190
201
|
value={row?.[col.key]}
|
|
191
202
|
onChange={(v: any) => handleCell(idx, col.key, v)}
|
|
203
|
+
onPick={(opt) => handlePick(idx, col.key, opt)}
|
|
192
204
|
disabled={disabled}
|
|
193
205
|
formValues={formValues}
|
|
194
206
|
rowValues={row}
|
|
@@ -260,6 +272,7 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
260
272
|
field={col}
|
|
261
273
|
value={row?.[col.key]}
|
|
262
274
|
onChange={(v: any) => handleCell(idx, col.key, v)}
|
|
275
|
+
onPick={(opt) => handlePick(idx, col.key, opt)}
|
|
263
276
|
disabled={disabled}
|
|
264
277
|
formValues={formValues}
|
|
265
278
|
rowValues={row}
|
|
@@ -377,6 +390,7 @@ interface CellRendererProps {
|
|
|
377
390
|
field: ActionFieldDef
|
|
378
391
|
value: any
|
|
379
392
|
onChange: (v: any) => void
|
|
393
|
+
onPick?: (opt: ResolvedOption) => void
|
|
380
394
|
disabled?: boolean
|
|
381
395
|
/** Header form values — for resolving a cell's `dependsOn` to a header field. */
|
|
382
396
|
formValues?: Record<string, any>
|
|
@@ -390,7 +404,7 @@ interface CellRendererProps {
|
|
|
390
404
|
// without the per-field Label (the column header is the label) and sized for a
|
|
391
405
|
// table cell. Nested line-items inside a row are not supported (a row column is
|
|
392
406
|
// a scalar widget).
|
|
393
|
-
function CellRenderer({ field, value, onChange, disabled, formValues, rowValues, invalid }: CellRendererProps) {
|
|
407
|
+
function CellRenderer({ field, value, onChange, onPick, disabled, formValues, rowValues, invalid }: CellRendererProps) {
|
|
394
408
|
const widget = resolveWidget(field)
|
|
395
409
|
const invalidCls = invalid
|
|
396
410
|
? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
|
|
@@ -434,6 +448,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
|
|
|
434
448
|
field={field}
|
|
435
449
|
value={value}
|
|
436
450
|
onChange={onChange}
|
|
451
|
+
onPick={onPick}
|
|
437
452
|
dependsValue={dependsValue}
|
|
438
453
|
readonly={ro}
|
|
439
454
|
invalid={invalid}
|
|
@@ -446,6 +461,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
|
|
|
446
461
|
field={field}
|
|
447
462
|
value={value}
|
|
448
463
|
onChange={onChange}
|
|
464
|
+
onPick={onPick}
|
|
449
465
|
disabled={disabled}
|
|
450
466
|
formValues={formValues}
|
|
451
467
|
rowValues={rowValues}
|
|
@@ -541,7 +557,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
|
|
|
541
557
|
}
|
|
542
558
|
}
|
|
543
559
|
|
|
544
|
-
function RefCell({ field, value, onChange, disabled, formValues, rowValues }: CellRendererProps) {
|
|
560
|
+
function RefCell({ field, value, onChange, onPick, disabled, formValues, rowValues }: CellRendererProps) {
|
|
545
561
|
// Cascade: resolve the value of the field this cell `dependsOn` from the
|
|
546
562
|
// row (sibling) first, then the header form. While empty, the picker is
|
|
547
563
|
// disabled with a hint instead of listing the whole (unscoped) table.
|
|
@@ -581,7 +597,11 @@ function RefCell({ field, value, onChange, disabled, formValues, rowValues }: Ce
|
|
|
581
597
|
return (
|
|
582
598
|
<Select
|
|
583
599
|
value={value || ''}
|
|
584
|
-
onValueChange={
|
|
600
|
+
onValueChange={(v) => {
|
|
601
|
+
onChange(v)
|
|
602
|
+
const opt = options.find((o) => String(o.id) === String(v))
|
|
603
|
+
if (opt) onPick?.(opt)
|
|
604
|
+
}}
|
|
585
605
|
disabled={disabled || loading || blockedByDependency}
|
|
586
606
|
>
|
|
587
607
|
<SelectTrigger className="w-full" data-depends-blocked={blockedByDependency ? '' : undefined}>
|
|
@@ -149,6 +149,8 @@ export interface DynamicSelectFieldProps {
|
|
|
149
149
|
field: ActionFieldDef
|
|
150
150
|
value: any
|
|
151
151
|
onChange: (v: any) => void
|
|
152
|
+
/** Opción completa recién elegida (incluye `meta` para autollenar columnas hermanas). */
|
|
153
|
+
onPick?: (opt: ResolvedOption) => void
|
|
152
154
|
/**
|
|
153
155
|
* Pre-resolved option for the CURRENT value (label + image/color/icon) the
|
|
154
156
|
* caller already has — e.g. the relation sibling the table served. Lets the
|
|
@@ -212,6 +214,7 @@ export function DynamicSelectField({
|
|
|
212
214
|
field,
|
|
213
215
|
value,
|
|
214
216
|
onChange,
|
|
217
|
+
onPick,
|
|
215
218
|
seedOption,
|
|
216
219
|
dependsValue,
|
|
217
220
|
dependsHint,
|
|
@@ -327,6 +330,7 @@ export function DynamicSelectField({
|
|
|
327
330
|
const handlePick = (opt: ResolvedOption) => {
|
|
328
331
|
setPicked(opt)
|
|
329
332
|
onChange(String(opt.id))
|
|
333
|
+
onPick?.(opt)
|
|
330
334
|
setOpen(false)
|
|
331
335
|
setSearch('')
|
|
332
336
|
}
|
|
@@ -27,6 +27,11 @@ export interface ResolvedOption {
|
|
|
27
27
|
image?: string | null
|
|
28
28
|
color?: string | null
|
|
29
29
|
icon?: string | null
|
|
30
|
+
/**
|
|
31
|
+
* Campos extra del payload (precio, costo, sku, tasa) que no son decoración.
|
|
32
|
+
* Solo se llena cuando el option trae números o strings además de id/label.
|
|
33
|
+
*/
|
|
34
|
+
meta?: Record<string, unknown>
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export interface OptionsMeta {
|
|
@@ -331,9 +336,18 @@ export function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsReso
|
|
|
331
336
|
* id/value and label/name fields for legacy parity — we accept either
|
|
332
337
|
* and surface a stable shape downstream.
|
|
333
338
|
*/
|
|
339
|
+
const OPTION_KNOWN_KEYS = new Set(['id', 'value', 'label', 'name', 'description', 'image', 'color', 'icon'])
|
|
340
|
+
|
|
334
341
|
export function projectOption(raw: any): ResolvedOption {
|
|
335
342
|
const id = raw?.id ?? raw?.value ?? ''
|
|
336
343
|
const label = String(raw?.label ?? raw?.name ?? id ?? '')
|
|
344
|
+
const meta: Record<string, unknown> = {}
|
|
345
|
+
if (raw && typeof raw === 'object') {
|
|
346
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
347
|
+
if (OPTION_KNOWN_KEYS.has(k)) continue
|
|
348
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') meta[k] = v
|
|
349
|
+
}
|
|
350
|
+
}
|
|
337
351
|
return {
|
|
338
352
|
id,
|
|
339
353
|
value: raw?.value ?? id,
|
|
@@ -343,5 +357,6 @@ export function projectOption(raw: any): ResolvedOption {
|
|
|
343
357
|
image: raw?.image ?? null,
|
|
344
358
|
color: raw?.color ?? null,
|
|
345
359
|
icon: raw?.icon ?? null,
|
|
360
|
+
...(Object.keys(meta).length > 0 ? { meta } : {}),
|
|
346
361
|
}
|
|
347
362
|
}
|