@asteby/metacore-runtime-react 46.2.3 → 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 +6 -0
- 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-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/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-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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 46.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8754c3c: DocumentLinesGrid: editor de renglones en línea (el mismo componente que LineItemsEditor). Buscador en la fila, cantidad con stepper, precio de lista o costo de catálogo, descuento en porcentaje o importe y totales al centavo. `serializeLineItems` solo agrega `unit`, `catalog_price` y `discount_kind` cuando el renglón los trae.
|
|
8
|
+
|
|
3
9
|
## 46.2.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EA8EtD,CAAA"}
|
package/dist/business/catalog.js
CHANGED
|
@@ -16,12 +16,24 @@ export const BUSINESS_COMPONENTS = [
|
|
|
16
16
|
helpers: ['parseProductQuery', 'parseTireSize', 'availableStock', 'productToLine'],
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
name: '
|
|
20
|
-
value: 'value: LineItem[]',
|
|
19
|
+
name: 'DocumentLinesGrid',
|
|
20
|
+
value: 'value: LineItem[] (LineItemsEditor es el mismo componente)',
|
|
21
21
|
events: ['onChange', 'onValidate', 'onRequestProduct'],
|
|
22
22
|
permissions: ['editPermission (prop, opcional)'],
|
|
23
|
-
usedBy: ['Cotización', 'Pedido', 'Factura', 'OC', 'Recepción', 'OT', 'RMA', 'NC'],
|
|
24
|
-
helpers: [
|
|
23
|
+
usedBy: ['Cotización', 'Pedido', 'Factura', 'OC', 'Recepción', 'OT', 'RMA', 'NC', 'Devolución'],
|
|
24
|
+
helpers: [
|
|
25
|
+
'makeLine',
|
|
26
|
+
'computeLine',
|
|
27
|
+
'computeTotals',
|
|
28
|
+
'taxBreakdown',
|
|
29
|
+
'validateLineItems',
|
|
30
|
+
'serializeLineItems',
|
|
31
|
+
'parseLineItems',
|
|
32
|
+
'priceFromProduct',
|
|
33
|
+
'applyProductToLine',
|
|
34
|
+
'addProductLine',
|
|
35
|
+
'lineGridKeyCommand',
|
|
36
|
+
],
|
|
25
37
|
},
|
|
26
38
|
{
|
|
27
39
|
name: 'PaymentCapture',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type LineItem } from './line-items';
|
|
2
|
+
import { type ProductResult, type ProductVariant } from './product-search';
|
|
3
|
+
export type PriceSource = 'sale' | 'cost' | 'origin';
|
|
4
|
+
export type DocumentLinesMode = 'free' | 'from_source';
|
|
5
|
+
/** Precio que se escribe en el renglón según el documento. */
|
|
6
|
+
export declare function priceFromProduct(product: ProductResult, variant: ProductVariant | undefined, source: PriceSource): number;
|
|
7
|
+
/** Rellena un renglón con el producto elegido (nombre, SKU, precio, costo de catálogo, existencia). */
|
|
8
|
+
export declare function applyProductToLine(line: LineItem, product: ProductResult, variant?: ProductVariant, opts?: {
|
|
9
|
+
priceSource?: PriceSource;
|
|
10
|
+
warehouseId?: string;
|
|
11
|
+
quantity?: number;
|
|
12
|
+
}): LineItem;
|
|
13
|
+
/**
|
|
14
|
+
* Agrega el producto como renglón. Por defecto, un segundo clic del mismo
|
|
15
|
+
* producto incrementa la cantidad (como el catálogo de Compras) y respeta `max_quantity`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function addProductLine(lines: LineItem[], product: ProductResult, variant?: ProductVariant, opts?: {
|
|
18
|
+
priceSource?: PriceSource;
|
|
19
|
+
warehouseId?: string;
|
|
20
|
+
quantity?: number;
|
|
21
|
+
mergeSameProduct?: boolean;
|
|
22
|
+
}): LineItem[];
|
|
23
|
+
export type LineGridCommand = 'add' | 'next' | 'delete' | 'none';
|
|
24
|
+
/** Enter agrega, Tab avanza, Supr/Backspace en celda vacía borra. No actúa si hay sugerencias abiertas. */
|
|
25
|
+
export declare function lineGridKeyCommand(key: string, opts?: {
|
|
26
|
+
suggestionsOpen?: boolean;
|
|
27
|
+
cellEmpty?: boolean;
|
|
28
|
+
mode?: DocumentLinesMode;
|
|
29
|
+
}): LineGridCommand;
|
|
30
|
+
//# sourceMappingURL=document-lines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-lines.d.ts","sourceRoot":"","sources":["../../src/business/document-lines.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AACpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,aAAa,CAAA;AAEtD,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAC5B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,cAAc,GAAG,SAAS,EACnC,MAAM,EAAE,WAAW,GACpB,MAAM,CAKR;AAED,uGAAuG;AACvG,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,QAAQ,CAmBV;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IACF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACxB,GACP,QAAQ,EAAE,CAeZ;AAED,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEhE,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,iBAAiB,CAAA;CAAO,GACxF,eAAe,CAKjB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Helpers del editor único de renglones (DocumentLinesGrid / LineItemsEditor).
|
|
2
|
+
// Puros: el componente solo pinta. `serializeLineItems` sigue siendo la puerta al backend.
|
|
3
|
+
import { toAmount } from './format';
|
|
4
|
+
import { makeLine } from './line-items';
|
|
5
|
+
import { availableStock } from './product-search';
|
|
6
|
+
/** Precio que se escribe en el renglón según el documento. */
|
|
7
|
+
export function priceFromProduct(product, variant, source) {
|
|
8
|
+
const list = variant?.price ?? product.price;
|
|
9
|
+
const cost = variant?.cost ?? product.cost;
|
|
10
|
+
if (source === 'cost')
|
|
11
|
+
return cost ?? list ?? 0;
|
|
12
|
+
return list ?? 0;
|
|
13
|
+
}
|
|
14
|
+
/** Rellena un renglón con el producto elegido (nombre, SKU, precio, costo de catálogo, existencia). */
|
|
15
|
+
export function applyProductToLine(line, product, variant, opts = {}) {
|
|
16
|
+
const source = opts.priceSource ?? 'sale';
|
|
17
|
+
const src = variant ?? product;
|
|
18
|
+
const list = variant?.price ?? product.price;
|
|
19
|
+
const cost = variant?.cost ?? product.cost;
|
|
20
|
+
const catalog = source === 'cost' ? (cost ?? list) : list;
|
|
21
|
+
return {
|
|
22
|
+
...line,
|
|
23
|
+
kind: 'item',
|
|
24
|
+
product_id: variant?.id ?? product.id,
|
|
25
|
+
sku: variant?.sku ?? product.sku,
|
|
26
|
+
description: variant ? `${product.name} · ${variant.label}` : product.name,
|
|
27
|
+
quantity: opts.quantity ?? (toAmount(line.quantity) || 1),
|
|
28
|
+
unit_price: priceFromProduct(product, variant, source),
|
|
29
|
+
catalog_price: catalog,
|
|
30
|
+
cost,
|
|
31
|
+
tax_rate: product.tax_rate ?? line.tax_rate ?? 0,
|
|
32
|
+
available: availableStock(src, opts.warehouseId) ?? availableStock(product, opts.warehouseId),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Agrega el producto como renglón. Por defecto, un segundo clic del mismo
|
|
37
|
+
* producto incrementa la cantidad (como el catálogo de Compras) y respeta `max_quantity`.
|
|
38
|
+
*/
|
|
39
|
+
export function addProductLine(lines, product, variant, opts = {}) {
|
|
40
|
+
const id = variant?.id ?? product.id;
|
|
41
|
+
const addQty = opts.quantity ?? 1;
|
|
42
|
+
if (opts.mergeSameProduct !== false) {
|
|
43
|
+
const idx = lines.findIndex((l) => l.kind === 'item' && l.product_id === id);
|
|
44
|
+
if (idx >= 0) {
|
|
45
|
+
return lines.map((l, i) => {
|
|
46
|
+
if (i !== idx)
|
|
47
|
+
return l;
|
|
48
|
+
const next = toAmount(l.quantity) + addQty;
|
|
49
|
+
const capped = l.max_quantity != null ? Math.min(next, toAmount(l.max_quantity)) : next;
|
|
50
|
+
return { ...l, quantity: capped };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...lines, applyProductToLine(makeLine(), product, variant, { ...opts, quantity: addQty })];
|
|
55
|
+
}
|
|
56
|
+
/** Enter agrega, Tab avanza, Supr/Backspace en celda vacía borra. No actúa si hay sugerencias abiertas. */
|
|
57
|
+
export function lineGridKeyCommand(key, opts = {}) {
|
|
58
|
+
if (key === 'Tab')
|
|
59
|
+
return 'next';
|
|
60
|
+
if (key === 'Enter' && !opts.suggestionsOpen && opts.mode !== 'from_source')
|
|
61
|
+
return 'add';
|
|
62
|
+
if ((key === 'Delete' || key === 'Backspace') && opts.cellEmpty && opts.mode !== 'from_source')
|
|
63
|
+
return 'delete';
|
|
64
|
+
return 'none';
|
|
65
|
+
}
|
package/dist/business/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA"}
|
package/dist/business/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type DocumentLinesMode, type PriceSource } from './document-lines';
|
|
1
2
|
import { type LineItem, type LineItemsPolicy, type LineItemsValidation } from './line-items';
|
|
2
|
-
|
|
3
|
-
export
|
|
3
|
+
import { type ProductQuery, type ProductResult } from './product-search';
|
|
4
|
+
export type LineItemsColumn = 'discount' | 'tax' | 'technician' | 'lot' | 'dot' | 'unit';
|
|
5
|
+
export interface DocumentLinesGridProps {
|
|
4
6
|
value: LineItem[];
|
|
5
7
|
/** Se emite con cada cambio; las líneas siempre traen `key`. */
|
|
6
8
|
onChange: (lines: LineItem[]) => void;
|
|
@@ -11,8 +13,26 @@ export interface LineItemsEditorProps {
|
|
|
11
13
|
serverErrors?: Record<string, string>;
|
|
12
14
|
/** Se emite tras cada cambio con el resultado de validar (para bloquear «Guardar»). */
|
|
13
15
|
onValidate?: (v: LineItemsValidation) => void;
|
|
14
|
-
/** Slot: abre el ProductPicker
|
|
16
|
+
/** Slot legado: abre el ProductPicker. Con `search`, el buscador va en la fila. */
|
|
15
17
|
onRequestProduct?: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Búsqueda en la fila (mismo contrato que ProductPicker). Al elegir, el renglón
|
|
20
|
+
* se llena aquí; un segundo clic del mismo producto suma cantidad.
|
|
21
|
+
*/
|
|
22
|
+
search?: (query: Exclude<ProductQuery, {
|
|
23
|
+
kind: 'empty';
|
|
24
|
+
}>, signal: AbortSignal) => Promise<ProductResult[]>;
|
|
25
|
+
/** `sale` usa el precio de lista; `cost` el costo de catálogo (compras). Default `sale`. */
|
|
26
|
+
priceSource?: PriceSource;
|
|
27
|
+
/** `from_source` oculta alta/borrado y permite cantidad 0 hasta `max_quantity`. */
|
|
28
|
+
mode?: DocumentLinesMode;
|
|
29
|
+
/** Cómo se captura el descuento. Default `percent` (el comportamiento anterior). */
|
|
30
|
+
discountMode?: 'percent' | 'amount' | 'both';
|
|
31
|
+
/** − / cantidad / +. Default: activo cuando hay `search`. */
|
|
32
|
+
quantityStepper?: boolean;
|
|
33
|
+
/** Un segundo clic incrementa la cantidad del mismo producto. Default true. */
|
|
34
|
+
mergeSameProduct?: boolean;
|
|
35
|
+
warehouseId?: string;
|
|
16
36
|
/** Permite secciones y notas (Odoo-style). Default true. */
|
|
17
37
|
allowSections?: boolean;
|
|
18
38
|
readOnly?: boolean;
|
|
@@ -21,5 +41,9 @@ export interface LineItemsEditorProps {
|
|
|
21
41
|
/** Moneda ISO (default: la de la org). */
|
|
22
42
|
currency?: string;
|
|
23
43
|
}
|
|
24
|
-
|
|
44
|
+
/** @deprecated Usa `DocumentLinesGridProps`. Es el mismo contrato. */
|
|
45
|
+
export type LineItemsEditorProps = DocumentLinesGridProps;
|
|
46
|
+
export declare function DocumentLinesGrid({ value, onChange, columns, policy, serverErrors, onValidate, onRequestProduct, search, priceSource, mode, discountMode, quantityStepper, mergeSameProduct, warehouseId, allowSections, readOnly, editPermission, currency, }: DocumentLinesGridProps): import("react").JSX.Element;
|
|
47
|
+
/** Mismo componente. El nombre anterior sigue exportado. */
|
|
48
|
+
export declare const LineItemsEditor: typeof DocumentLinesGrid;
|
|
25
49
|
//# sourceMappingURL=line-items-editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"AASA,OAAO,EAGH,KAAK,iBAAiB,EACtB,KAAK,WAAW,EACnB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAKH,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAC3B,MAAM,cAAc,CAAA;AAErB,OAAO,EAEH,KAAK,YAAY,EACjB,KAAK,aAAa,EAErB,MAAM,kBAAkB,CAAA;AAEzB,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,KAAK,GAAG,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;AAExF,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,gEAAgE;IAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAA;IACrC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAC7C,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IAC3G,4FAA4F;IAC5F,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,mFAAmF;IACnF,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,oFAAoF;IACpF,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC5C,6DAA6D;IAC7D,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AAIzD,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,OAAsB,EACtB,MAAM,EACN,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,WAAoB,EACpB,IAAa,EACb,YAAwB,EACxB,eAAe,EACf,gBAAuB,EACvB,WAAW,EACX,aAAoB,EACpB,QAAgB,EAChB,cAAc,EACd,QAAQ,GACX,EAAE,sBAAsB,+BAwWxB;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,0BAAoB,CAAA"}
|
|
@@ -1,36 +1,161 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
//
|
|
2
|
+
// DocumentLinesGrid — editor único de renglones (UX-2).
|
|
3
|
+
// `LineItemsEditor` es el mismo componente: las props nuevas son opcionales.
|
|
3
4
|
// Controlado: `value`/`onChange` con `LineItem[]`. Guardar = `serializeLineItems`.
|
|
4
|
-
import { useMemo } from 'react';
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { Plus,
|
|
7
|
+
import { Heading, Minus, Plus, Search, StickyNote, Trash2 } from 'lucide-react';
|
|
7
8
|
import { Button, Input } from '@asteby/metacore-ui';
|
|
8
9
|
import { useCan } from '../permissions-context';
|
|
9
10
|
import { useFormatter } from './format';
|
|
11
|
+
import { addProductLine, lineGridKeyCommand, } from './document-lines';
|
|
10
12
|
import { computeLine, computeTotals, makeLine, validateLineItems, } from './line-items';
|
|
13
|
+
import { useAsyncSearch } from './use-async-search';
|
|
14
|
+
import { parseProductQuery, } from './product-search';
|
|
11
15
|
const num = (s) => (s.trim() === '' ? 0 : Number(s.replace(',', '.')) || 0);
|
|
12
|
-
export function
|
|
16
|
+
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, }) {
|
|
13
17
|
const { t } = useTranslation();
|
|
14
18
|
const can = useCan();
|
|
15
19
|
const fmt = useFormatter({ currency });
|
|
16
20
|
const locked = readOnly || (editPermission ? !can(editPermission) : false);
|
|
17
|
-
const
|
|
18
|
-
const
|
|
21
|
+
const fromSource = mode === 'from_source';
|
|
22
|
+
const effectivePolicy = useMemo(() => (fromSource ? { ...policy, allowZeroQuantity: policy?.allowZeroQuantity ?? true } : (policy ?? {})), [policy, fromSource]);
|
|
23
|
+
const view = useMemo(() => discountMode === 'amount'
|
|
24
|
+
? value.map((l) => (l.kind === 'item' ? { ...l, discount_kind: 'amount' } : l))
|
|
25
|
+
: value, [value, discountMode]);
|
|
26
|
+
const validation = useMemo(() => validateLineItems(view, effectivePolicy), [view, effectivePolicy]);
|
|
27
|
+
const totals = useMemo(() => computeTotals(view), [view]);
|
|
19
28
|
const errors = { ...validation.errors, ...serverErrors };
|
|
20
29
|
const show = (c) => columns.includes(c);
|
|
30
|
+
const stepper = quantityStepper ?? !!search;
|
|
31
|
+
const canAdd = !locked && !fromSource;
|
|
32
|
+
const [undo, setUndo] = useState(null);
|
|
21
33
|
const emit = (next) => {
|
|
22
34
|
onChange(next);
|
|
23
|
-
|
|
35
|
+
const priced = discountMode === 'amount' ? next.map((l) => (l.kind === 'item' ? { ...l, discount_kind: 'amount' } : l)) : next;
|
|
36
|
+
onValidate?.(validateLineItems(priced, effectivePolicy));
|
|
37
|
+
};
|
|
38
|
+
const patch = (idx, p) => {
|
|
39
|
+
if (discountMode === 'amount')
|
|
40
|
+
p = { ...p, discount_kind: 'amount' };
|
|
41
|
+
emit(value.map((l, i) => (i === idx ? { ...l, ...p } : l)));
|
|
42
|
+
};
|
|
43
|
+
const remove = (idx) => {
|
|
44
|
+
setUndo(value);
|
|
45
|
+
emit(value.filter((_, i) => i !== idx));
|
|
24
46
|
};
|
|
25
|
-
const patch = (idx, p) => emit(value.map((l, i) => (i === idx ? { ...l, ...p } : l)));
|
|
26
|
-
const remove = (idx) => emit(value.filter((_, i) => i !== idx));
|
|
27
47
|
const cell = (i, field) => errors[`${i}.${field}`];
|
|
28
48
|
const warn = (i, field) => validation.warnings[`${i}.${field}`];
|
|
29
|
-
|
|
49
|
+
const pickProduct = (product, variant) => {
|
|
50
|
+
emit(addProductLine(value, product, variant, { priceSource, warehouseId, mergeSameProduct }));
|
|
51
|
+
};
|
|
52
|
+
return (_jsxs("div", { "data-slot": "line-items-editor", "data-component": "DocumentLinesGrid", "data-mode": mode, className: "space-y-2", children: [_jsx("div", { className: "overflow-x-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-2 py-2", children: t('lineItems.description', { defaultValue: 'Descripción' }) }), show('unit') && _jsx("th", { className: "w-20 px-2 py-2", children: t('lineItems.unit', { defaultValue: 'Unidad' }) }), _jsx("th", { className: "w-36 px-2 py-2 text-right", children: t('lineItems.quantity', { defaultValue: 'Cant.' }) }), _jsx("th", { className: "w-32 px-2 py-2 text-right", children: priceSource === 'cost'
|
|
53
|
+
? t('lineItems.unitCost', { defaultValue: 'Costo unitario' })
|
|
54
|
+
: t('lineItems.unitPrice', { defaultValue: 'Precio' }) }), show('discount') && (_jsx("th", { className: "w-28 px-2 py-2 text-right", children: discountMode === 'amount'
|
|
55
|
+
? t('lineItems.discountAmount', { defaultValue: 'Desc.' })
|
|
56
|
+
: t('lineItems.discount', { defaultValue: 'Desc. %' }) })), show('tax') && _jsx("th", { className: "w-20 px-2 py-2 text-right", children: t('lineItems.tax', { defaultValue: 'IVA %' }) }), show('technician') && _jsx("th", { className: "w-32 px-2 py-2", children: t('lineItems.technician', { defaultValue: 'Técnico' }) }), show('lot') && _jsx("th", { className: "w-28 px-2 py-2", children: t('lineItems.lot', { defaultValue: 'Lote' }) }), show('dot') && _jsx("th", { className: "w-24 px-2 py-2", children: "DOT" }), _jsx("th", { className: "w-32 px-2 py-2 text-right", children: t('lineItems.amount', { defaultValue: 'Importe' }) }), _jsx("th", { className: "w-10" })] }) }), _jsxs("tbody", { children: [value.map((l, i) => {
|
|
30
57
|
if (l.kind !== 'item') {
|
|
31
|
-
return (_jsx("tr", { className: "border-t bg-muted/30", children: _jsxs("td", { colSpan: 99, className: "px-2 py-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-invalid": !!cell(i, 'description'), placeholder: l.kind === 'section' ? t('lineItems.sectionPlaceholder', { defaultValue: 'Sección' }) : t('lineItems.notePlaceholder', { defaultValue: 'Nota' }), className: l.kind === 'section' ? 'font-semibold' : 'italic', onChange: (e) => patch(i, { description: e.target.value })
|
|
58
|
+
return (_jsx("tr", { className: "border-t bg-muted/30", children: _jsxs("td", { colSpan: 99, className: "px-2 py-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-invalid": !!cell(i, 'description'), placeholder: l.kind === 'section' ? t('lineItems.sectionPlaceholder', { defaultValue: 'Sección' }) : t('lineItems.notePlaceholder', { defaultValue: 'Nota' }), className: l.kind === 'section' ? 'font-semibold' : 'italic', onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => {
|
|
59
|
+
const cmd = lineGridKeyCommand(e.key, { cellEmpty: !l.description.trim(), mode });
|
|
60
|
+
if (cmd === 'delete') {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
remove(i);
|
|
63
|
+
}
|
|
64
|
+
} }), canAdd && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) }))] }), cell(i, 'description') && _jsx("p", { className: "mt-1 text-xs text-destructive", children: cell(i, 'description') })] }) }, l.key));
|
|
32
65
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
const shown = view[i] ?? l;
|
|
67
|
+
const a = computeLine(shown);
|
|
68
|
+
const kind = shown.discount_kind ?? 'percent';
|
|
69
|
+
const qtyMax = l.max_quantity;
|
|
70
|
+
return (_jsxs("tr", { className: "border-t align-top", "data-line-key": l.key, children: [_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-label": t('lineItems.description', { defaultValue: 'Descripción' }), "aria-invalid": !!cell(i, 'description'), onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => {
|
|
71
|
+
const cmd = lineGridKeyCommand(e.key, {
|
|
72
|
+
cellEmpty: !l.description.trim() && !l.product_id,
|
|
73
|
+
mode,
|
|
74
|
+
});
|
|
75
|
+
if (cmd === 'delete') {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
remove(i);
|
|
78
|
+
}
|
|
79
|
+
else if (cmd === 'add') {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
emit([...value, makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {})]);
|
|
82
|
+
}
|
|
83
|
+
} }), (l.sku || l.unit) && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [l.sku, !show('unit') ? l.unit : ''].filter(Boolean).join(' · ') })), cell(i, 'description') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'description') })] }), show('unit') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.unit ?? '', disabled: locked, onChange: (e) => patch(i, { unit: e.target.value }) }) })), _jsxs("td", { className: "px-2 py-1.5", 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", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => {
|
|
84
|
+
let q = num(e.target.value);
|
|
85
|
+
if (qtyMax != null && q > qtyMax)
|
|
86
|
+
q = qtyMax;
|
|
87
|
+
if (q < 0 && !effectivePolicy.allowNegative)
|
|
88
|
+
q = 0;
|
|
89
|
+
patch(i, { quantity: q });
|
|
90
|
+
} }), _jsx(Button, { type: "button", size: "icon", variant: "outline", className: "size-7", disabled: locked || (qtyMax != null && toNum(l.quantity) >= qtyMax), "aria-label": t('lineItems.increaseQty', { defaultValue: 'Aumentar cantidad' }), onClick: () => {
|
|
91
|
+
const next = toNum(l.quantity) + 1;
|
|
92
|
+
patch(i, { quantity: qtyMax != null ? Math.min(next, qtyMax) : next });
|
|
93
|
+
}, children: _jsx(Plus, { className: "size-3" }) })] })) : (_jsx(Input, { inputMode: "decimal", className: "text-right", 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: "px-2 py-1.5", children: [_jsx(Input, { inputMode: "decimal", className: "text-right", 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 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: "px-2 py-1.5", 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: "text-right", 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: "px-2 py-1.5", children: _jsx(Input, { inputMode: "decimal", className: "text-right", 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: "px-2 py-1.5", children: _jsx(Input, { value: l.technician_id ?? '', disabled: locked, onChange: (e) => patch(i, { technician_id: e.target.value }) }) })), show('lot') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.lot ?? '', disabled: locked, onChange: (e) => patch(i, { lot: e.target.value }) }) })), show('dot') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.dot ?? '', disabled: locked, onChange: (e) => patch(i, { dot: e.target.value }) }) })), _jsx("td", { className: "px-2 py-2 text-right tabular-nums", "data-slot": "line-amount", children: fmt.money(a.net) }), _jsx("td", { className: "px-1 py-1.5", children: canAdd && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) })) })] }, l.key));
|
|
94
|
+
}), 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(SearchRow, { search: search, warehouseId: warehouseId, currency: currency, onPick: pickProduct, onAddFree: () => emit([...value, makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {})]) }))] })] }) }), 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: () => {
|
|
95
|
+
emit(undo);
|
|
96
|
+
setUndo(null);
|
|
97
|
+
}, children: t('lineItems.undo', { defaultValue: 'Deshacer' }) })] })), _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [canAdd ? (_jsxs("div", { className: "flex flex-wrap gap-2", 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: "outline", onClick: () => emit([...value, makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {})]), 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' })] })] }))] })) : (_jsx("span", {})), _jsxs("dl", { className: "min-w-56 space-y-0.5 text-sm tabular-nums", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.subtotal', { defaultValue: 'Subtotal' }) }), _jsx("dd", { children: fmt.money(totals.subtotal) })] }), totals.discount > 0 && _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.discountTotal', { defaultValue: 'Descuento' }) }), _jsxs("dd", { children: ["-", fmt.money(totals.discount)] })] }), totals.tax > 0 && _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.taxTotal', { defaultValue: 'Impuestos' }) }), _jsx("dd", { "data-slot": "tax-total", children: fmt.money(totals.tax) })] }), _jsxs("div", { className: "flex justify-between gap-6 font-semibold", children: [_jsx("dt", { children: t('lineItems.total', { defaultValue: 'Total' }) }), _jsx("dd", { "data-slot": "grand-total", children: fmt.money(totals.total) })] })] })] })] }));
|
|
98
|
+
}
|
|
99
|
+
/** Mismo componente. El nombre anterior sigue exportado. */
|
|
100
|
+
export const LineItemsEditor = DocumentLinesGrid;
|
|
101
|
+
function toNum(n) {
|
|
102
|
+
return Number.isFinite(n) ? n : 0;
|
|
103
|
+
}
|
|
104
|
+
function SearchRow({ search, warehouseId, currency, onPick, onAddFree, }) {
|
|
105
|
+
const { t } = useTranslation();
|
|
106
|
+
const fmt = useFormatter({ currency });
|
|
107
|
+
const [text, setText] = useState('');
|
|
108
|
+
const [active, setActive] = useState(0);
|
|
109
|
+
const parsed = parseProductQuery(text);
|
|
110
|
+
const run = useMemo(() => {
|
|
111
|
+
return (q, signal) => {
|
|
112
|
+
const p = parseProductQuery(q);
|
|
113
|
+
return p.kind === 'empty' ? Promise.resolve([]) : search(p, signal);
|
|
114
|
+
};
|
|
115
|
+
}, [search]);
|
|
116
|
+
const { results, loading } = useAsyncSearch(text, run, {
|
|
117
|
+
minChars: parsed.kind === 'barcode' ? 8 : 2,
|
|
118
|
+
delay: parsed.kind === 'barcode' ? 0 : 250,
|
|
119
|
+
});
|
|
120
|
+
const flat = results.flatMap((p) => (p.variants?.length ?? 0) > 0 ? p.variants.map((v) => ({ product: p, variant: v })) : [{ product: p, variant: undefined }]);
|
|
121
|
+
const open = flat.length > 0;
|
|
122
|
+
const choose = (idx) => {
|
|
123
|
+
const hit = flat[idx];
|
|
124
|
+
if (!hit)
|
|
125
|
+
return;
|
|
126
|
+
onPick(hit.product, hit.variant);
|
|
127
|
+
setText('');
|
|
128
|
+
setActive(0);
|
|
129
|
+
};
|
|
130
|
+
return (_jsx("tr", { className: "border-t", children: _jsx("td", { colSpan: 99, className: "px-2 py-1.5", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", value: text, role: "combobox", "aria-expanded": open, "aria-label": t('lineItems.searchProduct', { defaultValue: 'Buscar producto' }), placeholder: t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), onChange: (e) => {
|
|
131
|
+
setText(e.target.value);
|
|
132
|
+
setActive(0);
|
|
133
|
+
}, onKeyDown: (e) => {
|
|
134
|
+
if (e.key === 'ArrowDown' && open) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
setActive((n) => Math.min(flat.length - 1, n + 1));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (e.key === 'ArrowUp' && open) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
setActive((n) => Math.max(0, n - 1));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const cmd = lineGridKeyCommand(e.key, { suggestionsOpen: open, cellEmpty: !text.trim(), mode: 'free' });
|
|
145
|
+
if (e.key === 'Enter' && open) {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
choose(active);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (cmd === 'add' && !text.trim()) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
onAddFree();
|
|
153
|
+
}
|
|
154
|
+
} }), open && (_jsx("ul", { role: "listbox", className: "absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover shadow-md", children: flat.map((hit, idx) => {
|
|
155
|
+
const price = priceOf(hit.product, hit.variant, warehouseId);
|
|
156
|
+
return (_jsx("li", { role: "option", "aria-selected": idx === active, children: _jsxs("button", { type: "button", className: 'flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left text-sm hover:bg-accent ' + (idx === active ? 'bg-accent' : ''), onMouseDown: (e) => e.preventDefault(), onClick: () => choose(idx), children: [_jsxs("span", { children: [_jsx("span", { className: "block font-medium", children: hit.variant ? `${hit.product.name} · ${hit.variant.label}` : hit.product.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: hit.variant?.sku ?? hit.product.sku })] }), price != null && _jsx("span", { className: "tabular-nums", children: fmt.money(price) })] }) }, `${hit.product.id}:${hit.variant?.id ?? ''}`));
|
|
157
|
+
}) })), loading && _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) })] }) }) }));
|
|
158
|
+
}
|
|
159
|
+
function priceOf(p, v, _warehouseId) {
|
|
160
|
+
return v?.price ?? p.price;
|
|
36
161
|
}
|
|
@@ -9,10 +9,22 @@ export interface LineItem {
|
|
|
9
9
|
description: string;
|
|
10
10
|
quantity: number;
|
|
11
11
|
unit_price: number;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Descuento. Porcentaje 0-100 salvo `discount_kind: 'amount'`, donde es importe.
|
|
14
|
+
*/
|
|
13
15
|
discount: number;
|
|
16
|
+
/** `percent` (default, se omite al serializar) o `amount`. */
|
|
17
|
+
discount_kind?: 'percent' | 'amount';
|
|
14
18
|
/** Tasa de impuesto como fracción (0.16). Viene de la config fiscal de la org. */
|
|
15
19
|
tax_rate: number;
|
|
20
|
+
/** Unidad de medida (pza, juego…). Solo se serializa si viene informada. */
|
|
21
|
+
unit?: string;
|
|
22
|
+
/** Precio o costo de catálogo, para mostrarlo junto al precio editado. */
|
|
23
|
+
catalog_price?: number;
|
|
24
|
+
/** Costo conocido. Aviso si el precio de venta queda por debajo. No se serializa. */
|
|
25
|
+
cost?: number;
|
|
26
|
+
/** Tope de cantidad (NC: facturado − acreditado; devolución: vendido − devuelto). */
|
|
27
|
+
max_quantity?: number;
|
|
16
28
|
technician_id?: string;
|
|
17
29
|
lot?: string;
|
|
18
30
|
dot?: string;
|
|
@@ -28,6 +40,8 @@ export interface LineItemsPolicy {
|
|
|
28
40
|
allowNegative?: boolean;
|
|
29
41
|
/** Exige al menos un renglón `item`. Default true. */
|
|
30
42
|
requireItems?: boolean;
|
|
43
|
+
/** Permite cantidad 0 (renglón de origen que no se acredita/devuelve). Default false. */
|
|
44
|
+
allowZeroQuantity?: boolean;
|
|
31
45
|
}
|
|
32
46
|
/** Clave de UI única en el proceso. */
|
|
33
47
|
export declare function newLineKey(): string;
|
|
@@ -39,7 +53,9 @@ export interface LineAmounts {
|
|
|
39
53
|
tax: number;
|
|
40
54
|
total: number;
|
|
41
55
|
}
|
|
42
|
-
export declare function computeLine(l: Pick<LineItem, 'kind' | 'quantity' | 'unit_price' | 'discount' | 'tax_rate'>
|
|
56
|
+
export declare function computeLine(l: Pick<LineItem, 'kind' | 'quantity' | 'unit_price' | 'discount' | 'tax_rate'> & {
|
|
57
|
+
discount_kind?: LineItem['discount_kind'];
|
|
58
|
+
}): LineAmounts;
|
|
43
59
|
export interface LineItemsTotals {
|
|
44
60
|
subtotal: number;
|
|
45
61
|
discount: number;
|
|
@@ -49,6 +65,12 @@ export interface LineItemsTotals {
|
|
|
49
65
|
units: number;
|
|
50
66
|
}
|
|
51
67
|
export declare function computeTotals(lines: LineItem[]): LineItemsTotals;
|
|
68
|
+
/** Impuesto agrupado por tasa, sumando el impuesto ya redondeado de cada renglón. */
|
|
69
|
+
export declare function taxBreakdown(lines: LineItem[]): Array<{
|
|
70
|
+
rate: number;
|
|
71
|
+
base: number;
|
|
72
|
+
tax: number;
|
|
73
|
+
}>;
|
|
52
74
|
export interface LineItemsValidation {
|
|
53
75
|
/** Errores por celda `"<índice>.<campo>"` (mismo formato que DynamicLineItems). */
|
|
54
76
|
errors: Record<string, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAEtD,MAAM,WAAW,QAAQ;IACrB,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,YAAY,CAAA;IAClB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB
|
|
1
|
+
{"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"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"}
|