@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,128 @@
1
+ // Formateador único de moneda, número y fecha (benchmark PIT-023 / PIT-025).
2
+ //
3
+ // Toda la UI de negocio (toasts, tickets, PDFs, pickers) formatea por aquí para
4
+ // no mezclar locale y moneda (el «MX$1,06.00» del toast de cobro) ni desfasar
5
+ // fechas por zona horaria. La moneda y la zona son config de la org — nunca se
6
+ // hardcodean: salen de <CurrencyContext>/<TimeZoneContext> o de `opts`.
7
+ import { useMemo } from 'react';
8
+ import { useCurrency, useTimeZone } from '../org-runtime-context';
9
+ /** Locale por defecto del producto. */
10
+ export const DEFAULT_LOCALE = 'es-MX';
11
+ /** Moneda cuando ni la org ni el llamador dan una (mismo fallback que las celdas). */
12
+ export const FALLBACK_CURRENCY = 'USD';
13
+ /** Redondea a centavos evitando el error binario (1.005 → 1.01). */
14
+ export function roundMoney(n, decimals = 2) {
15
+ if (!Number.isFinite(n))
16
+ return 0;
17
+ const f = 10 ** decimals;
18
+ return Math.round((n + Number.EPSILON) * f) / f;
19
+ }
20
+ /** Número (string con símbolo/separadores, o number) → number; NaN → 0. */
21
+ export function toAmount(v) {
22
+ if (typeof v === 'number')
23
+ return Number.isFinite(v) ? v : 0;
24
+ if (typeof v !== 'string')
25
+ return 0;
26
+ const s = v.replace(/[^0-9.,-]/g, '');
27
+ if (!s)
28
+ return 0;
29
+ const lastComma = s.lastIndexOf(',');
30
+ const lastDot = s.lastIndexOf('.');
31
+ const normalized = lastComma > lastDot ? s.replace(/\./g, '').replace(',', '.') : s.replace(/,/g, '');
32
+ const n = Number(normalized);
33
+ return Number.isFinite(n) ? n : 0;
34
+ }
35
+ /**
36
+ * Monto con símbolo de la moneda de la org: `formatMoney(1060, {currency:'MXN'})`
37
+ * → `$1,060.00`. Un valor no numérico devuelve `''` (nunca «NaN»).
38
+ */
39
+ export function formatMoney(amount, opts = {}) {
40
+ const n = typeof amount === 'number' ? amount : toAmount(amount);
41
+ if (!Number.isFinite(n))
42
+ return '';
43
+ const currency = opts.currency || FALLBACK_CURRENCY;
44
+ try {
45
+ return new Intl.NumberFormat(opts.locale || DEFAULT_LOCALE, {
46
+ style: 'currency',
47
+ currency,
48
+ currencyDisplay: 'narrowSymbol',
49
+ }).format(n);
50
+ }
51
+ catch {
52
+ return `${currency} ${n.toFixed(2)}`;
53
+ }
54
+ }
55
+ /** Número plano localizado (`1,234.5`). */
56
+ export function formatQuantity(n, opts = {}) {
57
+ const v = typeof n === 'number' ? n : toAmount(n);
58
+ if (!Number.isFinite(v))
59
+ return '';
60
+ return new Intl.NumberFormat(opts.locale || DEFAULT_LOCALE, {
61
+ maximumFractionDigits: opts.maximumFractionDigits ?? 3,
62
+ }).format(v);
63
+ }
64
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
65
+ function toDate(input) {
66
+ if (input == null || input === '')
67
+ return null;
68
+ if (input instanceof Date)
69
+ return Number.isNaN(input.getTime()) ? null : { date: input, dateOnly: false };
70
+ if (typeof input === 'string' || typeof input === 'number') {
71
+ const dateOnly = typeof input === 'string' && DATE_ONLY.test(input);
72
+ const d = new Date(input);
73
+ return Number.isNaN(d.getTime()) ? null : { date: d, dateOnly };
74
+ }
75
+ return null;
76
+ }
77
+ /**
78
+ * Fecha (`29 sep 2026`). Un `YYYY-MM-DD` es fecha de calendario: se formatea en
79
+ * UTC para que no retroceda un día en zonas con offset negativo.
80
+ */
81
+ export function formatDate(input, opts = {}) {
82
+ const parsed = toDate(input);
83
+ if (!parsed)
84
+ return '';
85
+ return new Intl.DateTimeFormat(opts.locale || DEFAULT_LOCALE, {
86
+ dateStyle: opts.style ?? 'medium',
87
+ timeZone: parsed.dateOnly ? 'UTC' : opts.timeZone || undefined,
88
+ }).format(parsed.date);
89
+ }
90
+ /** Fecha y hora en la zona de la org (`29 sep 2026, 14:05`). */
91
+ export function formatDateTime(input, opts = {}) {
92
+ const parsed = toDate(input);
93
+ if (!parsed)
94
+ return '';
95
+ if (parsed.dateOnly)
96
+ return formatDate(input, opts);
97
+ return new Intl.DateTimeFormat(opts.locale || DEFAULT_LOCALE, {
98
+ dateStyle: 'medium',
99
+ timeStyle: 'short',
100
+ timeZone: opts.timeZone || undefined,
101
+ }).format(parsed.date);
102
+ }
103
+ /** Formateador ligado a un locale/moneda/zona fijos (para tickets y PDFs fuera de React). */
104
+ export function createFormatter(opts = {}) {
105
+ const locale = opts.locale || DEFAULT_LOCALE;
106
+ const currency = opts.currency || FALLBACK_CURRENCY;
107
+ const timeZone = opts.timeZone || undefined;
108
+ return {
109
+ locale,
110
+ currency,
111
+ timeZone,
112
+ money: (amount, c) => formatMoney(amount, { locale, currency: c || currency }),
113
+ quantity: (n) => formatQuantity(n, { locale }),
114
+ date: (input) => formatDate(input, { locale, timeZone }),
115
+ dateTime: (input) => formatDateTime(input, { locale, timeZone }),
116
+ };
117
+ }
118
+ /** Formateador con la moneda y zona de la org tomadas del runtime provider. */
119
+ export function useFormatter(overrides = {}) {
120
+ const orgCurrency = useCurrency();
121
+ const orgZone = useTimeZone();
122
+ const { locale, currency, timeZone } = overrides;
123
+ return useMemo(() => createFormatter({
124
+ locale,
125
+ currency: currency ?? orgCurrency,
126
+ timeZone: timeZone ?? orgZone,
127
+ }), [locale, currency, timeZone, orgCurrency, orgZone]);
128
+ }
@@ -0,0 +1,16 @@
1
+ export * from './format';
2
+ export * from './field-errors';
3
+ export * from './feedback';
4
+ export * from './line-items';
5
+ export * from './line-items-editor';
6
+ export * from './payment';
7
+ export * from './payment-capture';
8
+ export * from './refund';
9
+ export * from './refund-destination';
10
+ export * from './product-search';
11
+ export * from './product-picker';
12
+ export * from './customer-picker';
13
+ export * from './catalog';
14
+ export * from './return';
15
+ export * from './return-wizard';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA"}
@@ -0,0 +1,15 @@
1
+ export * from './format';
2
+ export * from './field-errors';
3
+ export * from './feedback';
4
+ export * from './line-items';
5
+ export * from './line-items-editor';
6
+ export * from './payment';
7
+ export * from './payment-capture';
8
+ export * from './refund';
9
+ export * from './refund-destination';
10
+ export * from './product-search';
11
+ export * from './product-picker';
12
+ export * from './customer-picker';
13
+ export * from './catalog';
14
+ export * from './return';
15
+ export * from './return-wizard';
@@ -0,0 +1,25 @@
1
+ import { type LineItem, type LineItemsPolicy, type LineItemsValidation } from './line-items';
2
+ export type LineItemsColumn = 'discount' | 'tax' | 'technician' | 'lot' | 'dot';
3
+ export interface LineItemsEditorProps {
4
+ value: LineItem[];
5
+ /** Se emite con cada cambio; las líneas siempre traen `key`. */
6
+ onChange: (lines: LineItem[]) => void;
7
+ /** Columnas opcionales visibles. Default: `['discount']`. */
8
+ columns?: LineItemsColumn[];
9
+ policy?: LineItemsPolicy;
10
+ /** Errores externos del servidor `"<índice>.<campo>"` (de `mapApiError().fields`). */
11
+ serverErrors?: Record<string, string>;
12
+ /** Se emite tras cada cambio con el resultado de validar (para bloquear «Guardar»). */
13
+ onValidate?: (v: LineItemsValidation) => void;
14
+ /** Slot: abre el ProductPicker; al elegir, el llamador hace `onChange([...value, makeLine(...)])`. */
15
+ onRequestProduct?: () => void;
16
+ /** Permite secciones y notas (Odoo-style). Default true. */
17
+ allowSections?: boolean;
18
+ readOnly?: boolean;
19
+ /** Permiso requerido para editar (default: sin gate). Sin él → solo lectura. */
20
+ editPermission?: string;
21
+ /** Moneda ISO (default: la de la org). */
22
+ currency?: string;
23
+ }
24
+ export declare function LineItemsEditor({ value, onChange, columns, policy, serverErrors, onValidate, onRequestProduct, allowSections, readOnly, editPermission, currency, }: LineItemsEditorProps): import("react").JSX.Element;
25
+ //# sourceMappingURL=line-items-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"AAQA,OAAO,EAKH,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAC3B,MAAM,cAAc,CAAA;AAErB,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,KAAK,GAAG,YAAY,GAAG,KAAK,GAAG,KAAK,CAAA;AAE/E,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,gEAAgE;IAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAA;IACrC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAC7C,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,4DAA4D;IAC5D,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAID,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,OAAsB,EACtB,MAAM,EACN,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,aAAoB,EACpB,QAAgB,EAChB,cAAc,EACd,QAAQ,GACX,EAAE,oBAAoB,+BA0MtB"}
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // LineItemsEditor — editor de renglones compartido (benchmark §7, COT-1/PIT-018).
3
+ // Controlado: `value`/`onChange` con `LineItem[]`. Guardar = `serializeLineItems`.
4
+ import { useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Plus, Trash2, StickyNote, Heading } from 'lucide-react';
7
+ import { Button, Input } from '@asteby/metacore-ui';
8
+ import { useCan } from '../permissions-context';
9
+ import { useFormatter } from './format';
10
+ import { computeLine, computeTotals, makeLine, validateLineItems, } from './line-items';
11
+ const num = (s) => (s.trim() === '' ? 0 : Number(s.replace(',', '.')) || 0);
12
+ export function LineItemsEditor({ value, onChange, columns = ['discount'], policy, serverErrors, onValidate, onRequestProduct, allowSections = true, readOnly = false, editPermission, currency, }) {
13
+ const { t } = useTranslation();
14
+ const can = useCan();
15
+ const fmt = useFormatter({ currency });
16
+ const locked = readOnly || (editPermission ? !can(editPermission) : false);
17
+ const validation = useMemo(() => validateLineItems(value, policy), [value, policy]);
18
+ const totals = useMemo(() => computeTotals(value), [value]);
19
+ const errors = { ...validation.errors, ...serverErrors };
20
+ const show = (c) => columns.includes(c);
21
+ const emit = (next) => {
22
+ onChange(next);
23
+ onValidate?.(validateLineItems(next, policy));
24
+ };
25
+ const patch = (idx, p) => emit(value.map((l, i) => (i === idx ? { ...l, ...p } : l)));
26
+ const remove = (idx) => emit(value.filter((_, i) => i !== idx));
27
+ const cell = (i, field) => errors[`${i}.${field}`];
28
+ const warn = (i, field) => validation.warnings[`${i}.${field}`];
29
+ return (_jsxs("div", { "data-slot": "line-items-editor", className: "space-y-2", children: [_jsx("div", { className: "overflow-x-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-2 py-2", children: t('lineItems.description', { defaultValue: 'Descripción' }) }), _jsx("th", { className: "w-24 px-2 py-2 text-right", children: t('lineItems.quantity', { defaultValue: 'Cant.' }) }), _jsx("th", { className: "w-32 px-2 py-2 text-right", children: t('lineItems.unitPrice', { defaultValue: 'Precio' }) }), show('discount') && _jsx("th", { className: "w-20 px-2 py-2 text-right", children: t('lineItems.discount', { defaultValue: 'Desc. %' }) }), show('tax') && _jsx("th", { className: "w-20 px-2 py-2 text-right", children: t('lineItems.tax', { defaultValue: 'IVA %' }) }), show('technician') && _jsx("th", { className: "w-32 px-2 py-2", children: t('lineItems.technician', { defaultValue: 'Técnico' }) }), show('lot') && _jsx("th", { className: "w-28 px-2 py-2", children: t('lineItems.lot', { defaultValue: 'Lote' }) }), show('dot') && _jsx("th", { className: "w-24 px-2 py-2", children: "DOT" }), _jsx("th", { className: "w-32 px-2 py-2 text-right", children: t('lineItems.total', { defaultValue: 'Importe' }) }), _jsx("th", { className: "w-10" })] }) }), _jsxs("tbody", { children: [value.map((l, i) => {
30
+ if (l.kind !== 'item') {
31
+ return (_jsx("tr", { className: "border-t bg-muted/30", children: _jsxs("td", { colSpan: 99, className: "px-2 py-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-invalid": !!cell(i, 'description'), placeholder: l.kind === 'section' ? t('lineItems.sectionPlaceholder', { defaultValue: 'Sección' }) : t('lineItems.notePlaceholder', { defaultValue: 'Nota' }), className: l.kind === 'section' ? 'font-semibold' : 'italic', onChange: (e) => patch(i, { description: e.target.value }) }), !locked && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) }))] }), cell(i, 'description') && _jsx("p", { className: "mt-1 text-xs text-destructive", children: cell(i, 'description') })] }) }, l.key));
32
+ }
33
+ const a = computeLine(l);
34
+ return (_jsxs("tr", { className: "border-t align-top", children: [_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-invalid": !!cell(i, 'description'), onChange: (e) => patch(i, { description: e.target.value }) }), l.sku && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: l.sku }), cell(i, 'description') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'description') })] }), _jsxs("td", { className: "px-2 py-1.5", children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(l.quantity), disabled: locked, "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => patch(i, { quantity: num(e.target.value) }) }), cell(i, 'quantity') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'quantity') }), !cell(i, 'quantity') && warn(i, 'quantity') && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: warn(i, 'quantity') })] }), _jsxs("td", { className: "px-2 py-1.5", children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(l.unit_price), disabled: locked, "aria-invalid": !!cell(i, 'unit_price'), onChange: (e) => patch(i, { unit_price: num(e.target.value) }) }), cell(i, 'unit_price') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'unit_price') })] }), show('discount') && (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(l.discount), disabled: locked, "aria-invalid": !!cell(i, 'discount'), onChange: (e) => patch(i, { discount: num(e.target.value) }) }), cell(i, 'discount') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'discount') })] })), show('tax') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { inputMode: "decimal", className: "text-right", value: String(Math.round(l.tax_rate * 10000) / 100), disabled: locked, onChange: (e) => patch(i, { tax_rate: num(e.target.value) / 100 }) }) })), show('technician') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.technician_id ?? '', disabled: locked, onChange: (e) => patch(i, { technician_id: e.target.value }) }) })), show('lot') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.lot ?? '', disabled: locked, onChange: (e) => patch(i, { lot: e.target.value }) }) })), show('dot') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.dot ?? '', disabled: locked, onChange: (e) => patch(i, { dot: e.target.value }) }) })), _jsx("td", { className: "px-2 py-2 text-right tabular-nums", children: fmt.money(a.net) }), _jsx("td", { className: "px-1 py-1.5", children: !locked && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) })) })] }, l.key));
35
+ }), value.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 99, className: "px-3 py-6 text-center text-muted-foreground", children: t('lineItems.empty', { defaultValue: 'Aún no hay renglones. Agrega un producto o un renglón libre.' }) }) }))] })] }) }), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form }), _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [!locked ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [onRequestProduct && (_jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: onRequestProduct, children: [_jsx(Plus, { className: "mr-1 size-4" }), t('lineItems.addProduct', { defaultValue: 'Agregar producto' })] })), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: () => emit([...value, makeLine()]), children: [_jsx(Plus, { className: "mr-1 size-4" }), t('lineItems.addLine', { defaultValue: 'Renglón libre' })] }), allowSections && (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, makeLine({ kind: 'section' })]), children: [_jsx(Heading, { className: "mr-1 size-4" }), t('lineItems.addSection', { defaultValue: 'Sección' })] }), _jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, makeLine({ kind: 'note' })]), children: [_jsx(StickyNote, { className: "mr-1 size-4" }), t('lineItems.addNote', { defaultValue: 'Nota' })] })] }))] })) : (_jsx("span", {})), _jsxs("dl", { className: "min-w-56 space-y-0.5 text-sm tabular-nums", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.subtotal', { defaultValue: 'Subtotal' }) }), _jsx("dd", { children: fmt.money(totals.subtotal) })] }), totals.discount > 0 && _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.discountTotal', { defaultValue: 'Descuento' }) }), _jsxs("dd", { children: ["-", fmt.money(totals.discount)] })] }), totals.tax > 0 && _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.taxTotal', { defaultValue: 'Impuestos' }) }), _jsx("dd", { children: fmt.money(totals.tax) })] }), _jsxs("div", { className: "flex justify-between gap-6 font-semibold", children: [_jsx("dt", { children: t('lineItems.total', { defaultValue: 'Total' }) }), _jsx("dd", { children: fmt.money(totals.total) })] })] })] })] }));
36
+ }
@@ -0,0 +1,69 @@
1
+ export type LineItemKind = 'item' | 'section' | 'note';
2
+ export interface LineItem {
3
+ /** Clave estable de UI (no se envía al backend). */
4
+ key: string;
5
+ kind: LineItemKind;
6
+ /** Id del producto/variante en el catálogo (kind=item). */
7
+ product_id?: string;
8
+ sku?: string;
9
+ description: string;
10
+ quantity: number;
11
+ unit_price: number;
12
+ /** Descuento en porcentaje 0-100. */
13
+ discount: number;
14
+ /** Tasa de impuesto como fracción (0.16). Viene de la config fiscal de la org. */
15
+ tax_rate: number;
16
+ technician_id?: string;
17
+ lot?: string;
18
+ dot?: string;
19
+ /** Existencia disponible conocida (para la política de sobreventa). */
20
+ available?: number;
21
+ }
22
+ /** Qué hacer si `quantity > available`. */
23
+ export type StockPolicy = 'allow' | 'warn' | 'block';
24
+ export interface LineItemsPolicy {
25
+ /** Default `warn`. */
26
+ stockPolicy?: StockPolicy;
27
+ /** Permite cantidades/precios negativos (notas de crédito). Default false. */
28
+ allowNegative?: boolean;
29
+ /** Exige al menos un renglón `item`. Default true. */
30
+ requireItems?: boolean;
31
+ }
32
+ /** Clave de UI única en el proceso. */
33
+ export declare function newLineKey(): string;
34
+ export declare function makeLine(partial?: Partial<LineItem>): LineItem;
35
+ export interface LineAmounts {
36
+ gross: number;
37
+ discount: number;
38
+ net: number;
39
+ tax: number;
40
+ total: number;
41
+ }
42
+ export declare function computeLine(l: Pick<LineItem, 'kind' | 'quantity' | 'unit_price' | 'discount' | 'tax_rate'>): LineAmounts;
43
+ export interface LineItemsTotals {
44
+ subtotal: number;
45
+ discount: number;
46
+ tax: number;
47
+ total: number;
48
+ /** Suma de cantidades de los renglones `item`. */
49
+ units: number;
50
+ }
51
+ export declare function computeTotals(lines: LineItem[]): LineItemsTotals;
52
+ export interface LineItemsValidation {
53
+ /** Errores por celda `"<índice>.<campo>"` (mismo formato que DynamicLineItems). */
54
+ errors: Record<string, string>;
55
+ /** Avisos no bloqueantes (sobreventa con política `warn`). */
56
+ warnings: Record<string, string>;
57
+ /** Error de documento (sin renglones). */
58
+ form?: string;
59
+ valid: boolean;
60
+ }
61
+ export declare function validateLineItems(lines: LineItem[], policy?: LineItemsPolicy): LineItemsValidation;
62
+ /**
63
+ * Forma que se manda al backend: números reales (nunca `""`), sin claves de UI,
64
+ * opcionales vacíos omitidos y `position` 1-based conservando el orden.
65
+ */
66
+ export declare function serializeLineItems(lines: LineItem[]): Array<Record<string, unknown>>;
67
+ /** Inversa de `serializeLineItems`: filas del API → líneas editables (defaults seguros). */
68
+ export declare function parseLineItems(rows: unknown): LineItem[];
69
+ //# sourceMappingURL=line-items.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAEtD,MAAM,WAAW,QAAQ;IACrB,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,YAAY,CAAA;IAClB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,2CAA2C;AAC3C,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;AAEpD,MAAM,WAAW,eAAe;IAC5B,sBAAsB;IACtB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,sDAAsD;IACtD,YAAY,CAAC,EAAE,OAAO,CAAA;CACzB;AAGD,uCAAuC;AACvC,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,OAAO,CAAC,QAAQ,CAAM,GAAG,QAAQ,CAWlE;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,GAAG,UAAU,CAAC,GAAG,WAAW,CAOxH;AAED,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CAehE;AAED,MAAM,WAAW,mBAAmB;IAChC,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,GAAE,eAAoB,GAAG,mBAAmB,CA2BtG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAmBpF;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,CAoBxD"}
@@ -0,0 +1,133 @@
1
+ // Renglones de documento (cotización, pedido, factura, OC, recepción, OT, RMA, NC).
2
+ // Lógica pura del <LineItemsEditor>: cálculo, validación y serialización. El
3
+ // guardado de renglones fallaba (PIT-018) por mandar al API cadenas vacías y
4
+ // claves de UI; `serializeLineItems` es la única puerta de salida hacia el backend.
5
+ import { roundMoney, toAmount } from './format';
6
+ let seq = 0;
7
+ /** Clave de UI única en el proceso. */
8
+ export function newLineKey() {
9
+ seq += 1;
10
+ return `li_${Date.now().toString(36)}_${seq}`;
11
+ }
12
+ export function makeLine(partial = {}) {
13
+ return {
14
+ key: partial.key ?? newLineKey(),
15
+ kind: 'item',
16
+ description: '',
17
+ quantity: 1,
18
+ unit_price: 0,
19
+ discount: 0,
20
+ tax_rate: 0,
21
+ ...partial,
22
+ };
23
+ }
24
+ export function computeLine(l) {
25
+ if (l.kind !== 'item')
26
+ return { gross: 0, discount: 0, net: 0, tax: 0, total: 0 };
27
+ const gross = roundMoney(toAmount(l.quantity) * toAmount(l.unit_price));
28
+ const discount = roundMoney(gross * (toAmount(l.discount) / 100));
29
+ const net = roundMoney(gross - discount);
30
+ const tax = roundMoney(net * toAmount(l.tax_rate));
31
+ return { gross, discount, net, tax, total: roundMoney(net + tax) };
32
+ }
33
+ export function computeTotals(lines) {
34
+ const t = { subtotal: 0, discount: 0, tax: 0, total: 0, units: 0 };
35
+ for (const l of lines) {
36
+ if (l.kind !== 'item')
37
+ continue;
38
+ const a = computeLine(l);
39
+ t.subtotal += a.gross;
40
+ t.discount += a.discount;
41
+ t.tax += a.tax;
42
+ t.units += toAmount(l.quantity);
43
+ }
44
+ t.subtotal = roundMoney(t.subtotal);
45
+ t.discount = roundMoney(t.discount);
46
+ t.tax = roundMoney(t.tax);
47
+ t.total = roundMoney(t.subtotal - t.discount + t.tax);
48
+ return t;
49
+ }
50
+ export function validateLineItems(lines, policy = {}) {
51
+ const { stockPolicy = 'warn', allowNegative = false, requireItems = true } = policy;
52
+ const errors = {};
53
+ const warnings = {};
54
+ let items = 0;
55
+ lines.forEach((l, i) => {
56
+ if (l.kind === 'section' || l.kind === 'note') {
57
+ if (!l.description.trim())
58
+ errors[`${i}.description`] = 'Escribe el texto';
59
+ return;
60
+ }
61
+ items += 1;
62
+ const qty = toAmount(l.quantity);
63
+ const price = toAmount(l.unit_price);
64
+ const disc = toAmount(l.discount);
65
+ if (!l.description.trim() && !l.product_id)
66
+ errors[`${i}.description`] = 'Elige un producto o escribe una descripción';
67
+ if (qty === 0)
68
+ errors[`${i}.quantity`] = 'La cantidad debe ser distinta de cero';
69
+ else if (qty < 0 && !allowNegative)
70
+ errors[`${i}.quantity`] = 'La cantidad no puede ser negativa';
71
+ if (price < 0 && !allowNegative)
72
+ errors[`${i}.unit_price`] = 'El precio no puede ser negativo';
73
+ if (disc < 0 || disc > 100)
74
+ errors[`${i}.discount`] = 'El descuento debe estar entre 0 y 100%';
75
+ if (l.available != null && qty > l.available && stockPolicy !== 'allow') {
76
+ const msg = `Solo hay ${l.available} disponibles`;
77
+ if (stockPolicy === 'block')
78
+ errors[`${i}.quantity`] ??= msg;
79
+ else
80
+ warnings[`${i}.quantity`] = msg;
81
+ }
82
+ });
83
+ const form = requireItems && items === 0 ? 'Agrega al menos un renglón' : undefined;
84
+ return { errors, warnings, form, valid: !form && Object.keys(errors).length === 0 };
85
+ }
86
+ /**
87
+ * Forma que se manda al backend: números reales (nunca `""`), sin claves de UI,
88
+ * opcionales vacíos omitidos y `position` 1-based conservando el orden.
89
+ */
90
+ export function serializeLineItems(lines) {
91
+ return lines.map((l, i) => {
92
+ const out = {
93
+ position: i + 1,
94
+ kind: l.kind,
95
+ description: l.description.trim(),
96
+ };
97
+ if (l.kind === 'item') {
98
+ out.quantity = toAmount(l.quantity);
99
+ out.unit_price = toAmount(l.unit_price);
100
+ out.discount = toAmount(l.discount);
101
+ out.tax_rate = toAmount(l.tax_rate);
102
+ for (const k of ['product_id', 'sku', 'technician_id', 'lot', 'dot']) {
103
+ const v = l[k];
104
+ if (typeof v === 'string' && v.trim())
105
+ out[k] = v.trim();
106
+ }
107
+ }
108
+ return out;
109
+ });
110
+ }
111
+ /** Inversa de `serializeLineItems`: filas del API → líneas editables (defaults seguros). */
112
+ export function parseLineItems(rows) {
113
+ if (!Array.isArray(rows))
114
+ return [];
115
+ return rows.map((r) => {
116
+ const o = (r ?? {});
117
+ const kind = o.kind === 'section' || o.kind === 'note' ? o.kind : 'item';
118
+ const str = (v) => (typeof v === 'string' && v ? v : undefined);
119
+ return makeLine({
120
+ kind,
121
+ product_id: str(o.product_id),
122
+ sku: str(o.sku),
123
+ description: typeof o.description === 'string' ? o.description : '',
124
+ quantity: o.quantity == null ? 1 : toAmount(o.quantity),
125
+ unit_price: toAmount(o.unit_price),
126
+ discount: toAmount(o.discount),
127
+ tax_rate: toAmount(o.tax_rate),
128
+ technician_id: str(o.technician_id),
129
+ lot: str(o.lot),
130
+ dot: str(o.dot),
131
+ });
132
+ });
133
+ }
@@ -0,0 +1,21 @@
1
+ import { serializePayment, type PaymentMethodOption, type PaymentPolicy, type PaymentTender } from './payment';
2
+ export interface PaymentCaptureProps {
3
+ /** Total a cobrar. */
4
+ total: number;
5
+ methods: PaymentMethodOption[];
6
+ value: PaymentTender[];
7
+ onChange: (tenders: PaymentTender[]) => void;
8
+ /** Confirmar cobro: recibe el payload ya serializado. Solo se llama si es válido. */
9
+ onSubmit?: (payload: ReturnType<typeof serializePayment>) => void;
10
+ policy?: PaymentPolicy;
11
+ /** Errores del servidor `"<índice>.<campo>"`. */
12
+ serverErrors?: Record<string, string>;
13
+ submitLabel?: string;
14
+ submitting?: boolean;
15
+ /** Capability requerida para cobrar (p. ej. `pos_orders.pagar`). Sin ella, botón oculto. */
16
+ submitPermission?: string;
17
+ currency?: string;
18
+ disabled?: boolean;
19
+ }
20
+ export declare function PaymentCapture({ total, methods, value, onChange, onSubmit, policy, serverErrors, submitLabel, submitting, submitPermission, currency, disabled, }: PaymentCaptureProps): import("react").JSX.Element;
21
+ //# sourceMappingURL=payment-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-capture.d.ts","sourceRoot":"","sources":["../../src/business/payment-capture.tsx"],"names":[],"mappings":"AAOA,OAAO,EAEH,gBAAgB,EAGhB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,aAAa,EACrB,MAAM,WAAW,CAAA;AAElB,MAAM,WAAW,mBAAmB;IAChC,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,mBAAmB,EAAE,CAAA;IAC9B,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;IAC5C,qFAAqF;IACrF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,KAAK,IAAI,CAAA;IACjE,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,QAAQ,EACR,QAAQ,GACX,EAAE,mBAAmB,+BA4GrB"}
@@ -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"}