@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
|
@@ -25,7 +25,10 @@ export function computeLine(l) {
|
|
|
25
25
|
if (l.kind !== 'item')
|
|
26
26
|
return { gross: 0, discount: 0, net: 0, tax: 0, total: 0 };
|
|
27
27
|
const gross = roundMoney(toAmount(l.quantity) * toAmount(l.unit_price));
|
|
28
|
-
const
|
|
28
|
+
const raw = toAmount(l.discount);
|
|
29
|
+
const discount = l.discount_kind === 'amount'
|
|
30
|
+
? roundMoney(Math.min(Math.max(raw, 0), Math.max(gross, 0)))
|
|
31
|
+
: roundMoney(gross * (raw / 100));
|
|
29
32
|
const net = roundMoney(gross - discount);
|
|
30
33
|
const tax = roundMoney(net * toAmount(l.tax_rate));
|
|
31
34
|
return { gross, discount, net, tax, total: roundMoney(net + tax) };
|
|
@@ -47,8 +50,27 @@ export function computeTotals(lines) {
|
|
|
47
50
|
t.total = roundMoney(t.subtotal - t.discount + t.tax);
|
|
48
51
|
return t;
|
|
49
52
|
}
|
|
53
|
+
/** Impuesto agrupado por tasa, sumando el impuesto ya redondeado de cada renglón. */
|
|
54
|
+
export function taxBreakdown(lines) {
|
|
55
|
+
const map = new Map();
|
|
56
|
+
for (const l of lines) {
|
|
57
|
+
if (l.kind !== 'item')
|
|
58
|
+
continue;
|
|
59
|
+
const a = computeLine(l);
|
|
60
|
+
const rate = toAmount(l.tax_rate);
|
|
61
|
+
const cur = map.get(rate) ?? { base: 0, tax: 0 };
|
|
62
|
+
cur.base += a.net;
|
|
63
|
+
cur.tax += a.tax;
|
|
64
|
+
map.set(rate, cur);
|
|
65
|
+
}
|
|
66
|
+
return [...map.entries()].map(([rate, v]) => ({
|
|
67
|
+
rate,
|
|
68
|
+
base: roundMoney(v.base),
|
|
69
|
+
tax: roundMoney(v.tax),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
50
72
|
export function validateLineItems(lines, policy = {}) {
|
|
51
|
-
const { stockPolicy = 'warn', allowNegative = false, requireItems = true } = policy;
|
|
73
|
+
const { stockPolicy = 'warn', allowNegative = false, requireItems = true, allowZeroQuantity = false } = policy;
|
|
52
74
|
const errors = {};
|
|
53
75
|
const warnings = {};
|
|
54
76
|
let items = 0;
|
|
@@ -64,14 +86,28 @@ export function validateLineItems(lines, policy = {}) {
|
|
|
64
86
|
const disc = toAmount(l.discount);
|
|
65
87
|
if (!l.description.trim() && !l.product_id)
|
|
66
88
|
errors[`${i}.description`] = 'Elige un producto o escribe una descripción';
|
|
67
|
-
if (qty === 0)
|
|
89
|
+
if (qty === 0 && !allowZeroQuantity)
|
|
68
90
|
errors[`${i}.quantity`] = 'La cantidad debe ser distinta de cero';
|
|
69
91
|
else if (qty < 0 && !allowNegative)
|
|
70
92
|
errors[`${i}.quantity`] = 'La cantidad no puede ser negativa';
|
|
93
|
+
if (l.max_quantity != null && qty > toAmount(l.max_quantity)) {
|
|
94
|
+
errors[`${i}.quantity`] = `El máximo es ${l.max_quantity}`;
|
|
95
|
+
}
|
|
71
96
|
if (price < 0 && !allowNegative)
|
|
72
97
|
errors[`${i}.unit_price`] = 'El precio no puede ser negativo';
|
|
73
|
-
if (
|
|
98
|
+
if (l.cost != null && price < toAmount(l.cost)) {
|
|
99
|
+
warnings[`${i}.unit_price`] = 'El precio está por debajo del costo';
|
|
100
|
+
}
|
|
101
|
+
if (l.discount_kind === 'amount') {
|
|
102
|
+
const gross = roundMoney(qty * price);
|
|
103
|
+
if (disc < 0)
|
|
104
|
+
errors[`${i}.discount`] = 'El descuento no puede ser negativo';
|
|
105
|
+
else if (disc > gross)
|
|
106
|
+
errors[`${i}.discount`] = 'El descuento no puede superar el importe';
|
|
107
|
+
}
|
|
108
|
+
else if (disc < 0 || disc > 100) {
|
|
74
109
|
errors[`${i}.discount`] = 'El descuento debe estar entre 0 y 100%';
|
|
110
|
+
}
|
|
75
111
|
if (l.available != null && qty > l.available && stockPolicy !== 'allow') {
|
|
76
112
|
const msg = `Solo hay ${l.available} disponibles`;
|
|
77
113
|
if (stockPolicy === 'block')
|
|
@@ -99,7 +135,11 @@ export function serializeLineItems(lines) {
|
|
|
99
135
|
out.unit_price = toAmount(l.unit_price);
|
|
100
136
|
out.discount = toAmount(l.discount);
|
|
101
137
|
out.tax_rate = toAmount(l.tax_rate);
|
|
102
|
-
|
|
138
|
+
if (l.discount_kind === 'amount')
|
|
139
|
+
out.discount_kind = 'amount';
|
|
140
|
+
if (l.catalog_price != null)
|
|
141
|
+
out.catalog_price = toAmount(l.catalog_price);
|
|
142
|
+
for (const k of ['product_id', 'sku', 'technician_id', 'lot', 'dot', 'unit']) {
|
|
103
143
|
const v = l[k];
|
|
104
144
|
if (typeof v === 'string' && v.trim())
|
|
105
145
|
out[k] = v.trim();
|
|
@@ -124,7 +164,12 @@ export function parseLineItems(rows) {
|
|
|
124
164
|
quantity: o.quantity == null ? 1 : toAmount(o.quantity),
|
|
125
165
|
unit_price: toAmount(o.unit_price),
|
|
126
166
|
discount: toAmount(o.discount),
|
|
167
|
+
discount_kind: o.discount_kind === 'amount' ? 'amount' : undefined,
|
|
127
168
|
tax_rate: toAmount(o.tax_rate),
|
|
169
|
+
unit: str(o.unit),
|
|
170
|
+
catalog_price: o.catalog_price == null || o.catalog_price === '' ? undefined : toAmount(o.catalog_price),
|
|
171
|
+
cost: o.cost == null || o.cost === '' ? undefined : toAmount(o.cost),
|
|
172
|
+
max_quantity: o.max_quantity == null || o.max_quantity === '' ? undefined : toAmount(o.max_quantity),
|
|
128
173
|
technician_id: str(o.technician_id),
|
|
129
174
|
lot: str(o.lot),
|
|
130
175
|
dot: str(o.dot),
|
|
@@ -36,6 +36,8 @@ export interface ProductVariant {
|
|
|
36
36
|
sku?: string;
|
|
37
37
|
barcode?: string;
|
|
38
38
|
price?: number;
|
|
39
|
+
/** Costo de catálogo (compras). Opcional; el precio de lista sigue en `price`. */
|
|
40
|
+
cost?: number;
|
|
39
41
|
stock?: StockByWarehouse[];
|
|
40
42
|
}
|
|
41
43
|
export interface ProductResult {
|
|
@@ -47,6 +49,8 @@ export interface ProductResult {
|
|
|
47
49
|
supplier_sku?: string;
|
|
48
50
|
kind?: 'product' | 'tire' | 'service' | 'bundle';
|
|
49
51
|
price?: number;
|
|
52
|
+
/** Costo de catálogo (compras). */
|
|
53
|
+
cost?: number;
|
|
50
54
|
tax_rate?: number;
|
|
51
55
|
tire?: TireSize;
|
|
52
56
|
variants?: ProductVariant[];
|
|
@@ -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;AAG5C,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAMjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAW5D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAO3D;AAED,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAC1B,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAAE,EACtC,WAAW,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAKpB;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CACzB,CAAC,EAAE,aAAa,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,QAAQ,
|
|
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;AAG5C,MAAM,WAAW,QAAQ;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAMjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAW5D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAO3D;AAED,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAC1B,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAAE,EACtC,WAAW,CAAC,EAAE,MAAM,GACrB,MAAM,GAAG,SAAS,CAKpB;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CACzB,CAAC,EAAE,aAAa,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,QAAQ,CAeV"}
|
|
@@ -39,12 +39,16 @@ export function availableStock(source, warehouseId) {
|
|
|
39
39
|
/** Producto (+ variante) → renglón listo para <LineItemsEditor>. */
|
|
40
40
|
export function productToLine(p, variant, opts = {}) {
|
|
41
41
|
const src = variant ?? p;
|
|
42
|
+
const list = variant?.price ?? p.price;
|
|
43
|
+
const cost = variant?.cost ?? p.cost;
|
|
42
44
|
return makeLine({
|
|
43
45
|
product_id: variant?.id ?? p.id,
|
|
44
46
|
sku: variant?.sku ?? p.sku,
|
|
45
47
|
description: variant ? `${p.name} · ${variant.label}` : p.name,
|
|
46
48
|
quantity: opts.quantity ?? 1,
|
|
47
|
-
unit_price:
|
|
49
|
+
unit_price: list ?? 0,
|
|
50
|
+
catalog_price: list,
|
|
51
|
+
cost,
|
|
48
52
|
tax_rate: p.tax_rate ?? 0,
|
|
49
53
|
available: availableStock(src, opts.warehouseId) ?? availableStock(p, opts.warehouseId),
|
|
50
54
|
});
|
|
@@ -3,7 +3,9 @@ export interface DynamicDateFieldProps {
|
|
|
3
3
|
field: ActionFieldDef;
|
|
4
4
|
value: any;
|
|
5
5
|
onChange: (v: any) => void;
|
|
6
|
+
/** Server or client validation failed for this field (PIT-046). */
|
|
7
|
+
invalid?: boolean;
|
|
6
8
|
}
|
|
7
|
-
export declare function DynamicDateField({ field, value, onChange }: DynamicDateFieldProps): import("react").JSX.Element;
|
|
9
|
+
export declare function DynamicDateField({ field, value, onChange, invalid }: DynamicDateFieldProps): import("react").JSX.Element;
|
|
8
10
|
export default DynamicDateField;
|
|
9
11
|
//# sourceMappingURL=dynamic-date-field.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-date-field.d.ts","sourceRoot":"","sources":["../src/dynamic-date-field.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-date-field.d.ts","sourceRoot":"","sources":["../src/dynamic-date-field.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAsBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,qBAAqB,+BAsC1F;AAED,eAAe,gBAAgB,CAAA"}
|
|
@@ -35,10 +35,11 @@ function label(d) {
|
|
|
35
35
|
return toISO(d);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
export function DynamicDateField({ field, value, onChange }) {
|
|
38
|
+
export function DynamicDateField({ field, value, onChange, invalid }) {
|
|
39
39
|
const [open, setOpen] = useState(false);
|
|
40
40
|
const selected = toDate(value);
|
|
41
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", id: field.key, "data-empty": !selected, className:
|
|
41
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", id: field.key, "data-empty": !selected, "aria-invalid": invalid || undefined, className: 'w-full justify-start text-start font-normal data-[empty=true]:text-muted-foreground' +
|
|
42
|
+
(invalid ? ' border-destructive ring-1 ring-destructive/30' : ''), children: [_jsx(CalendarIcon, { className: "mr-2 size-4 shrink-0 opacity-50" }), _jsx("span", { className: "truncate", children: selected ? label(selected) : (field.placeholder || 'Elegí una fecha') })] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", captionLayout: "dropdown", selected: selected, defaultMonth: selected, onSelect: (d) => {
|
|
42
43
|
onChange(d ? toISO(d) : '');
|
|
43
44
|
setOpen(false);
|
|
44
45
|
} }) })] }));
|
|
@@ -47,6 +47,15 @@ export declare function computeLineItemTotals(field: ActionFieldDef, rows: any[]
|
|
|
47
47
|
* No-op when the row has no amount column or no qty/price pair.
|
|
48
48
|
*/
|
|
49
49
|
export declare function applyLineItemRowFormulas(itemFields: ActionFieldDef[], row: Record<string, any>): Record<string, any>;
|
|
50
|
+
/**
|
|
51
|
+
* Al elegir un producto en una grilla declarativa, copia precio/costo/SKU/tasa
|
|
52
|
+
* que vengan en `option.meta` a las columnas hermanas y recalcula el importe.
|
|
53
|
+
* No-op si el meta no trae datos útiles (el journal entry no se toca).
|
|
54
|
+
*/
|
|
55
|
+
export declare function autofillLineFromOption(itemFields: ActionFieldDef[], row: Record<string, any>, option: {
|
|
56
|
+
label?: string;
|
|
57
|
+
meta?: Record<string, unknown>;
|
|
58
|
+
}): Record<string, any>;
|
|
50
59
|
export interface BalanceState {
|
|
51
60
|
debit: number;
|
|
52
61
|
credit: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -162,6 +162,67 @@ export function applyLineItemRowFormulas(itemFields, row) {
|
|
|
162
162
|
out[discountKey] = 0;
|
|
163
163
|
return out;
|
|
164
164
|
}
|
|
165
|
+
const AUTOFILL_PRICE_KEYS = new Set(['unit_price', 'price', 'sale_price']);
|
|
166
|
+
const AUTOFILL_COST_KEYS = new Set(['cost', 'unit_cost', 'catalog_cost', 'catalog_price']);
|
|
167
|
+
function metaNumber(meta, keys) {
|
|
168
|
+
for (const k of keys) {
|
|
169
|
+
const v = meta[k];
|
|
170
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
171
|
+
return v;
|
|
172
|
+
if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
|
|
173
|
+
return Number(v);
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Al elegir un producto en una grilla declarativa, copia precio/costo/SKU/tasa
|
|
179
|
+
* que vengan en `option.meta` a las columnas hermanas y recalcula el importe.
|
|
180
|
+
* No-op si el meta no trae datos útiles (el journal entry no se toca).
|
|
181
|
+
*/
|
|
182
|
+
export function autofillLineFromOption(itemFields, row, option) {
|
|
183
|
+
const meta = option.meta;
|
|
184
|
+
if (!meta || Object.keys(meta).length === 0)
|
|
185
|
+
return row;
|
|
186
|
+
const next = { ...row };
|
|
187
|
+
let changed = false;
|
|
188
|
+
for (const f of itemFields) {
|
|
189
|
+
if (AUTOFILL_PRICE_KEYS.has(f.key)) {
|
|
190
|
+
const v = metaNumber(meta, ['unit_price', 'price', 'sale_price']);
|
|
191
|
+
if (v != null) {
|
|
192
|
+
next[f.key] = v;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (AUTOFILL_COST_KEYS.has(f.key)) {
|
|
197
|
+
const v = metaNumber(meta, ['cost', 'unit_cost', 'catalog_price', 'catalog_cost']);
|
|
198
|
+
if (v != null) {
|
|
199
|
+
next[f.key] = v;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (f.key === 'tax_rate' || f.key === 'tax') {
|
|
204
|
+
const v = metaNumber(meta, ['tax_rate', 'tax']);
|
|
205
|
+
if (v != null) {
|
|
206
|
+
next[f.key] = v;
|
|
207
|
+
changed = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (f.key === 'sku' || f.key === 'code') {
|
|
211
|
+
const v = meta.sku ?? meta.code;
|
|
212
|
+
if (typeof v === 'string' && v.trim()) {
|
|
213
|
+
next[f.key] = v.trim();
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if ((f.key === 'description' || f.key === 'name') && !next[f.key] && option.label) {
|
|
218
|
+
next[f.key] = option.label;
|
|
219
|
+
changed = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!changed)
|
|
223
|
+
return row;
|
|
224
|
+
return applyLineItemRowFormulas(itemFields, next);
|
|
225
|
+
}
|
|
165
226
|
/**
|
|
166
227
|
* Evaluates a line-items field's balance rule against its rows. Returns
|
|
167
228
|
* `undefined` when the field declares no balance rule. `balanced` is true when
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAkB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAyBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,qBAAqB,+BAoRvH"}
|
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
|
|
12
12
|
import { useEffect, useRef } from 'react';
|
|
13
13
|
import { Plus, Trash2, Check } from 'lucide-react';
|
|
14
|
-
import { resolveWidget, getItemFields, computeLineItemTotals, applyLineItemRowFormulas, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
|
|
14
|
+
import { resolveWidget, getItemFields, computeLineItemTotals, applyLineItemRowFormulas, autofillLineFromOption, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
|
|
15
15
|
import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field';
|
|
16
16
|
import { useOptionsResolver } from './use-options-resolver';
|
|
17
17
|
const fmtNumber = (n) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
@@ -78,14 +78,19 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
78
78
|
}
|
|
79
79
|
updateCell(idx, key, cellValue);
|
|
80
80
|
};
|
|
81
|
+
const handlePick = (idx, key, opt) => {
|
|
82
|
+
onChange(rows.map((r, i) => i === idx
|
|
83
|
+
? autofillLineFromOption(itemFields, { ...r, [key]: String(opt.id) }, opt)
|
|
84
|
+
: r));
|
|
85
|
+
};
|
|
81
86
|
const totalCols = itemFields.filter((c) => c.total);
|
|
82
87
|
return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => {
|
|
83
88
|
const cellErr = errMap[`${idx}.${col.key}`];
|
|
84
|
-
return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive text-xs", children: cellErr }))] }, col.key));
|
|
89
|
+
return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), onPick: (opt) => handlePick(idx, col.key, opt), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive text-xs", children: cellErr }))] }, col.key));
|
|
85
90
|
}) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
|
|
86
91
|
(isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => {
|
|
87
92
|
const cellErr = errMap[`${idx}.${col.key}`];
|
|
88
|
-
return (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive mt-1 text-xs", children: cellErr }))] }, col.key));
|
|
93
|
+
return (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), onPick: (opt) => handlePick(idx, col.key, opt), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive mt-1 text-xs", children: cellErr }))] }, col.key));
|
|
89
94
|
}), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
|
|
90
95
|
if (ci === 0) {
|
|
91
96
|
return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
|
|
@@ -107,7 +112,7 @@ function BalanceBadge({ state, }) {
|
|
|
107
112
|
// without the per-field Label (the column header is the label) and sized for a
|
|
108
113
|
// table cell. Nested line-items inside a row are not supported (a row column is
|
|
109
114
|
// a scalar widget).
|
|
110
|
-
function CellRenderer({ field, value, onChange, disabled, formValues, rowValues, invalid }) {
|
|
115
|
+
function CellRenderer({ field, value, onChange, onPick, disabled, formValues, rowValues, invalid }) {
|
|
111
116
|
const widget = resolveWidget(field);
|
|
112
117
|
const invalidCls = invalid
|
|
113
118
|
? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
|
|
@@ -143,10 +148,10 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
|
|
|
143
148
|
// Async searchable picker per row cell — e.g. the account_id column of a
|
|
144
149
|
// journal entry's debit/credit lines. Same widget as the flat form.
|
|
145
150
|
if (widget === 'dynamic_select') {
|
|
146
|
-
return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro, invalid: invalid }));
|
|
151
|
+
return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, onPick: onPick, dependsValue: dependsValue, readonly: ro, invalid: invalid }));
|
|
147
152
|
}
|
|
148
153
|
if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
|
|
149
|
-
return (_jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled, formValues: formValues, rowValues: rowValues }));
|
|
154
|
+
return (_jsx(RefCell, { field: field, value: value, onChange: onChange, onPick: onPick, disabled: disabled, formValues: formValues, rowValues: rowValues }));
|
|
150
155
|
}
|
|
151
156
|
switch (widget) {
|
|
152
157
|
case 'textarea':
|
|
@@ -176,7 +181,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues,
|
|
|
176
181
|
return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
|
-
function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {
|
|
184
|
+
function RefCell({ field, value, onChange, onPick, disabled, formValues, rowValues }) {
|
|
180
185
|
// Cascade: resolve the value of the field this cell `dependsOn` from the
|
|
181
186
|
// row (sibling) first, then the header form. While empty, the picker is
|
|
182
187
|
// disabled with a hint instead of listing the whole (unscoped) table.
|
|
@@ -210,5 +215,10 @@ function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {
|
|
|
210
215
|
: loading
|
|
211
216
|
? 'Cargando…'
|
|
212
217
|
: field.placeholder || 'Seleccionar...';
|
|
213
|
-
return (_jsxs(Select, { value: value || '', onValueChange:
|
|
218
|
+
return (_jsxs(Select, { value: value || '', onValueChange: (v) => {
|
|
219
|
+
onChange(v);
|
|
220
|
+
const opt = options.find((o) => String(o.id) === String(v));
|
|
221
|
+
if (opt)
|
|
222
|
+
onPick?.(opt);
|
|
223
|
+
}, disabled: disabled || loading || blockedByDependency, children: [_jsx(SelectTrigger, { className: "w-full", "data-depends-blocked": blockedByDependency ? '' : undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] }) }, String(opt.id)))) })] }));
|
|
214
224
|
}
|
|
@@ -33,6 +33,8 @@ export interface DynamicSelectFieldProps {
|
|
|
33
33
|
field: ActionFieldDef;
|
|
34
34
|
value: any;
|
|
35
35
|
onChange: (v: any) => void;
|
|
36
|
+
/** Opción completa recién elegida (incluye `meta` para autollenar columnas hermanas). */
|
|
37
|
+
onPick?: (opt: ResolvedOption) => void;
|
|
36
38
|
/**
|
|
37
39
|
* Pre-resolved option for the CURRENT value (label + image/color/icon) the
|
|
38
40
|
* caller already has — e.g. the relation sibling the table served. Lets the
|
|
@@ -91,6 +93,6 @@ export interface DynamicSelectFieldProps {
|
|
|
91
93
|
/** Paint trigger with destructive border when validation failed. */
|
|
92
94
|
invalid?: boolean;
|
|
93
95
|
}
|
|
94
|
-
export declare function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, }: DynamicSelectFieldProps): import("react").JSX.Element;
|
|
96
|
+
export declare function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly, staticOptions, descriptionAsBadge, hideCreate, createDefaults, createLockedFields, invalid, }: DynamicSelectFieldProps): import("react").JSX.Element;
|
|
95
97
|
export default DynamicSelectField;
|
|
96
98
|
//# 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":"AA4CA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA4BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAMD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;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;;;;OAIG;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,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,EAChB,aAAoB,EACpB,kBAA0B,EAC1B,UAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,OAAe,GAClB,EAAE,uBAAuB,+
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AA4CA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA4BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAMD,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;;;;OAIG;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,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,+BA2TzB;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -88,7 +88,7 @@ export function OptionLead({ option, size = 20, }) {
|
|
|
88
88
|
function useDebounced(value, ms) {
|
|
89
89
|
return useDebouncedValue(value, ms);
|
|
90
90
|
}
|
|
91
|
-
export function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
|
|
91
|
+
export function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
|
|
92
92
|
const { t } = useTranslation();
|
|
93
93
|
const ph = (fallback) => field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback;
|
|
94
94
|
const [open, setOpen] = useState(false);
|
|
@@ -180,6 +180,7 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
|
|
|
180
180
|
const handlePick = (opt) => {
|
|
181
181
|
setPicked(opt);
|
|
182
182
|
onChange(String(opt.id));
|
|
183
|
+
onPick?.(opt);
|
|
183
184
|
setOpen(false);
|
|
184
185
|
setSearch('');
|
|
185
186
|
};
|
|
@@ -11,6 +11,11 @@ export interface ResolvedOption {
|
|
|
11
11
|
image?: string | null;
|
|
12
12
|
color?: string | null;
|
|
13
13
|
icon?: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Campos extra del payload (precio, costo, sku, tasa) que no son decoración.
|
|
16
|
+
* Solo se llena cuando el option trae números o strings además de id/label.
|
|
17
|
+
*/
|
|
18
|
+
meta?: Record<string, unknown>;
|
|
14
19
|
}
|
|
15
20
|
export interface OptionsMeta {
|
|
16
21
|
/** 'static' for inline options, 'dynamic' for FK-resolved lists. */
|
|
@@ -84,10 +89,5 @@ export interface UseOptionsResolverResult {
|
|
|
84
89
|
export declare function invalidateOptionsCache(): void;
|
|
85
90
|
export declare function optionsRequestKey(scope: string, url: string, field: string, query: string, limit: number | undefined, filter: string | undefined): string;
|
|
86
91
|
export declare function useOptionsResolver(args: UseOptionsResolverArgs): UseOptionsResolverResult;
|
|
87
|
-
/**
|
|
88
|
-
* Normalizes the wire shape into ResolvedOption. The kernel returns dual
|
|
89
|
-
* id/value and label/name fields for legacy parity — we accept either
|
|
90
|
-
* and surface a stable shape downstream.
|
|
91
|
-
*/
|
|
92
92
|
export declare function projectOption(raw: any): ResolvedOption;
|
|
93
93
|
//# sourceMappingURL=use-options-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"use-options-resolver.d.ts","sourceRoot":"","sources":["../src/use-options-resolver.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC3B,8CAA8C;IAC9C,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAuBD;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAI7C;AAED,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAER;AA8BD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,wBAAwB,CA6JzF;AASD,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAqBtD"}
|
|
@@ -218,9 +218,19 @@ export function useOptionsResolver(args) {
|
|
|
218
218
|
* id/value and label/name fields for legacy parity — we accept either
|
|
219
219
|
* and surface a stable shape downstream.
|
|
220
220
|
*/
|
|
221
|
+
const OPTION_KNOWN_KEYS = new Set(['id', 'value', 'label', 'name', 'description', 'image', 'color', 'icon']);
|
|
221
222
|
export function projectOption(raw) {
|
|
222
223
|
const id = raw?.id ?? raw?.value ?? '';
|
|
223
224
|
const label = String(raw?.label ?? raw?.name ?? id ?? '');
|
|
225
|
+
const meta = {};
|
|
226
|
+
if (raw && typeof raw === 'object') {
|
|
227
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
228
|
+
if (OPTION_KNOWN_KEYS.has(k))
|
|
229
|
+
continue;
|
|
230
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
231
|
+
meta[k] = v;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
224
234
|
return {
|
|
225
235
|
id,
|
|
226
236
|
value: raw?.value ?? id,
|
|
@@ -230,5 +240,6 @@ export function projectOption(raw) {
|
|
|
230
240
|
image: raw?.image ?? null,
|
|
231
241
|
color: raw?.color ?? null,
|
|
232
242
|
icon: raw?.icon ?? null,
|
|
243
|
+
...(Object.keys(meta).length > 0 ? { meta } : {}),
|
|
233
244
|
};
|
|
234
245
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
5
|
+
|
|
6
|
+
vi.mock('react-i18next', () => ({
|
|
7
|
+
useTranslation: () => ({
|
|
8
|
+
t: (k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? k,
|
|
9
|
+
i18n: { language: 'es' },
|
|
10
|
+
}),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
import { DocumentLinesGrid, LineItemsEditor } from '../business/line-items-editor'
|
|
14
|
+
import { makeLine, type LineItem } from '../business/line-items'
|
|
15
|
+
import type { ProductResult } from '../business/product-search'
|
|
16
|
+
|
|
17
|
+
afterEach(cleanup)
|
|
18
|
+
|
|
19
|
+
function Harness({
|
|
20
|
+
initial = [],
|
|
21
|
+
search,
|
|
22
|
+
priceSource,
|
|
23
|
+
mode,
|
|
24
|
+
}: {
|
|
25
|
+
initial?: LineItem[]
|
|
26
|
+
search?: (q: any, signal: AbortSignal) => Promise<ProductResult[]>
|
|
27
|
+
priceSource?: 'sale' | 'cost' | 'origin'
|
|
28
|
+
mode?: 'free' | 'from_source'
|
|
29
|
+
}) {
|
|
30
|
+
const [lines, setLines] = useState(initial)
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<DocumentLinesGrid value={lines} onChange={setLines} search={search} priceSource={priceSource} mode={mode} currency="MXN" />
|
|
34
|
+
<output data-testid="state">{JSON.stringify(lines)}</output>
|
|
35
|
+
</>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const state = () => JSON.parse(screen.getByTestId('state').textContent ?? '[]') as LineItem[]
|
|
40
|
+
|
|
41
|
+
const tire: ProductResult = { id: 'p1', name: 'Llanta X', sku: 'LX', price: 1200, cost: 800, tax_rate: 0.16 }
|
|
42
|
+
|
|
43
|
+
describe('DocumentLinesGrid', () => {
|
|
44
|
+
it('es el mismo componente que LineItemsEditor', () => {
|
|
45
|
+
expect(LineItemsEditor).toBe(DocumentLinesGrid)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('al elegir un producto carga cantidad 1 y el stepper actualiza el importe', async () => {
|
|
49
|
+
const search = vi.fn(async () => [tire])
|
|
50
|
+
render(<Harness search={search} priceSource="cost" />)
|
|
51
|
+
fireEvent.change(screen.getByLabelText('Buscar producto'), { target: { value: 'llan' } })
|
|
52
|
+
const option = await waitFor(() => screen.getByRole('button', { name: /Llanta X/ }), { timeout: 2000 })
|
|
53
|
+
fireEvent.click(option)
|
|
54
|
+
expect(state()[0]).toMatchObject({ description: 'Llanta X', sku: 'LX', quantity: 1, unit_price: 800, catalog_price: 800 })
|
|
55
|
+
expect(screen.getByText(/Catálogo/)).toBeTruthy()
|
|
56
|
+
expect(document.querySelector('[data-slot="line-amount"]')?.textContent).toMatch(/800/)
|
|
57
|
+
fireEvent.click(screen.getByRole('button', { name: 'Aumentar cantidad' }))
|
|
58
|
+
expect(state()[0]!.quantity).toBe(2)
|
|
59
|
+
expect(document.querySelector('[data-slot="line-amount"]')?.textContent).toMatch(/1,600/)
|
|
60
|
+
expect(document.querySelector('[data-slot="tax-total"]')?.textContent).toMatch(/256/)
|
|
61
|
+
expect(document.querySelector('[data-slot="grand-total"]')?.textContent).toMatch(/1,856/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('Supr en una descripción vacía borra y se puede deshacer', () => {
|
|
65
|
+
render(<Harness initial={[makeLine({ description: '' })]} />)
|
|
66
|
+
fireEvent.keyDown(screen.getByLabelText('Descripción'), { key: 'Backspace' })
|
|
67
|
+
expect(state()).toHaveLength(0)
|
|
68
|
+
fireEvent.click(screen.getByRole('button', { name: 'Deshacer' }))
|
|
69
|
+
expect(state()).toHaveLength(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('desde origen no ofrece alta ni borrado', () => {
|
|
73
|
+
const search = vi.fn(async () => [tire])
|
|
74
|
+
render(<Harness initial={[makeLine({ description: 'Llanta', quantity: 2, max_quantity: 2 })]} search={search} mode="from_source" />)
|
|
75
|
+
expect(screen.queryByLabelText('Buscar producto')).toBeNull()
|
|
76
|
+
expect(screen.queryByRole('button', { name: 'Eliminar' })).toBeNull()
|
|
77
|
+
expect(screen.queryByRole('button', { name: 'Renglón libre' })).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { applyLineItemRowFormulas } from '../dynamic-form-schema'
|
|
2
|
+
import { applyLineItemRowFormulas, autofillLineFromOption } from '../dynamic-form-schema'
|
|
3
3
|
import type { ActionFieldDef } from '../types'
|
|
4
4
|
|
|
5
5
|
const fields: ActionFieldDef[] = [
|
|
@@ -53,3 +53,20 @@ describe('applyLineItemRowFormulas', () => {
|
|
|
53
53
|
expect(applyLineItemRowFormulas(lean, input)).toBe(input)
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
describe('autofillLineFromOption', () => {
|
|
58
|
+
it('copia precio, sku y recalcula el importe', () => {
|
|
59
|
+
const row = autofillLineFromOption(
|
|
60
|
+
fields,
|
|
61
|
+
{ product_id: '', qty: 2, unit_price: '', discount: 0, subtotal: 0 },
|
|
62
|
+
{ label: 'Llanta', meta: { price: 800, sku: 'LX' } },
|
|
63
|
+
)
|
|
64
|
+
expect(row.unit_price).toBe(800)
|
|
65
|
+
expect(row.subtotal).toBe(1600)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('no toca la fila si el option no trae meta', () => {
|
|
69
|
+
const input = { product_id: 'p1', qty: 1, unit_price: 10, discount: 0, subtotal: 10 }
|
|
70
|
+
expect(autofillLineFromOption(fields, input, { label: 'X' })).toBe(input)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -60,6 +60,11 @@ describe('projectOption', () => {
|
|
|
60
60
|
expect(out.icon).toBeNull()
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
it('keeps extra price and sku on meta', () => {
|
|
64
|
+
const out = projectOption({ id: 'p1', label: 'Llanta', price: 800, sku: 'LX', nested: { a: 1 } })
|
|
65
|
+
expect(out.meta).toEqual({ price: 800, sku: 'LX' })
|
|
66
|
+
})
|
|
67
|
+
|
|
63
68
|
it('survives empty payload (defensive)', () => {
|
|
64
69
|
const out = projectOption({})
|
|
65
70
|
expect(out.id).toBe('')
|
|
@@ -1501,7 +1501,7 @@ function renderField(
|
|
|
1501
1501
|
case 'date':
|
|
1502
1502
|
// Modern shadcn Calendar in a Popover (portaled, never clipped by the
|
|
1503
1503
|
// modal) instead of the native, dated, easily-cut <input type=date>.
|
|
1504
|
-
return <DynamicDateField field={field} value={value} onChange={onChange} />
|
|
1504
|
+
return <DynamicDateField field={field} value={value} onChange={onChange} invalid={invalid} />
|
|
1505
1505
|
default:
|
|
1506
1506
|
return <Input id={field.key} type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'} value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} placeholder={field.placeholder} aria-invalid={invalid || undefined} className={invalidCls || undefined} />
|
|
1507
1507
|
}
|