@asteby/metacore-runtime-react 45.0.0 → 45.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/action-modal-dispatcher.d.ts +29 -0
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +115 -6
  5. package/dist/activity-diff.d.ts +2 -0
  6. package/dist/activity-diff.d.ts.map +1 -1
  7. package/dist/activity-diff.js +2 -2
  8. package/dist/approval-events.d.ts +16 -0
  9. package/dist/approval-events.d.ts.map +1 -0
  10. package/dist/approval-events.js +27 -0
  11. package/dist/approval-gate.d.ts +39 -0
  12. package/dist/approval-gate.d.ts.map +1 -0
  13. package/dist/approval-gate.js +110 -0
  14. package/dist/approval-pin-dialog.d.ts +25 -0
  15. package/dist/approval-pin-dialog.d.ts.map +1 -0
  16. package/dist/approval-pin-dialog.js +66 -0
  17. package/dist/business/catalog.d.ts +16 -0
  18. package/dist/business/catalog.d.ts.map +1 -0
  19. package/dist/business/catalog.js +50 -0
  20. package/dist/business/customer-picker.d.ts +39 -0
  21. package/dist/business/customer-picker.d.ts.map +1 -0
  22. package/dist/business/customer-picker.js +51 -0
  23. package/dist/business/feedback.d.ts +72 -0
  24. package/dist/business/feedback.d.ts.map +1 -0
  25. package/dist/business/feedback.js +52 -0
  26. package/dist/business/field-errors.d.ts +26 -0
  27. package/dist/business/field-errors.d.ts.map +1 -0
  28. package/dist/business/field-errors.js +59 -0
  29. package/dist/business/format.d.ts +49 -0
  30. package/dist/business/format.d.ts.map +1 -0
  31. package/dist/business/format.js +128 -0
  32. package/dist/business/index.d.ts +16 -0
  33. package/dist/business/index.d.ts.map +1 -0
  34. package/dist/business/index.js +15 -0
  35. package/dist/business/line-items-editor.d.ts +25 -0
  36. package/dist/business/line-items-editor.d.ts.map +1 -0
  37. package/dist/business/line-items-editor.js +36 -0
  38. package/dist/business/line-items.d.ts +69 -0
  39. package/dist/business/line-items.d.ts.map +1 -0
  40. package/dist/business/line-items.js +133 -0
  41. package/dist/business/payment-capture.d.ts +21 -0
  42. package/dist/business/payment-capture.d.ts.map +1 -0
  43. package/dist/business/payment-capture.js +24 -0
  44. package/dist/business/payment.d.ts +61 -0
  45. package/dist/business/payment.d.ts.map +1 -0
  46. package/dist/business/payment.js +74 -0
  47. package/dist/business/product-picker.d.ts +24 -0
  48. package/dist/business/product-picker.d.ts.map +1 -0
  49. package/dist/business/product-picker.js +48 -0
  50. package/dist/business/product-search.d.ts +70 -0
  51. package/dist/business/product-search.d.ts.map +1 -0
  52. package/dist/business/product-search.js +51 -0
  53. package/dist/business/refund-destination.d.ts +19 -0
  54. package/dist/business/refund-destination.d.ts.map +1 -0
  55. package/dist/business/refund-destination.js +31 -0
  56. package/dist/business/refund.d.ts +29 -0
  57. package/dist/business/refund.d.ts.map +1 -0
  58. package/dist/business/refund.js +44 -0
  59. package/dist/business/return-wizard.d.ts +23 -0
  60. package/dist/business/return-wizard.d.ts.map +1 -0
  61. package/dist/business/return-wizard.js +59 -0
  62. package/dist/business/return.d.ts +90 -0
  63. package/dist/business/return.d.ts.map +1 -0
  64. package/dist/business/return.js +112 -0
  65. package/dist/business/use-async-search.d.ts +10 -0
  66. package/dist/business/use-async-search.d.ts.map +1 -0
  67. package/dist/business/use-async-search.js +38 -0
  68. package/dist/document/action-bar.d.ts +12 -0
  69. package/dist/document/action-bar.d.ts.map +1 -0
  70. package/dist/document/action-bar.js +39 -0
  71. package/dist/document/cancel-with-reason.d.ts +47 -0
  72. package/dist/document/cancel-with-reason.d.ts.map +1 -0
  73. package/dist/document/cancel-with-reason.js +52 -0
  74. package/dist/document/document-header.d.ts +41 -0
  75. package/dist/document/document-header.d.ts.map +1 -0
  76. package/dist/document/document-header.js +30 -0
  77. package/dist/document/document-page.d.ts +37 -0
  78. package/dist/document/document-page.d.ts.map +1 -0
  79. package/dist/document/document-page.js +137 -0
  80. package/dist/document/document-timeline.d.ts +20 -0
  81. package/dist/document/document-timeline.d.ts.map +1 -0
  82. package/dist/document/document-timeline.js +26 -0
  83. package/dist/document/evaluate.d.ts +31 -0
  84. package/dist/document/evaluate.d.ts.map +1 -0
  85. package/dist/document/evaluate.js +170 -0
  86. package/dist/document/format.d.ts +9 -0
  87. package/dist/document/format.d.ts.map +1 -0
  88. package/dist/document/format.js +52 -0
  89. package/dist/document/index.d.ts +13 -0
  90. package/dist/document/index.d.ts.map +1 -0
  91. package/dist/document/index.js +12 -0
  92. package/dist/document/registry.d.ts +20 -0
  93. package/dist/document/registry.d.ts.map +1 -0
  94. package/dist/document/registry.js +20 -0
  95. package/dist/document/smart-buttons.d.ts +16 -0
  96. package/dist/document/smart-buttons.d.ts.map +1 -0
  97. package/dist/document/smart-buttons.js +16 -0
  98. package/dist/document/status-badge.d.ts +16 -0
  99. package/dist/document/status-badge.d.ts.map +1 -0
  100. package/dist/document/status-badge.js +23 -0
  101. package/dist/document/types.d.ts +196 -0
  102. package/dist/document/types.d.ts.map +1 -0
  103. package/dist/document/types.js +1 -0
  104. package/dist/document/use-document-sources.d.ts +14 -0
  105. package/dist/document/use-document-sources.d.ts.map +1 -0
  106. package/dist/document/use-document-sources.js +72 -0
  107. package/dist/dynamic-columns.d.ts.map +1 -1
  108. package/dist/dynamic-columns.js +3 -1
  109. package/dist/dynamic-relation.d.ts +4 -0
  110. package/dist/dynamic-relation.d.ts.map +1 -1
  111. package/dist/dynamic-relation.js +10 -1
  112. package/dist/dynamic-row-actions.d.ts.map +1 -1
  113. package/dist/dynamic-row-actions.js +14 -2
  114. package/dist/dynamic-table.d.ts.map +1 -1
  115. package/dist/dynamic-table.js +28 -3
  116. package/dist/index.d.ts +5 -0
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +6 -0
  119. package/dist/reason-prompt.d.ts +59 -0
  120. package/dist/reason-prompt.d.ts.map +1 -0
  121. package/dist/reason-prompt.js +111 -0
  122. package/dist/server-error.d.ts.map +1 -1
  123. package/dist/server-error.js +19 -0
  124. package/dist/use-addon-settings.d.ts +17 -3
  125. package/dist/use-addon-settings.d.ts.map +1 -1
  126. package/dist/use-addon-settings.js +20 -8
  127. package/package.json +1 -1
  128. package/src/__tests__/approval-gate.test.tsx +134 -0
  129. package/src/__tests__/prefill-from-record.test.ts +69 -0
  130. package/src/__tests__/reason-prompt.test.tsx +121 -0
  131. package/src/__tests__/use-addon-settings.test.ts +16 -1
  132. package/src/action-modal-dispatcher.tsx +145 -3
  133. package/src/activity-diff.tsx +10 -1
  134. package/src/approval-events.ts +39 -0
  135. package/src/approval-gate.tsx +180 -0
  136. package/src/approval-pin-dialog.tsx +162 -0
  137. package/src/business/business.test.ts +210 -0
  138. package/src/business/catalog.ts +68 -0
  139. package/src/business/customer-picker.tsx +201 -0
  140. package/src/business/feedback.tsx +141 -0
  141. package/src/business/field-errors.ts +92 -0
  142. package/src/business/format.ts +151 -0
  143. package/src/business/index.ts +15 -0
  144. package/src/business/line-items-editor.tsx +259 -0
  145. package/src/business/line-items.ts +190 -0
  146. package/src/business/payment-capture.tsx +158 -0
  147. package/src/business/payment.ts +125 -0
  148. package/src/business/product-picker.tsx +171 -0
  149. package/src/business/product-search.ts +108 -0
  150. package/src/business/refund-destination.tsx +121 -0
  151. package/src/business/refund.ts +72 -0
  152. package/src/business/return-wizard.tsx +223 -0
  153. package/src/business/return.ts +181 -0
  154. package/src/business/use-async-search.ts +43 -0
  155. package/src/document/__tests__/document-page.test.tsx +107 -0
  156. package/src/document/__tests__/evaluate.test.ts +140 -0
  157. package/src/document/action-bar.tsx +140 -0
  158. package/src/document/cancel-with-reason.tsx +214 -0
  159. package/src/document/document-header.tsx +140 -0
  160. package/src/document/document-page.tsx +268 -0
  161. package/src/document/document-timeline.tsx +66 -0
  162. package/src/document/evaluate.ts +180 -0
  163. package/src/document/format.ts +59 -0
  164. package/src/document/index.ts +28 -0
  165. package/src/document/registry.ts +39 -0
  166. package/src/document/smart-buttons.tsx +54 -0
  167. package/src/document/status-badge.tsx +44 -0
  168. package/src/document/types.ts +203 -0
  169. package/src/document/use-document-sources.ts +91 -0
  170. package/src/dynamic-columns.tsx +5 -1
  171. package/src/dynamic-relation.tsx +14 -1
  172. package/src/dynamic-row-actions.tsx +13 -1
  173. package/src/dynamic-table.tsx +22 -2
  174. package/src/index.ts +33 -0
  175. package/src/reason-prompt.tsx +247 -0
  176. package/src/server-error.ts +20 -0
  177. package/src/use-addon-settings.ts +26 -7
@@ -0,0 +1,74 @@
1
+ // Cobro mixto (POS/Caja, Apartado, Cobranza, Factura «Registrar pago», OT).
2
+ // Lógica pura de <PaymentCapture>: resumen (pagado/pendiente/cambio) y validación.
3
+ import { roundMoney, toAmount } from './format';
4
+ let seq = 0;
5
+ export function newTender(method, amount = 0) {
6
+ seq += 1;
7
+ return { key: `pt_${Date.now().toString(36)}_${seq}`, method, amount };
8
+ }
9
+ export function summarizePayment(total, tenders, methods) {
10
+ const byId = new Map(methods.map((m) => [m.id, m]));
11
+ let paid = 0;
12
+ let cash = 0;
13
+ for (const t of tenders) {
14
+ const a = Math.max(0, toAmount(t.amount));
15
+ paid += a;
16
+ if (byId.get(t.method)?.cash)
17
+ cash += a;
18
+ }
19
+ paid = roundMoney(paid);
20
+ const tot = roundMoney(total);
21
+ const excess = roundMoney(Math.max(0, paid - tot));
22
+ const change = roundMoney(Math.min(excess, cash));
23
+ return {
24
+ total: tot,
25
+ paid,
26
+ remaining: roundMoney(Math.max(0, tot - paid)),
27
+ change,
28
+ overpaid: roundMoney(excess - change),
29
+ settled: paid >= tot,
30
+ };
31
+ }
32
+ export function validatePayment(total, tenders, methods, policy = {}) {
33
+ const byId = new Map(methods.map((m) => [m.id, m]));
34
+ const errors = {};
35
+ tenders.forEach((t, i) => {
36
+ const m = byId.get(t.method);
37
+ const a = toAmount(t.amount);
38
+ if (!m)
39
+ errors[`${i}.method`] = 'Método de pago no disponible';
40
+ if (a <= 0)
41
+ errors[`${i}.amount`] = 'Captura un monto mayor a cero';
42
+ if (m?.maxAmount != null && a > m.maxAmount)
43
+ errors[`${i}.amount`] = `El máximo para ${m.label} es ${m.maxAmount}`;
44
+ if (m?.requiresReference && !t.reference?.trim())
45
+ errors[`${i}.reference`] = 'La referencia es obligatoria';
46
+ if (m?.requiresAccount && !t.account_id)
47
+ errors[`${i}.account_id`] = 'Elige la cuenta';
48
+ });
49
+ const s = summarizePayment(total, tenders, methods);
50
+ let form;
51
+ if (tenders.length === 0)
52
+ form = 'Agrega al menos un pago';
53
+ else if (s.overpaid > 0)
54
+ form = 'Solo el efectivo puede exceder el total (se devuelve como cambio)';
55
+ else if (!policy.allowPartial && !s.settled)
56
+ form = 'El pago no cubre el total';
57
+ return { errors, form, valid: !form && Object.keys(errors).length === 0 };
58
+ }
59
+ /** Payload hacia el backend: montos reales; el cambio no es un pago. */
60
+ export function serializePayment(tenders, methods, total) {
61
+ const s = summarizePayment(total, tenders, methods);
62
+ return {
63
+ payments: tenders.map((t) => ({
64
+ method: t.method,
65
+ // Monto entregado por el cliente; el cambio va aparte en `change`.
66
+ amount: roundMoney(toAmount(t.amount)),
67
+ ...(t.reference?.trim() ? { reference: t.reference.trim() } : {}),
68
+ ...(t.account_id ? { account_id: t.account_id } : {}),
69
+ })),
70
+ paid: s.paid,
71
+ change: s.change,
72
+ remaining: s.remaining,
73
+ };
74
+ }
@@ -0,0 +1,24 @@
1
+ import { type ProductQuery, type ProductResult, type ProductVariant } from './product-search';
2
+ export interface ProductPickerProps {
3
+ /**
4
+ * Búsqueda: recibe la consulta ya clasificada (`barcode` | `tire_size` |
5
+ * `text`) para que el backend elija el índice. Debe ser estable (useCallback).
6
+ */
7
+ search: (query: Exclude<ProductQuery, {
8
+ kind: 'empty';
9
+ }>, signal: AbortSignal) => Promise<ProductResult[]>;
10
+ /** Se emite al elegir un producto o una variante concreta. */
11
+ onSelect: (product: ProductResult, variant?: ProductVariant) => void;
12
+ /** Almacén para mostrar/validar existencias; sin él, se suma el total. */
13
+ warehouseId?: string;
14
+ /** Un código de barras con un único resultado se selecciona solo al pulsar Enter. Default true. */
15
+ autoSelectBarcode?: boolean;
16
+ /** Permite elegir productos sin existencia (se marcan). Default true. */
17
+ allowOutOfStock?: boolean;
18
+ placeholder?: string;
19
+ autoFocus?: boolean;
20
+ currency?: string;
21
+ disabled?: boolean;
22
+ }
23
+ export declare function ProductPicker({ search, onSelect, warehouseId, autoSelectBarcode, allowOutOfStock, placeholder, autoFocus, currency, disabled, }: ProductPickerProps): import("react").JSX.Element;
24
+ //# sourceMappingURL=product-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-picker.d.ts","sourceRoot":"","sources":["../../src/business/product-picker.tsx"],"names":[],"mappings":"AAQA,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACtB,MAAM,kBAAkB,CAAA;AAGzB,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,MAAM,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;IAC1G,8DAA8D;IAC9D,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACpE,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,QAAQ,EACR,WAAW,EACX,iBAAwB,EACxB,eAAsB,EACtB,WAAW,EACX,SAAS,EACT,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,+BA2HpB"}
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // ProductPicker — una sola búsqueda para productos, llantas y variantes
3
+ // (medida «205/55R16», SKU, código de barras, clave de proveedor). Benchmark §7.
4
+ import { useCallback, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Search } from 'lucide-react';
7
+ import { Badge, Input } from '@asteby/metacore-ui';
8
+ import { useFormatter } from './format';
9
+ import { useAsyncSearch } from './use-async-search';
10
+ import { availableStock, parseProductQuery, } from './product-search';
11
+ import { EmptyState } from './feedback';
12
+ export function ProductPicker({ search, onSelect, warehouseId, autoSelectBarcode = true, allowOutOfStock = true, placeholder, autoFocus, currency, disabled, }) {
13
+ const { t } = useTranslation();
14
+ const fmt = useFormatter({ currency });
15
+ const [text, setText] = useState('');
16
+ const parsed = parseProductQuery(text);
17
+ // Un código de barras es exacto: no se espera a 2 caracteres ni se debouncea mucho.
18
+ const run = useCallback((q, signal) => {
19
+ const p = parseProductQuery(q);
20
+ return p.kind === 'empty' ? Promise.resolve([]) : search(p, signal);
21
+ }, [search]);
22
+ const { results, loading, error } = useAsyncSearch(text, run, {
23
+ minChars: parsed.kind === 'barcode' ? 8 : 2,
24
+ delay: parsed.kind === 'barcode' ? 0 : 250,
25
+ });
26
+ const pick = (p, v) => {
27
+ const stock = availableStock(v ?? p, warehouseId);
28
+ if (!allowOutOfStock && stock != null && stock <= 0)
29
+ return;
30
+ onSelect(p, v);
31
+ setText('');
32
+ };
33
+ return (_jsxs("div", { "data-slot": "product-picker", className: "space-y-2", 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, { autoFocus: autoFocus, disabled: disabled, className: "pl-8", value: text, placeholder: placeholder ?? t('productPicker.placeholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
34
+ if (e.key === 'Enter' && autoSelectBarcode && parsed.kind === 'barcode' && results.length === 1) {
35
+ e.preventDefault();
36
+ const only = results[0];
37
+ const exact = only.variants?.find((v) => v.barcode === parsed.barcode);
38
+ pick(only, exact);
39
+ }
40
+ } })] }), parsed.kind === 'tire_size' && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [t('productPicker.tireSize', { defaultValue: 'Medida de llanta' }), ": ", parsed.tire.normalized] })), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('productPicker.error', { defaultValue: 'No se pudo buscar. Revisa tu conexión e inténtalo de nuevo.' }) })), !loading && !error && parsed.kind !== 'empty' && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('productPicker.empty', { defaultValue: 'No encontramos ese producto' }), description: t('productPicker.emptyHint', { defaultValue: 'Prueba con otra medida, SKU o código de barras.' }) })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-72 divide-y overflow-auto rounded-md border", children: results.map((p) => {
41
+ const stock = availableStock(p, warehouseId);
42
+ const hasVariants = (p.variants?.length ?? 0) > 0;
43
+ return (_jsxs("li", { role: "option", "aria-selected": false, className: "px-2 py-1.5", children: [_jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 text-left disabled:opacity-50", disabled: hasVariants || (!allowOutOfStock && stock != null && stock <= 0), onClick: () => pick(p), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: p.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: [p.sku, p.tire?.normalized, p.supplier_sku && `Prov. ${p.supplier_sku}`].filter(Boolean).join(' · ') })] }), _jsxs("span", { className: "flex items-center gap-2", children: [stock != null && (_jsx(Badge, { variant: stock > 0 ? 'success' : 'danger', children: stock > 0 ? t('productPicker.inStock', { defaultValue: '{{n}} disp.', n: stock }) : t('productPicker.outOfStock', { defaultValue: 'Sin existencia' }) })), p.price != null && !hasVariants && _jsx("span", { className: "text-sm tabular-nums", children: fmt.money(p.price) })] })] }), hasVariants && (_jsx("ul", { className: "mt-1 space-y-0.5 pl-3", children: p.variants.map((v) => {
44
+ const vs = availableStock(v, warehouseId);
45
+ return (_jsx("li", { children: _jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 rounded px-1 py-0.5 text-left text-sm hover:bg-accent disabled:opacity-50", disabled: !allowOutOfStock && vs != null && vs <= 0, onClick: () => pick(p, v), children: [_jsx("span", { children: v.label }), _jsxs("span", { className: "flex items-center gap-2", children: [vs != null && _jsx(Badge, { variant: vs > 0 ? 'success' : 'danger', children: vs }), (v.price ?? p.price) != null && _jsx("span", { className: "tabular-nums", children: fmt.money(v.price ?? p.price) })] })] }) }, v.id));
46
+ }) }))] }, p.id));
47
+ }) }))] }));
48
+ }
@@ -0,0 +1,70 @@
1
+ import type { LineItem } from './line-items';
2
+ export interface TireSize {
3
+ width: number;
4
+ ratio: number;
5
+ rim: number;
6
+ /** Forma canónica `205/55R16`. */
7
+ normalized: string;
8
+ }
9
+ export type ProductQuery = {
10
+ kind: 'empty';
11
+ raw: string;
12
+ } | {
13
+ kind: 'barcode';
14
+ raw: string;
15
+ barcode: string;
16
+ } | {
17
+ kind: 'tire_size';
18
+ raw: string;
19
+ tire: TireSize;
20
+ } | {
21
+ kind: 'text';
22
+ raw: string;
23
+ text: string;
24
+ };
25
+ export declare function parseTireSize(input: string): TireSize | null;
26
+ export declare function parseProductQuery(raw: string): ProductQuery;
27
+ export interface StockByWarehouse {
28
+ warehouse_id: string;
29
+ warehouse_name?: string;
30
+ available: number;
31
+ }
32
+ export interface ProductVariant {
33
+ id: string;
34
+ /** «Rin 16 · 205/55R16 · 91V». */
35
+ label: string;
36
+ sku?: string;
37
+ barcode?: string;
38
+ price?: number;
39
+ stock?: StockByWarehouse[];
40
+ }
41
+ export interface ProductResult {
42
+ id: string;
43
+ name: string;
44
+ sku?: string;
45
+ barcode?: string;
46
+ /** Clave del proveedor (búsqueda por SKU del proveedor). */
47
+ supplier_sku?: string;
48
+ kind?: 'product' | 'tire' | 'service' | 'bundle';
49
+ price?: number;
50
+ tax_rate?: number;
51
+ tire?: TireSize;
52
+ variants?: ProductVariant[];
53
+ stock?: StockByWarehouse[];
54
+ /** Paquetes automáticos: ids/nombres que se sugieren junto al producto. */
55
+ bundle_items?: Array<{
56
+ product_id: string;
57
+ name: string;
58
+ quantity: number;
59
+ }>;
60
+ }
61
+ /** Existencia disponible: de la variante o del producto, en un almacén o en total. */
62
+ export declare function availableStock(source: {
63
+ stock?: StockByWarehouse[];
64
+ }, warehouseId?: string): number | undefined;
65
+ /** Producto (+ variante) → renglón listo para <LineItemsEditor>. */
66
+ export declare function productToLine(p: ProductResult, variant?: ProductVariant, opts?: {
67
+ warehouseId?: string;
68
+ quantity?: number;
69
+ }): LineItem;
70
+ //# sourceMappingURL=product-search.d.ts.map
@@ -0,0 +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,CAWV"}
@@ -0,0 +1,51 @@
1
+ import { makeLine } from './line-items';
2
+ // 205/55R16, 205 55 16, 205-55-16, 205/55ZR16, 2055516 (sin separadores).
3
+ const TIRE_RE = /^(\d{3})\s*[\/\-x\s]?\s*(\d{2})\s*[\/\-x\s]?\s*(?:z?r|-)?\s*(\d{2})$/i;
4
+ const BARCODE_RE = /^\d{8,14}$/;
5
+ export function parseTireSize(input) {
6
+ const s = input.trim();
7
+ // 7 dígitos pegados también son medida, pero 8+ dígitos es código de barras.
8
+ if (BARCODE_RE.test(s))
9
+ return null;
10
+ const m = TIRE_RE.exec(s);
11
+ if (!m)
12
+ return null;
13
+ const width = Number(m[1]);
14
+ const ratio = Number(m[2]);
15
+ const rim = Number(m[3]);
16
+ if (width < 125 || width > 395 || ratio < 25 || ratio > 95 || rim < 10 || rim > 26)
17
+ return null;
18
+ return { width, ratio, rim, normalized: `${width}/${ratio}R${rim}` };
19
+ }
20
+ export function parseProductQuery(raw) {
21
+ const s = raw.trim();
22
+ if (!s)
23
+ return { kind: 'empty', raw };
24
+ if (BARCODE_RE.test(s))
25
+ return { kind: 'barcode', raw, barcode: s };
26
+ const tire = parseTireSize(s);
27
+ if (tire)
28
+ return { kind: 'tire_size', raw, tire };
29
+ return { kind: 'text', raw, text: s };
30
+ }
31
+ /** Existencia disponible: de la variante o del producto, en un almacén o en total. */
32
+ export function availableStock(source, warehouseId) {
33
+ if (!source.stock)
34
+ return undefined;
35
+ return source.stock
36
+ .filter((s) => !warehouseId || s.warehouse_id === warehouseId)
37
+ .reduce((n, s) => n + s.available, 0);
38
+ }
39
+ /** Producto (+ variante) → renglón listo para <LineItemsEditor>. */
40
+ export function productToLine(p, variant, opts = {}) {
41
+ const src = variant ?? p;
42
+ return makeLine({
43
+ product_id: variant?.id ?? p.id,
44
+ sku: variant?.sku ?? p.sku,
45
+ description: variant ? `${p.name} · ${variant.label}` : p.name,
46
+ quantity: opts.quantity ?? 1,
47
+ unit_price: variant?.price ?? p.price ?? 0,
48
+ tax_rate: p.tax_rate ?? 0,
49
+ available: availableStock(src, opts.warehouseId) ?? availableStock(p, opts.warehouseId),
50
+ });
51
+ }
@@ -0,0 +1,19 @@
1
+ import { type RefundAllocation, type RefundDestinationOption, type RefundValidation } from './refund';
2
+ export interface RefundDestinationProps {
3
+ /** Monto total a reembolsar. */
4
+ total: number;
5
+ value: RefundAllocation[];
6
+ onChange: (allocations: RefundAllocation[]) => void;
7
+ /** Destinos ofrecidos (default: los cinco). Filtra/limita según el documento. */
8
+ options?: RefundDestinationOption[];
9
+ /** Emite la validación tras cada cambio (para bloquear «Confirmar»). */
10
+ onValidate?: (v: RefundValidation) => void;
11
+ /** Errores del servidor `"<índice>.<campo>"`. */
12
+ serverErrors?: Record<string, string>;
13
+ /** Permite repartir entre varios destinos. Default false (un solo destino). */
14
+ allowSplit?: boolean;
15
+ currency?: string;
16
+ disabled?: boolean;
17
+ }
18
+ export declare function RefundDestination({ total, value, onChange, options, onValidate, serverErrors, allowSplit, currency, disabled, }: RefundDestinationProps): import("react").JSX.Element;
19
+ //# sourceMappingURL=refund-destination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refund-destination.d.ts","sourceRoot":"","sources":["../../src/business/refund-destination.tsx"],"names":[],"mappings":"AAMA,OAAO,EAIH,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACxB,MAAM,UAAU,CAAA;AAEjB,MAAM,WAAW,sBAAsB;IACnC,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,QAAQ,EAAE,CAAC,WAAW,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAA;IACnD,iFAAiF;IACjF,OAAO,CAAC,EAAE,uBAAuB,EAAE,CAAA;IACnC,wEAAwE;IACxE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC1C,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAqC,EACrC,UAAU,EACV,YAAY,EACZ,UAAkB,EAClB,QAAQ,EACR,QAAQ,GACX,EAAE,sBAAsB,+BA6ExB"}
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // RefundDestination — a dónde regresa el dinero: efectivo, tarjeta, monedero,
3
+ // saldo a favor o NC CFDI (benchmark §7, POS-3/DEV-*).
4
+ import { useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Button, Input } from '@asteby/metacore-ui';
7
+ import { useFormatter, roundMoney } from './format';
8
+ import { DEFAULT_REFUND_DESTINATIONS, singleDestination, validateRefund, } from './refund';
9
+ export function RefundDestination({ total, value, onChange, options = DEFAULT_REFUND_DESTINATIONS, onValidate, serverErrors, allowSplit = false, currency, disabled, }) {
10
+ const { t } = useTranslation();
11
+ const fmt = useFormatter({ currency });
12
+ const validation = useMemo(() => validateRefund(total, value, options), [total, value, options]);
13
+ const errors = { ...validation.errors, ...serverErrors };
14
+ const chosen = (k) => value.some((a) => a.kind === k);
15
+ const emit = (next) => {
16
+ onChange(next);
17
+ onValidate?.(validateRefund(total, next, options));
18
+ };
19
+ const toggle = (o) => {
20
+ if (!allowSplit)
21
+ return emit(singleDestination(o.kind, total));
22
+ if (chosen(o.kind))
23
+ return emit(value.filter((a) => a.kind !== o.kind));
24
+ emit([...value, { kind: o.kind, amount: Math.max(0, roundMoney(total - validation.allocated)) }]);
25
+ };
26
+ return (_jsxs("div", { "data-slot": "refund-destination", className: "space-y-2", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: [t('refund.amount', { defaultValue: 'Monto a reembolsar' }), ": ", _jsx("span", { className: "font-semibold text-foreground", children: fmt.money(total) })] }), _jsx("div", { role: allowSplit ? 'group' : 'radiogroup', className: "grid gap-2 sm:grid-cols-2", children: options.map((o) => {
27
+ const off = o.available === false;
28
+ const on = chosen(o.kind);
29
+ return (_jsx(Button, { type: "button", variant: on ? 'default' : 'outline', role: allowSplit ? 'checkbox' : 'radio', "aria-checked": on, disabled: disabled || off, title: off ? o.disabledReason : undefined, className: "h-auto justify-start whitespace-normal py-2 text-left", onClick: () => toggle(o), children: _jsxs("span", { children: [_jsx("span", { className: "block", children: o.label }), off && o.disabledReason && _jsx("span", { className: "block text-xs opacity-70", children: o.disabledReason }), !off && o.maxAmount != null && _jsxs("span", { className: "block text-xs opacity-70", children: [t('refund.max', { defaultValue: 'Máx.' }), " ", fmt.money(o.maxAmount)] })] }) }, o.kind));
30
+ }) }), value.map((a, i) => (_jsxs("div", { className: "grid grid-cols-[1fr_8rem_10rem] items-start gap-2", children: [_jsx("span", { className: "pt-2 text-sm", children: options.find((o) => o.kind === a.kind)?.label ?? a.kind }), _jsxs("div", { children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(a.amount), disabled: disabled || !allowSplit, "aria-invalid": !!errors[`${i}.amount`], onChange: (e) => emit(value.map((x, j) => (j === i ? { ...x, amount: Number(e.target.value.replace(',', '.')) || 0 } : x))) }), errors[`${i}.amount`] && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: errors[`${i}.amount`] })] }), (a.kind === 'card' || a.kind === 'transfer' || a.kind === 'credit_note') && (_jsx(Input, { placeholder: a.kind === 'card' || a.kind === 'transfer' ? t('refund.cardRef', { defaultValue: 'Referencia' }) : t('refund.ncRef', { defaultValue: 'Folio / UUID relacionado' }), value: a.reference ?? '', disabled: disabled, onChange: (e) => emit(value.map((x, j) => (j === i ? { ...x, reference: e.target.value } : x))) })), errors[`${i}.kind`] && _jsx("p", { className: "col-span-3 text-xs text-destructive", children: errors[`${i}.kind`] })] }, a.kind))), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form })] }));
31
+ }
@@ -0,0 +1,29 @@
1
+ export type RefundDestinationKind = 'cash' | 'card' | 'transfer' | 'wallet' | 'store_credit' | 'credit_note';
2
+ export interface RefundDestinationOption {
3
+ kind: RefundDestinationKind;
4
+ label: string;
5
+ /** Tope reembolsable por este destino (p. ej. lo pagado con tarjeta). */
6
+ maxAmount?: number;
7
+ /** false → la opción se muestra deshabilitada con `disabledReason`. */
8
+ available?: boolean;
9
+ disabledReason?: string;
10
+ }
11
+ export interface RefundAllocation {
12
+ kind: RefundDestinationKind;
13
+ amount: number;
14
+ /** Referencia del reembolso a tarjeta / folio de NC relacionada. */
15
+ reference?: string;
16
+ }
17
+ /** Catálogo por defecto: el llamador filtra/limita según el documento. */
18
+ export declare const DEFAULT_REFUND_DESTINATIONS: RefundDestinationOption[];
19
+ export interface RefundValidation {
20
+ errors: Record<string, string>;
21
+ form?: string;
22
+ allocated: number;
23
+ remaining: number;
24
+ valid: boolean;
25
+ }
26
+ export declare function validateRefund(total: number, allocations: RefundAllocation[], options?: RefundDestinationOption[]): RefundValidation;
27
+ /** Un solo destino cubriendo todo el monto (caso común). */
28
+ export declare function singleDestination(kind: RefundDestinationKind, total: number): RefundAllocation[];
29
+ //# sourceMappingURL=refund.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refund.d.ts","sourceRoot":"","sources":["../../src/business/refund.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CAAA;AAE5G,MAAM,WAAW,uBAAuB;IACpC,IAAI,EAAE,qBAAqB,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,qBAAqB,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,EAAE,uBAAuB,EAOhE,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,cAAc,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,gBAAgB,EAAE,EAC/B,OAAO,GAAE,uBAAuB,EAAgC,GACjE,gBAAgB,CAqBlB;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAEhG"}
@@ -0,0 +1,44 @@
1
+ // Destino de un reembolso (ReturnWizard, cancelación de apartado, anticipos).
2
+ // Lógica pura de <RefundDestination>.
3
+ import { roundMoney, toAmount } from './format';
4
+ /** Catálogo por defecto: el llamador filtra/limita según el documento. */
5
+ export const DEFAULT_REFUND_DESTINATIONS = [
6
+ { kind: 'cash', label: 'Efectivo' },
7
+ { kind: 'card', label: 'Tarjeta' },
8
+ { kind: 'transfer', label: 'Transferencia' },
9
+ { kind: 'wallet', label: 'Monedero' },
10
+ { kind: 'store_credit', label: 'Saldo a favor' },
11
+ { kind: 'credit_note', label: 'Nota de crédito (CFDI)' },
12
+ ];
13
+ export function validateRefund(total, allocations, options = DEFAULT_REFUND_DESTINATIONS) {
14
+ const byKind = new Map(options.map((o) => [o.kind, o]));
15
+ const errors = {};
16
+ let allocated = 0;
17
+ const seen = new Set();
18
+ allocations.forEach((a, i) => {
19
+ const o = byKind.get(a.kind);
20
+ const amt = toAmount(a.amount);
21
+ allocated += amt;
22
+ if (!o || o.available === false)
23
+ errors[`${i}.kind`] = o?.disabledReason ?? 'Destino no disponible';
24
+ if (seen.has(a.kind))
25
+ errors[`${i}.kind`] = 'Ese destino ya está en la lista';
26
+ seen.add(a.kind);
27
+ if (amt <= 0)
28
+ errors[`${i}.amount`] = 'Captura un monto mayor a cero';
29
+ else if (o?.maxAmount != null && amt > o.maxAmount)
30
+ errors[`${i}.amount`] = `El máximo para ${o.label} es ${o.maxAmount}`;
31
+ });
32
+ allocated = roundMoney(allocated);
33
+ const remaining = roundMoney(total - allocated);
34
+ let form;
35
+ if (allocations.length === 0)
36
+ form = 'Elige a dónde se devuelve el dinero';
37
+ else if (remaining !== 0)
38
+ form = remaining > 0 ? 'Falta asignar parte del reembolso' : 'La suma excede el monto a reembolsar';
39
+ return { errors, form, allocated, remaining, valid: !form && Object.keys(errors).length === 0 };
40
+ }
41
+ /** Un solo destino cubriendo todo el monto (caso común). */
42
+ export function singleDestination(kind, total) {
43
+ return [{ kind, amount: roundMoney(total) }];
44
+ }
@@ -0,0 +1,23 @@
1
+ import { type RefundDestinationOption } from './refund';
2
+ import { type ReturnSubmitPayload, type ReturnableLine } from './return';
3
+ export interface ReturnWizardProps {
4
+ lines: ReturnableLine[];
5
+ /** ¿Hay un addon de almacén que confirma la recepción? Sin él, «Recibir» es un paso. */
6
+ warehouseConnected?: boolean;
7
+ /** La venta está facturada: habilita la NC CFDI con relación 01/03 automática. */
8
+ invoiced?: boolean;
9
+ /** Destinos de reembolso; `credit_note` se deshabilita solo si no está facturada. */
10
+ refundOptions?: RefundDestinationOption[];
11
+ reasons?: {
12
+ value: string;
13
+ label: string;
14
+ }[];
15
+ currency?: string;
16
+ submitting?: boolean;
17
+ /** Errores del servidor (`"<key>.<campo>"`). */
18
+ serverErrors?: Record<string, string>;
19
+ onSubmit: (payload: ReturnSubmitPayload) => void | Promise<void>;
20
+ onCancel?: () => void;
21
+ }
22
+ export declare function ReturnWizard({ lines, warehouseConnected, invoiced, refundOptions, reasons, currency, submitting, serverErrors, onSubmit, onCancel, }: ReturnWizardProps): import("react").JSX.Element;
23
+ //# sourceMappingURL=return-wizard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"return-wizard.d.ts","sourceRoot":"","sources":["../../src/business/return-wizard.tsx"],"names":[],"mappings":"AAQA,OAAO,EAGH,KAAK,uBAAuB,EAC/B,MAAM,UAAU,CAAA;AACjB,OAAO,EAcH,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACtB,MAAM,UAAU,CAAA;AAEjB,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qFAAqF;IACrF,aAAa,CAAC,EAAE,uBAAuB,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAYD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,kBAA0B,EAC1B,QAAgB,EAChB,aAA2C,EAC3C,OAAyB,EACzB,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,GACX,EAAE,iBAAiB,+BAyJnB"}
@@ -0,0 +1,59 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // ReturnWizard («Devolver mercancía») — un solo componente para POS, Pedido,
3
+ // Factura, RMA y OT (benchmark §7, DEV-2). No conoce endpoints: recibe los
4
+ // renglones devolvibles y entrega un payload por `onSubmit`.
5
+ import { useMemo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Button, Input } from '@asteby/metacore-ui';
8
+ import { useFormatter } from './format';
9
+ import { RefundDestination } from './refund-destination';
10
+ import { DEFAULT_REFUND_DESTINATIONS, } from './refund';
11
+ import { RETURN_CONDITION_DESTINATIONS, computeReturnTotals, creditNoteRelation, defaultStockDestination, returnSteps, returnableQty, serializeReturn, validateReturnChoices, validateReturnRefund, } from './return';
12
+ const DEFAULT_REASONS = [
13
+ { value: 'not_wanted', label: 'No lo quiso' },
14
+ { value: 'defective', label: 'Defectuoso' },
15
+ { value: 'wrong_item', label: 'Producto equivocado' },
16
+ { value: 'warranty', label: 'Garantía' },
17
+ ];
18
+ const STOCK_LABEL = { stock: 'Regresa a stock', scrap: 'Merma', warranty: 'Garantía con proveedor' };
19
+ const STEP_LABEL = { lines: 'Renglones', condition: 'Condición', receive: 'Recibir', refund: 'Reembolso' };
20
+ export function ReturnWizard({ lines, warehouseConnected = false, invoiced = false, refundOptions = DEFAULT_REFUND_DESTINATIONS, reasons = DEFAULT_REASONS, currency, submitting, serverErrors, onSubmit, onCancel, }) {
21
+ const { t } = useTranslation();
22
+ const fmt = useFormatter({ currency });
23
+ const steps = useMemo(() => returnSteps({ warehouseConnected }), [warehouseConnected]);
24
+ const [stepIdx, setStepIdx] = useState(0);
25
+ const [choices, setChoices] = useState({});
26
+ const [reason, setReason] = useState('');
27
+ const [notes, setNotes] = useState('');
28
+ const [received, setReceived] = useState(false);
29
+ const [refund, setRefund] = useState([]);
30
+ const step = steps[stepIdx];
31
+ const totals = useMemo(() => computeReturnTotals(lines, choices), [lines, choices]);
32
+ const validation = useMemo(() => validateReturnChoices(lines, choices, reason), [lines, choices, reason]);
33
+ const relation = useMemo(() => creditNoteRelation(lines, choices), [lines, choices]);
34
+ const options = useMemo(() => refundOptions.map((o) => o.kind === 'credit_note' && !invoiced
35
+ ? { ...o, available: false, disabledReason: 'La venta no está facturada' }
36
+ : o.kind === 'credit_note'
37
+ ? { ...o, label: `${o.label} · relación ${relation}` }
38
+ : o), [refundOptions, invoiced, relation]);
39
+ const refundValidation = useMemo(() => validateReturnRefund(totals.total, refund, options), [totals.total, refund, options]);
40
+ const errors = { ...validation.errors, ...serverErrors };
41
+ const setChoice = (key, patch) => setChoices((prev) => {
42
+ const cur = prev[key] ?? { quantity: 0, condition: 'sellable', destination: 'stock' };
43
+ const next = { ...cur, ...patch };
44
+ if (patch.condition && !patch.destination)
45
+ next.destination = defaultStockDestination(patch.condition);
46
+ return { ...prev, [key]: next };
47
+ });
48
+ const canNext = step === 'lines' ? validation.valid
49
+ : step === 'condition' ? validation.valid
50
+ : step === 'receive' ? received
51
+ : refundValidation.valid;
52
+ const submit = () => onSubmit(serializeReturn({ lines, choices, reason, notes, received: warehouseConnected ? false : received, refund }));
53
+ return (_jsxs("div", { "data-slot": "return-wizard", className: "space-y-4", children: [_jsxs("ol", { className: "flex flex-wrap gap-2 text-xs", children: [steps.map((s, i) => (_jsxs("li", { "aria-current": i === stepIdx ? 'step' : undefined, className: i === stepIdx ? 'font-semibold text-foreground' : 'text-muted-foreground', children: [i + 1, ". ", t(`returns.step.${s}`, { defaultValue: STEP_LABEL[s] })] }, s))), warehouseConnected && (_jsx("li", { className: "text-muted-foreground", children: t('returns.step.warehouse', { defaultValue: 'Almacén confirma la recepción' }) }))] }), (step === 'lines' || step === 'condition') && (_jsxs("div", { className: "space-y-2", children: [lines.map((l) => {
54
+ const c = choices[l.key];
55
+ const max = returnableQty(l);
56
+ return (_jsxs("div", { className: "grid gap-2 rounded-md border p-2 sm:grid-cols-[1fr_6rem_auto]", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: l.description }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [t('returns.sold', { defaultValue: 'Vendido' }), ": ", l.sold, " \u00B7 ", t('returns.returnable', { defaultValue: 'Por devolver' }), ": ", max, " \u00B7 ", fmt.money(l.unit_price)] })] }), step === 'lines' ? (_jsxs("div", { children: [_jsx(Input, { inputMode: "decimal", className: "text-right", "aria-label": l.description, disabled: max === 0, value: c ? String(c.quantity) : '', placeholder: "0", onChange: (e) => setChoice(l.key, { quantity: Number(e.target.value.replace(',', '.')) || 0 }) }), errors[`${l.key}.quantity`] && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: errors[`${l.key}.quantity`] })] })) : ((c?.quantity ?? 0) > 0 && (_jsxs("div", { className: "sm:col-span-2 flex flex-wrap items-center gap-2", children: [['sellable', 'defective'].map((cond) => (_jsx(Button, { type: "button", size: "sm", variant: c.condition === cond ? 'default' : 'outline', onClick: () => setChoice(l.key, { condition: cond }), children: cond === 'sellable' ? t('returns.sellable', { defaultValue: 'Vendible' }) : t('returns.defective', { defaultValue: 'Defectuoso' }) }, cond))), RETURN_CONDITION_DESTINATIONS[c.condition].length > 1 &&
57
+ RETURN_CONDITION_DESTINATIONS[c.condition].map((d) => (_jsx(Button, { type: "button", size: "sm", variant: c.destination === d ? 'secondary' : 'ghost', onClick: () => setChoice(l.key, { destination: d }), children: STOCK_LABEL[d] }, d))), RETURN_CONDITION_DESTINATIONS[c.condition].length === 1 && (_jsx("span", { className: "text-xs text-muted-foreground", children: STOCK_LABEL[c.destination] })), errors[`${l.key}.destination`] && _jsx("p", { className: "w-full text-xs text-destructive", children: errors[`${l.key}.destination`] })] })))] }, l.key));
58
+ }), step === 'lines' && (_jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs("select", { className: "h-9 rounded-md border bg-background px-2 text-sm", value: reason, onChange: (e) => setReason(e.target.value), "aria-label": t('returns.reason', { defaultValue: 'Motivo' }), children: [_jsxs("option", { value: "", children: [t('returns.reason', { defaultValue: 'Motivo' }), "\u2026"] }), reasons.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value)))] }), _jsx(Input, { value: notes, onChange: (e) => setNotes(e.target.value), placeholder: t('returns.notes', { defaultValue: 'Notas (opcional)' }) })] })), validation.form && stepIdx === 0 && Object.keys(choices).length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form })] })), step === 'receive' && (_jsxs("label", { className: "flex items-start gap-2 rounded-md border p-3 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: received, onChange: (e) => setReceived(e.target.checked) }), _jsx("span", { children: t('returns.receiveHere', { defaultValue: 'Confirmo que la mercancía ya está en tienda. No hay almacén que la reciba, así que la recepción se registra aquí.' }) })] })), step === 'refund' && (_jsx(RefundDestination, { total: totals.total, value: refund, onChange: setRefund, options: options, currency: currency, allowSplit: true, serverErrors: serverErrors })), _jsxs("div", { className: "flex items-center justify-between gap-2 border-t pt-3", children: [_jsxs("span", { className: "text-sm", children: [t('returns.total', { defaultValue: 'Total a devolver' }), ": ", _jsx("strong", { children: fmt.money(totals.total) })] }), _jsxs("div", { className: "flex gap-2", children: [onCancel && stepIdx === 0 && _jsx(Button, { type: "button", variant: "outline", onClick: onCancel, children: t('common.cancel', { defaultValue: 'Cancelar' }) }), stepIdx > 0 && _jsx(Button, { type: "button", variant: "outline", onClick: () => setStepIdx(stepIdx - 1), children: t('common.back', { defaultValue: 'Atrás' }) }), stepIdx < steps.length - 1 ? (_jsx(Button, { type: "button", disabled: !canNext, onClick: () => setStepIdx(stepIdx + 1), children: t('common.next', { defaultValue: 'Siguiente' }) })) : (_jsxs(Button, { type: "button", disabled: !canNext || submitting, onClick: submit, children: [t('returns.confirm', { defaultValue: 'Confirmar devolución' }), " ", fmt.money(totals.total)] }))] })] })] }));
59
+ }
@@ -0,0 +1,90 @@
1
+ import { type RefundAllocation, type RefundDestinationOption } from './refund';
2
+ /** Estado físico de lo que regresa. */
3
+ export type ReturnCondition = 'sellable' | 'defective';
4
+ /** A dónde va la mercancía: stock vendible, merma o garantía (reclamo al proveedor). */
5
+ export type ReturnStockDestination = 'stock' | 'scrap' | 'warranty';
6
+ export declare const RETURN_CONDITION_DESTINATIONS: Record<ReturnCondition, ReturnStockDestination[]>;
7
+ /** Destino por defecto de una condición (vendible → stock, defectuoso → merma). */
8
+ export declare function defaultStockDestination(condition: ReturnCondition): ReturnStockDestination;
9
+ /** Renglón devolvible: lo vendido menos lo ya devuelto. */
10
+ export interface ReturnableLine {
11
+ key: string;
12
+ product_id?: string;
13
+ description: string;
14
+ sold: number;
15
+ alreadyReturned?: number;
16
+ /** Precio unitario neto de descuento tal como se vendió (congelado). */
17
+ unit_price: number;
18
+ /** Fracción (0.16). */
19
+ tax_rate?: number;
20
+ }
21
+ export interface ReturnLineChoice {
22
+ quantity: number;
23
+ condition: ReturnCondition;
24
+ destination: ReturnStockDestination;
25
+ }
26
+ export type ReturnChoices = Record<string, ReturnLineChoice>;
27
+ export declare function returnableQty(l: ReturnableLine): number;
28
+ export interface ReturnTotals {
29
+ base: number;
30
+ tax: number;
31
+ total: number;
32
+ goodsLines: number;
33
+ }
34
+ export declare function computeReturnTotals(lines: ReturnableLine[], choices: ReturnChoices): ReturnTotals;
35
+ /**
36
+ * Relación SAT (c_TipoRelacion) de la NC: 03 «devolución de mercancía» si regresan
37
+ * renglones con producto; 01 «nota de crédito de los documentos relacionados» si es
38
+ * solo ajuste de importe. Misma regla que aplica el backend al proyectar la NC.
39
+ */
40
+ export declare function creditNoteRelation(lines: ReturnableLine[], choices: ReturnChoices): '01' | '03';
41
+ export interface ReturnValidation {
42
+ errors: Record<string, string>;
43
+ form?: string;
44
+ valid: boolean;
45
+ }
46
+ export declare function validateReturnChoices(lines: ReturnableLine[], choices: ReturnChoices, reason?: string, requireReason?: boolean): ReturnValidation;
47
+ /** Pasos visibles. Sin almacén, «Recibir» es un paso del mismo asistente (PIT-021). */
48
+ export type ReturnStep = 'lines' | 'condition' | 'receive' | 'refund';
49
+ export declare function returnSteps(opts: {
50
+ warehouseConnected: boolean;
51
+ }): ReturnStep[];
52
+ export interface ReturnSubmitPayload {
53
+ reason: string;
54
+ notes?: string;
55
+ /** true cuando el asistente confirmó la recepción (sin almacén). */
56
+ received: boolean;
57
+ lines: {
58
+ key: string;
59
+ product_id?: string;
60
+ quantity: number;
61
+ condition: ReturnCondition;
62
+ destination: ReturnStockDestination;
63
+ }[];
64
+ refund: RefundAllocation[];
65
+ /** Presente solo si el reembolso incluye una NC CFDI. */
66
+ credit_note?: {
67
+ relation: '01' | '03';
68
+ };
69
+ totals: ReturnTotals;
70
+ }
71
+ /** Única puerta de salida hacia el backend: solo renglones con cantidad > 0. */
72
+ export declare function serializeReturn(args: {
73
+ lines: ReturnableLine[];
74
+ choices: ReturnChoices;
75
+ reason: string;
76
+ notes?: string;
77
+ received: boolean;
78
+ refund: RefundAllocation[];
79
+ }): ReturnSubmitPayload;
80
+ /** Valida el paso de reembolso contra el total de la devolución. */
81
+ export declare function validateReturnRefund(total: number, refund: RefundAllocation[], options?: RefundDestinationOption[]): import("./refund").RefundValidation;
82
+ export type RmaState = 'draft' | 'authorized' | 'received' | 'settled' | 'closed' | 'rejected' | 'cancelled';
83
+ export declare const RMA_TRANSITIONS: Record<RmaState, RmaState[]>;
84
+ /** Estados a los que se llega con motivo obligatorio. */
85
+ export declare const RMA_REASON_REQUIRED: RmaState[];
86
+ export declare function canTransitionRma(from: RmaState, to: RmaState, reason?: string): {
87
+ ok: boolean;
88
+ error?: string;
89
+ };
90
+ //# sourceMappingURL=return.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"return.d.ts","sourceRoot":"","sources":["../../src/business/return.ts"],"names":[],"mappings":"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"}