@asteby/metacore-runtime-react 45.0.0 → 45.2.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 (137) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/approval-events.d.ts +16 -0
  3. package/dist/approval-events.d.ts.map +1 -0
  4. package/dist/approval-events.js +27 -0
  5. package/dist/approval-gate.d.ts +39 -0
  6. package/dist/approval-gate.d.ts.map +1 -0
  7. package/dist/approval-gate.js +110 -0
  8. package/dist/approval-pin-dialog.d.ts +25 -0
  9. package/dist/approval-pin-dialog.d.ts.map +1 -0
  10. package/dist/approval-pin-dialog.js +66 -0
  11. package/dist/business/catalog.d.ts +16 -0
  12. package/dist/business/catalog.d.ts.map +1 -0
  13. package/dist/business/catalog.js +42 -0
  14. package/dist/business/customer-picker.d.ts +39 -0
  15. package/dist/business/customer-picker.d.ts.map +1 -0
  16. package/dist/business/customer-picker.js +51 -0
  17. package/dist/business/feedback.d.ts +72 -0
  18. package/dist/business/feedback.d.ts.map +1 -0
  19. package/dist/business/feedback.js +52 -0
  20. package/dist/business/field-errors.d.ts +26 -0
  21. package/dist/business/field-errors.d.ts.map +1 -0
  22. package/dist/business/field-errors.js +59 -0
  23. package/dist/business/format.d.ts +49 -0
  24. package/dist/business/format.d.ts.map +1 -0
  25. package/dist/business/format.js +128 -0
  26. package/dist/business/index.d.ts +14 -0
  27. package/dist/business/index.d.ts.map +1 -0
  28. package/dist/business/index.js +13 -0
  29. package/dist/business/line-items-editor.d.ts +25 -0
  30. package/dist/business/line-items-editor.d.ts.map +1 -0
  31. package/dist/business/line-items-editor.js +36 -0
  32. package/dist/business/line-items.d.ts +69 -0
  33. package/dist/business/line-items.d.ts.map +1 -0
  34. package/dist/business/line-items.js +133 -0
  35. package/dist/business/payment-capture.d.ts +21 -0
  36. package/dist/business/payment-capture.d.ts.map +1 -0
  37. package/dist/business/payment-capture.js +24 -0
  38. package/dist/business/payment.d.ts +61 -0
  39. package/dist/business/payment.d.ts.map +1 -0
  40. package/dist/business/payment.js +74 -0
  41. package/dist/business/product-picker.d.ts +24 -0
  42. package/dist/business/product-picker.d.ts.map +1 -0
  43. package/dist/business/product-picker.js +48 -0
  44. package/dist/business/product-search.d.ts +70 -0
  45. package/dist/business/product-search.d.ts.map +1 -0
  46. package/dist/business/product-search.js +51 -0
  47. package/dist/business/refund-destination.d.ts +19 -0
  48. package/dist/business/refund-destination.d.ts.map +1 -0
  49. package/dist/business/refund-destination.js +31 -0
  50. package/dist/business/refund.d.ts +29 -0
  51. package/dist/business/refund.d.ts.map +1 -0
  52. package/dist/business/refund.js +43 -0
  53. package/dist/business/use-async-search.d.ts +10 -0
  54. package/dist/business/use-async-search.d.ts.map +1 -0
  55. package/dist/business/use-async-search.js +38 -0
  56. package/dist/document/action-bar.d.ts +12 -0
  57. package/dist/document/action-bar.d.ts.map +1 -0
  58. package/dist/document/action-bar.js +39 -0
  59. package/dist/document/cancel-with-reason.d.ts +47 -0
  60. package/dist/document/cancel-with-reason.d.ts.map +1 -0
  61. package/dist/document/cancel-with-reason.js +52 -0
  62. package/dist/document/document-header.d.ts +41 -0
  63. package/dist/document/document-header.d.ts.map +1 -0
  64. package/dist/document/document-header.js +30 -0
  65. package/dist/document/document-page.d.ts +37 -0
  66. package/dist/document/document-page.d.ts.map +1 -0
  67. package/dist/document/document-page.js +137 -0
  68. package/dist/document/document-timeline.d.ts +20 -0
  69. package/dist/document/document-timeline.d.ts.map +1 -0
  70. package/dist/document/document-timeline.js +26 -0
  71. package/dist/document/evaluate.d.ts +31 -0
  72. package/dist/document/evaluate.d.ts.map +1 -0
  73. package/dist/document/evaluate.js +170 -0
  74. package/dist/document/format.d.ts +9 -0
  75. package/dist/document/format.d.ts.map +1 -0
  76. package/dist/document/format.js +52 -0
  77. package/dist/document/index.d.ts +13 -0
  78. package/dist/document/index.d.ts.map +1 -0
  79. package/dist/document/index.js +12 -0
  80. package/dist/document/registry.d.ts +20 -0
  81. package/dist/document/registry.d.ts.map +1 -0
  82. package/dist/document/registry.js +20 -0
  83. package/dist/document/smart-buttons.d.ts +16 -0
  84. package/dist/document/smart-buttons.d.ts.map +1 -0
  85. package/dist/document/smart-buttons.js +16 -0
  86. package/dist/document/status-badge.d.ts +16 -0
  87. package/dist/document/status-badge.d.ts.map +1 -0
  88. package/dist/document/status-badge.js +23 -0
  89. package/dist/document/types.d.ts +196 -0
  90. package/dist/document/types.d.ts.map +1 -0
  91. package/dist/document/types.js +1 -0
  92. package/dist/document/use-document-sources.d.ts +14 -0
  93. package/dist/document/use-document-sources.d.ts.map +1 -0
  94. package/dist/document/use-document-sources.js +72 -0
  95. package/dist/index.d.ts +4 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +5 -0
  98. package/dist/server-error.d.ts.map +1 -1
  99. package/dist/server-error.js +19 -0
  100. package/package.json +1 -1
  101. package/src/__tests__/approval-gate.test.tsx +134 -0
  102. package/src/approval-events.ts +39 -0
  103. package/src/approval-gate.tsx +180 -0
  104. package/src/approval-pin-dialog.tsx +162 -0
  105. package/src/business/business.test.ts +164 -0
  106. package/src/business/catalog.ts +60 -0
  107. package/src/business/customer-picker.tsx +201 -0
  108. package/src/business/feedback.tsx +141 -0
  109. package/src/business/field-errors.ts +92 -0
  110. package/src/business/format.ts +151 -0
  111. package/src/business/index.ts +13 -0
  112. package/src/business/line-items-editor.tsx +259 -0
  113. package/src/business/line-items.ts +190 -0
  114. package/src/business/payment-capture.tsx +158 -0
  115. package/src/business/payment.ts +125 -0
  116. package/src/business/product-picker.tsx +171 -0
  117. package/src/business/product-search.ts +108 -0
  118. package/src/business/refund-destination.tsx +121 -0
  119. package/src/business/refund.ts +71 -0
  120. package/src/business/use-async-search.ts +43 -0
  121. package/src/document/__tests__/document-page.test.tsx +107 -0
  122. package/src/document/__tests__/evaluate.test.ts +140 -0
  123. package/src/document/action-bar.tsx +140 -0
  124. package/src/document/cancel-with-reason.tsx +214 -0
  125. package/src/document/document-header.tsx +140 -0
  126. package/src/document/document-page.tsx +268 -0
  127. package/src/document/document-timeline.tsx +66 -0
  128. package/src/document/evaluate.ts +180 -0
  129. package/src/document/format.ts +59 -0
  130. package/src/document/index.ts +28 -0
  131. package/src/document/registry.ts +39 -0
  132. package/src/document/smart-buttons.tsx +54 -0
  133. package/src/document/status-badge.tsx +44 -0
  134. package/src/document/types.ts +203 -0
  135. package/src/document/use-document-sources.ts +91 -0
  136. package/src/index.ts +23 -0
  137. package/src/server-error.ts +20 -0
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // PaymentCapture — cobro mixto con cambio, referencia y cuenta (benchmark §7).
3
+ import { useMemo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Trash2 } from 'lucide-react';
6
+ import { Button, Input } from '@asteby/metacore-ui';
7
+ import { useCan } from '../permissions-context';
8
+ import { useFormatter } from './format';
9
+ import { newTender, serializePayment, summarizePayment, validatePayment, } from './payment';
10
+ export function PaymentCapture({ total, methods, value, onChange, onSubmit, policy, serverErrors, submitLabel, submitting, submitPermission, currency, disabled, }) {
11
+ const { t } = useTranslation();
12
+ const can = useCan();
13
+ const fmt = useFormatter({ currency });
14
+ const summary = useMemo(() => summarizePayment(total, value, methods), [total, value, methods]);
15
+ const validation = useMemo(() => validatePayment(total, value, methods, policy), [total, value, methods, policy]);
16
+ const errors = { ...validation.errors, ...serverErrors };
17
+ const byId = new Map(methods.map((m) => [m.id, m]));
18
+ const mayPay = submitPermission ? can(submitPermission) : true;
19
+ const patch = (idx, p) => onChange(value.map((x, i) => (i === idx ? { ...x, ...p } : x)));
20
+ return (_jsxs("div", { "data-slot": "payment-capture", className: "space-y-3", children: [_jsx("div", { className: "flex flex-wrap gap-2", children: methods.map((m) => (_jsx(Button, { type: "button", size: "sm", variant: "outline", disabled: disabled, onClick: () => onChange([...value, newTender(m.id, summary.remaining)]), children: m.label }, m.id))) }), value.map((tender, i) => {
21
+ const m = byId.get(tender.method);
22
+ return (_jsxs("div", { className: "grid grid-cols-[1fr_8rem_auto] items-start gap-2 rounded-md border p-2", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: m?.label ?? tender.method }), m?.requiresReference && (_jsx(Input, { placeholder: t('payment.reference', { defaultValue: 'Referencia' }), value: tender.reference ?? '', disabled: disabled, "aria-invalid": !!errors[`${i}.reference`], onChange: (e) => patch(i, { reference: e.target.value }) })), m?.requiresAccount && (_jsxs("select", { className: "h-9 w-full rounded-md border bg-background px-2 text-sm", value: tender.account_id ?? '', disabled: disabled, onChange: (e) => patch(i, { account_id: e.target.value || undefined }), children: [_jsx("option", { value: "", children: t('payment.account', { defaultValue: 'Cuenta…' }) }), (m.accounts ?? []).map((a) => (_jsx("option", { value: a.id, children: a.label }, a.id)))] })), ['reference', 'account_id', 'method'].map((f) => errors[`${i}.${f}`] && _jsx("p", { className: "text-xs text-destructive", children: errors[`${i}.${f}`] }, f))] }), _jsxs("div", { children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(tender.amount), disabled: disabled, "aria-invalid": !!errors[`${i}.amount`], onChange: (e) => patch(i, { amount: Number(e.target.value.replace(',', '.')) || 0 }) }), errors[`${i}.amount`] && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: errors[`${i}.amount`] })] }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", disabled: disabled, "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), onClick: () => onChange(value.filter((_, j) => j !== i)), children: _jsx(Trash2, { className: "size-4" }) })] }, tender.key));
23
+ }), _jsxs("dl", { className: "space-y-0.5 text-sm tabular-nums", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: t('payment.total', { defaultValue: 'Total' }) }), _jsx("dd", { children: fmt.money(summary.total) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: t('payment.paid', { defaultValue: 'Pagado' }) }), _jsx("dd", { children: fmt.money(summary.paid) })] }), summary.remaining > 0 && _jsxs("div", { className: "flex justify-between font-medium", children: [_jsx("dt", { children: t('payment.remaining', { defaultValue: 'Pendiente' }) }), _jsx("dd", { children: fmt.money(summary.remaining) })] }), summary.change > 0 && _jsxs("div", { className: "flex justify-between font-semibold", children: [_jsx("dt", { children: t('payment.change', { defaultValue: 'Cambio' }) }), _jsx("dd", { children: fmt.money(summary.change) })] })] }), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form }), onSubmit && mayPay && (_jsx(Button, { type: "button", className: "w-full", disabled: disabled || submitting || !validation.valid, onClick: () => onSubmit(serializePayment(value, methods, total)), children: submitLabel ?? t('payment.submit', { defaultValue: 'Cobrar' }) }))] }));
24
+ }
@@ -0,0 +1,61 @@
1
+ /** Método de pago configurable por org (el catálogo NO se hardcodea en el SDK). */
2
+ export interface PaymentMethodOption {
3
+ id: string;
4
+ label: string;
5
+ /** Efectivo: admite recibir de más y devolver cambio. */
6
+ cash?: boolean;
7
+ requiresReference?: boolean;
8
+ requiresAccount?: boolean;
9
+ /** Tope por método (saldo de cuenta de cliente, monedero). */
10
+ maxAmount?: number;
11
+ /** Cuentas/cajas destino elegibles (requiresAccount). */
12
+ accounts?: Array<{
13
+ id: string;
14
+ label: string;
15
+ }>;
16
+ }
17
+ export interface PaymentTender {
18
+ key: string;
19
+ method: string;
20
+ amount: number;
21
+ reference?: string;
22
+ account_id?: string;
23
+ }
24
+ export interface PaymentSummary {
25
+ total: number;
26
+ paid: number;
27
+ /** Lo que falta por cobrar (>= 0). */
28
+ remaining: number;
29
+ /** Cambio a entregar: exceso cubierto por efectivo (>= 0). */
30
+ change: number;
31
+ /** Exceso en métodos no-efectivo (no se puede devolver): error. */
32
+ overpaid: number;
33
+ settled: boolean;
34
+ }
35
+ export declare function newTender(method: string, amount?: number): PaymentTender;
36
+ export declare function summarizePayment(total: number, tenders: PaymentTender[], methods: PaymentMethodOption[]): PaymentSummary;
37
+ export interface PaymentValidation {
38
+ /** Errores por renglón de pago `"<índice>.<campo>"`. */
39
+ errors: Record<string, string>;
40
+ /** Error general. */
41
+ form?: string;
42
+ valid: boolean;
43
+ }
44
+ export interface PaymentPolicy {
45
+ /** Permite cobro parcial (anticipo/abono). Default false. */
46
+ allowPartial?: boolean;
47
+ }
48
+ export declare function validatePayment(total: number, tenders: PaymentTender[], methods: PaymentMethodOption[], policy?: PaymentPolicy): PaymentValidation;
49
+ /** Payload hacia el backend: montos reales; el cambio no es un pago. */
50
+ export declare function serializePayment(tenders: PaymentTender[], methods: PaymentMethodOption[], total: number): {
51
+ payments: {
52
+ account_id?: string | undefined;
53
+ reference?: string | undefined;
54
+ method: string;
55
+ amount: number;
56
+ }[];
57
+ paid: number;
58
+ change: number;
59
+ remaining: number;
60
+ };
61
+ //# sourceMappingURL=payment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment.d.ts","sourceRoot":"","sources":["../../src/business/payment.ts"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAClD;AAED,MAAM,WAAW,aAAa;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;CACnB;AAGD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,aAAa,CAGnE;AAED,wBAAgB,gBAAgB,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EAAE,EACxB,OAAO,EAAE,mBAAmB,EAAE,GAC/B,cAAc,CAqBhB;AAED,MAAM,WAAW,iBAAiB;IAC9B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC1B,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,wBAAgB,eAAe,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EAAE,EACxB,OAAO,EAAE,mBAAmB,EAAE,EAC9B,MAAM,GAAE,aAAkB,GAC3B,iBAAiB,CAkBnB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,mBAAmB,EAAE,EAAE,KAAK,EAAE,MAAM;;;;;;;;;;EAcvG"}
@@ -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 === 'credit_note') && (_jsx(Input, { placeholder: a.kind === 'card' ? 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' | '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,QAAQ,GAAG,cAAc,GAAG,aAAa,CAAA;AAE/F,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,EAMhE,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,43 @@
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: 'wallet', label: 'Monedero' },
9
+ { kind: 'store_credit', label: 'Saldo a favor' },
10
+ { kind: 'credit_note', label: 'Nota de crédito (CFDI)' },
11
+ ];
12
+ export function validateRefund(total, allocations, options = DEFAULT_REFUND_DESTINATIONS) {
13
+ const byKind = new Map(options.map((o) => [o.kind, o]));
14
+ const errors = {};
15
+ let allocated = 0;
16
+ const seen = new Set();
17
+ allocations.forEach((a, i) => {
18
+ const o = byKind.get(a.kind);
19
+ const amt = toAmount(a.amount);
20
+ allocated += amt;
21
+ if (!o || o.available === false)
22
+ errors[`${i}.kind`] = o?.disabledReason ?? 'Destino no disponible';
23
+ if (seen.has(a.kind))
24
+ errors[`${i}.kind`] = 'Ese destino ya está en la lista';
25
+ seen.add(a.kind);
26
+ if (amt <= 0)
27
+ errors[`${i}.amount`] = 'Captura un monto mayor a cero';
28
+ else if (o?.maxAmount != null && amt > o.maxAmount)
29
+ errors[`${i}.amount`] = `El máximo para ${o.label} es ${o.maxAmount}`;
30
+ });
31
+ allocated = roundMoney(allocated);
32
+ const remaining = roundMoney(total - allocated);
33
+ let form;
34
+ if (allocations.length === 0)
35
+ form = 'Elige a dónde se devuelve el dinero';
36
+ else if (remaining !== 0)
37
+ form = remaining > 0 ? 'Falta asignar parte del reembolso' : 'La suma excede el monto a reembolsar';
38
+ return { errors, form, allocated, remaining, valid: !form && Object.keys(errors).length === 0 };
39
+ }
40
+ /** Un solo destino cubriendo todo el monto (caso común). */
41
+ export function singleDestination(kind, total) {
42
+ return [{ kind, amount: roundMoney(total) }];
43
+ }
@@ -0,0 +1,10 @@
1
+ /** Búsqueda async con debounce y cancelación. `search` debe ser estable (useCallback). */
2
+ export declare function useAsyncSearch<T>(query: string, search: (q: string, signal: AbortSignal) => Promise<T[]>, opts?: {
3
+ minChars?: number;
4
+ delay?: number;
5
+ }): {
6
+ results: T[];
7
+ loading: boolean;
8
+ error: boolean;
9
+ };
10
+ //# sourceMappingURL=use-async-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-async-search.d.ts","sourceRoot":"","sources":["../../src/business/use-async-search.ts"],"names":[],"mappings":"AAGA,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,CAAC,EAC5B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,EACxD,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAkCpD"}
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useDebouncedValue } from '../use-debounced-value';
3
+ /** Búsqueda async con debounce y cancelación. `search` debe ser estable (useCallback). */
4
+ export function useAsyncSearch(query, search, opts = {}) {
5
+ const { minChars = 2, delay = 250 } = opts;
6
+ const q = useDebouncedValue(query.trim(), delay);
7
+ const [results, setResults] = useState([]);
8
+ const [loading, setLoading] = useState(false);
9
+ const [error, setError] = useState(false);
10
+ useEffect(() => {
11
+ if (q.length < minChars) {
12
+ setResults([]);
13
+ setLoading(false);
14
+ setError(false);
15
+ return;
16
+ }
17
+ const ctrl = new AbortController();
18
+ setLoading(true);
19
+ setError(false);
20
+ search(q, ctrl.signal)
21
+ .then((rows) => {
22
+ if (!ctrl.signal.aborted)
23
+ setResults(rows);
24
+ })
25
+ .catch(() => {
26
+ if (!ctrl.signal.aborted) {
27
+ setResults([]);
28
+ setError(true);
29
+ }
30
+ })
31
+ .finally(() => {
32
+ if (!ctrl.signal.aborted)
33
+ setLoading(false);
34
+ });
35
+ return () => ctrl.abort();
36
+ }, [q, search, minChars]);
37
+ return { results, loading, error };
38
+ }
@@ -0,0 +1,12 @@
1
+ import type { ResolvedAction, ResolvedActionLayout } from './types';
2
+ export interface ActionBarProps {
3
+ layout: ResolvedActionLayout;
4
+ /**
5
+ * Ejecuta la acción. Si devuelve una promesa, el botón muestra `loading`
6
+ * hasta que se resuelve (acciones asíncronas, §6.2).
7
+ */
8
+ onAction: (action: ResolvedAction) => void | Promise<unknown>;
9
+ className?: string;
10
+ }
11
+ export declare function ActionBar({ layout, onAction, className }: ActionBarProps): import("react").JSX.Element;
12
+ //# sourceMappingURL=action-bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-bar.d.ts","sourceRoot":"","sources":["../../src/document/action-bar.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnE,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,oBAAoB,CAAA;IAC5B;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,+BA2GxE"}