@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,112 @@
1
+ // Devolución de mercancía (ReturnWizard: POS, Pedido, Factura, RMA, OT, devolución
2
+ // a proveedor). Lógica pura, sin endpoints: cada consumidor cablea el envío.
3
+ // Flujo (benchmark §8.2): renglones → condición y destino → recepción → reembolso o NC.
4
+ import { roundMoney, toAmount } from './format';
5
+ import { validateRefund } from './refund';
6
+ export const RETURN_CONDITION_DESTINATIONS = {
7
+ sellable: ['stock'],
8
+ defective: ['scrap', 'warranty'],
9
+ };
10
+ /** Destino por defecto de una condición (vendible → stock, defectuoso → merma). */
11
+ export function defaultStockDestination(condition) {
12
+ return RETURN_CONDITION_DESTINATIONS[condition][0];
13
+ }
14
+ export function returnableQty(l) {
15
+ return Math.max(0, roundMoney(l.sold - (l.alreadyReturned ?? 0), 4));
16
+ }
17
+ export function computeReturnTotals(lines, choices) {
18
+ let base = 0;
19
+ let tax = 0;
20
+ let goodsLines = 0;
21
+ for (const l of lines) {
22
+ const c = choices[l.key];
23
+ const q = c ? toAmount(c.quantity) : 0;
24
+ if (q <= 0)
25
+ continue;
26
+ goodsLines += 1;
27
+ const b = roundMoney(q * l.unit_price);
28
+ base += b;
29
+ tax += roundMoney(b * (l.tax_rate ?? 0));
30
+ }
31
+ base = roundMoney(base);
32
+ tax = roundMoney(tax);
33
+ return { base, tax, total: roundMoney(base + tax), goodsLines };
34
+ }
35
+ /**
36
+ * Relación SAT (c_TipoRelacion) de la NC: 03 «devolución de mercancía» si regresan
37
+ * renglones con producto; 01 «nota de crédito de los documentos relacionados» si es
38
+ * solo ajuste de importe. Misma regla que aplica el backend al proyectar la NC.
39
+ */
40
+ export function creditNoteRelation(lines, choices) {
41
+ return lines.some((l) => !!l.product_id && toAmount(choices[l.key]?.quantity) > 0) ? '03' : '01';
42
+ }
43
+ export function validateReturnChoices(lines, choices, reason, requireReason = true) {
44
+ const errors = {};
45
+ let any = false;
46
+ for (const l of lines) {
47
+ const c = choices[l.key];
48
+ const q = c ? toAmount(c.quantity) : 0;
49
+ if (q === 0)
50
+ continue;
51
+ if (q < 0)
52
+ errors[`${l.key}.quantity`] = 'La cantidad no puede ser negativa';
53
+ else if (q > returnableQty(l))
54
+ errors[`${l.key}.quantity`] = `Máximo ${returnableQty(l)} por devolver`;
55
+ else
56
+ any = true;
57
+ if (c && !RETURN_CONDITION_DESTINATIONS[c.condition]?.includes(c.destination)) {
58
+ errors[`${l.key}.destination`] = 'Ese destino no aplica a la condición elegida';
59
+ }
60
+ }
61
+ let form;
62
+ if (!any && Object.keys(errors).length === 0)
63
+ form = 'Elige al menos un renglón a devolver';
64
+ else if (requireReason && !(reason ?? '').trim())
65
+ form = 'Indica el motivo de la devolución';
66
+ return { errors, form, valid: !form && Object.keys(errors).length === 0 };
67
+ }
68
+ export function returnSteps(opts) {
69
+ // Con almacén, la recepción la confirma almacén fuera del asistente: no es paso.
70
+ return opts.warehouseConnected ? ['lines', 'condition', 'refund'] : ['lines', 'condition', 'receive', 'refund'];
71
+ }
72
+ /** Única puerta de salida hacia el backend: solo renglones con cantidad > 0. */
73
+ export function serializeReturn(args) {
74
+ const lines = args.lines
75
+ .filter((l) => toAmount(args.choices[l.key]?.quantity) > 0)
76
+ .map((l) => {
77
+ const c = args.choices[l.key];
78
+ return { key: l.key, ...(l.product_id ? { product_id: l.product_id } : {}), quantity: toAmount(c.quantity), condition: c.condition, destination: c.destination };
79
+ });
80
+ const notes = (args.notes ?? '').trim();
81
+ return {
82
+ reason: args.reason.trim(),
83
+ ...(notes ? { notes } : {}),
84
+ received: args.received,
85
+ lines,
86
+ refund: args.refund.map((a) => ({ ...a, amount: roundMoney(a.amount) })),
87
+ ...(args.refund.some((a) => a.kind === 'credit_note') ? { credit_note: { relation: creditNoteRelation(args.lines, args.choices) } } : {}),
88
+ totals: computeReturnTotals(args.lines, args.choices),
89
+ };
90
+ }
91
+ /** Valida el paso de reembolso contra el total de la devolución. */
92
+ export function validateReturnRefund(total, refund, options) {
93
+ return validateRefund(total, refund, options);
94
+ }
95
+ export const RMA_TRANSITIONS = {
96
+ draft: ['authorized', 'rejected', 'cancelled'],
97
+ authorized: ['received', 'rejected', 'cancelled'],
98
+ received: ['settled', 'cancelled'],
99
+ settled: ['closed'],
100
+ closed: [],
101
+ rejected: [],
102
+ cancelled: [],
103
+ };
104
+ /** Estados a los que se llega con motivo obligatorio. */
105
+ export const RMA_REASON_REQUIRED = ['rejected', 'cancelled'];
106
+ export function canTransitionRma(from, to, reason) {
107
+ if (!RMA_TRANSITIONS[from]?.includes(to))
108
+ return { ok: false, error: `No se puede pasar de ${from} a ${to}` };
109
+ if (RMA_REASON_REQUIRED.includes(to) && !(reason ?? '').trim())
110
+ return { ok: false, error: 'Indica el motivo' };
111
+ return { ok: true };
112
+ }
@@ -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"}
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // ActionBar — barra de acciones de un documento (benchmark §5.4 / §6.2):
3
+ // una primaria (verbo + sustantivo), 2–3 secundarias visibles, el resto en
4
+ // «Más…» y las destructivas separadas (divisor) y en rojo. Una acción bloqueada se ve
5
+ // deshabilitada con su motivo (no desaparece: el operador entiende por qué).
6
+ import { useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Loader2, MoreHorizontal } from 'lucide-react';
9
+ import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@asteby/metacore-ui/primitives';
10
+ import { cn } from '@asteby/metacore-ui/lib';
11
+ import { DynamicIcon } from '../dynamic-icon';
12
+ export function ActionBar({ layout, onAction, className }) {
13
+ const { t } = useTranslation();
14
+ const [busy, setBusy] = useState(null);
15
+ const run = async (a) => {
16
+ if (a.blockedReason || busy)
17
+ return;
18
+ setBusy(a.def.key);
19
+ try {
20
+ await onAction(a);
21
+ }
22
+ finally {
23
+ setBusy(null);
24
+ }
25
+ };
26
+ const label = (a) => t(a.def.label, { defaultValue: a.def.label });
27
+ const { primary, secondary, more, destructive } = layout;
28
+ const hasMenu = more.length > 0;
29
+ const button = (a, variant) => {
30
+ const loading = busy === a.def.key;
31
+ const btn = (_jsxs(Button, { type: 'button', size: 'sm', variant: variant === 'destructive' ? 'outline' : variant, disabled: !!a.blockedReason || (!!busy && !loading), "aria-disabled": !!a.blockedReason, "data-action": a.def.key, onClick: () => run(a), className: cn(variant === 'destructive' &&
32
+ 'border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive'), children: [loading ? (_jsx(Loader2, { className: 'mr-1.5 h-4 w-4 animate-spin' })) : a.def.icon ? (_jsx("span", { className: 'mr-1.5 inline-flex h-4 w-4 items-center', children: _jsx(DynamicIcon, { name: a.def.icon, className: 'h-4 w-4' }) })) : null, label(a)] }, a.def.key));
33
+ if (!a.blockedReason)
34
+ return btn;
35
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { tabIndex: 0, children: btn }) }), _jsx(TooltipContent, { className: 'max-w-xs', children: t(a.blockedReason, { defaultValue: a.blockedReason }) })] }, a.def.key));
36
+ };
37
+ const item = (a) => (_jsxs(DropdownMenuItem, { disabled: !!a.blockedReason, "data-action": a.def.key, onSelect: () => run(a), children: [a.def.icon ? (_jsx("span", { className: 'mr-2 inline-flex h-4 w-4 items-center', children: _jsx(DynamicIcon, { name: a.def.icon, className: 'h-4 w-4' }) })) : null, _jsxs("span", { className: 'flex flex-col', children: [label(a), a.blockedReason ? (_jsx("span", { className: 'text-xs font-normal text-muted-foreground', children: t(a.blockedReason, { defaultValue: a.blockedReason }) })) : null] })] }, a.def.key));
38
+ return (_jsx(TooltipProvider, { delayDuration: 150, children: _jsxs("div", { className: cn('flex flex-wrap items-center gap-2', className), "data-document-actions": '', children: [primary ? button(primary, primary.def.destructive ? 'destructive' : 'default') : null, secondary.map((a) => button(a, 'outline')), hasMenu ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: 'button', size: 'sm', variant: 'outline', "aria-label": t('document.more', { defaultValue: 'Más…' }), children: [_jsx(MoreHorizontal, { className: 'mr-1.5 h-4 w-4' }), t('document.more', { defaultValue: 'Más…' })] }) }), _jsx(DropdownMenuContent, { align: 'end', className: 'min-w-56', children: more.map((a) => item(a)) })] })) : null, destructive.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: 'mx-1 h-6 w-px bg-border' }), destructive.map((a) => button(a, 'destructive'))] })) : null] }) }));
39
+ }
@@ -0,0 +1,47 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface CancelReason {
3
+ value: string;
4
+ label: string;
5
+ /** Ayuda corta bajo el motivo. */
6
+ hint?: string;
7
+ /** Exige apuntar a un documento sustituto (motivo 01 del SAT). */
8
+ requiresSubstitute?: boolean;
9
+ /** El motivo no aplica a este documento (se ve deshabilitado con `disabledReason`). */
10
+ disabled?: boolean;
11
+ disabledReason?: string;
12
+ }
13
+ export interface CancelPayload {
14
+ reason: string;
15
+ substitute?: string;
16
+ }
17
+ export interface CancelWithReasonProps {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ /** Identificación del documento («Factura A-1042»). */
21
+ documentLabel: string;
22
+ title?: string;
23
+ reasons: CancelReason[];
24
+ /** Consecuencias que se muestran en el paso de confirmación (§6.2). */
25
+ consequences: string[];
26
+ /**
27
+ * Aviso según el motivo elegido, p. ej. «requiere aceptación del receptor».
28
+ * Devuelve `undefined` si no hay.
29
+ */
30
+ notice?: (reason: CancelReason) => string | undefined;
31
+ /** Formato del identificador del sustituto; si se define, valida en línea. */
32
+ substitutePattern?: RegExp;
33
+ substituteLabel?: string;
34
+ substituteHelp?: string;
35
+ /** Zona para crear el sustituto desde el mismo asistente (el host la implementa). */
36
+ substituteSlot?: (props: {
37
+ value: string;
38
+ onChange: (v: string) => void;
39
+ }) => ReactNode;
40
+ /** Ejecuta la cancelación; debe lanzar con `message` si falla. */
41
+ onConfirm: (payload: CancelPayload) => Promise<unknown>;
42
+ /** Se llama tras un éxito, antes de cerrar. */
43
+ onDone?: () => void;
44
+ confirmLabel?: string;
45
+ }
46
+ export declare function CancelWithReason(props: CancelWithReasonProps): import("react").JSX.Element;
47
+ //# sourceMappingURL=cancel-with-reason.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cancel-with-reason.d.ts","sourceRoot":"","sources":["../../src/document/cancel-with-reason.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAiB3D,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,uEAAuE;IACvE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAA;IACrD,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qFAAqF;IACrF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,KAAK,SAAS,CAAA;IACvF,kEAAkE;IAClE,SAAS,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACvD,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,+BAgJ5D"}
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // CancelWithReason — cancelación guiada de 2 pasos (benchmark §5.5 / §7): motivo
3
+ // del catálogo del tipo de documento → (documento sustituto si el motivo lo
4
+ // exige) → confirmación con consecuencias. Genérico: el catálogo de motivos, las
5
+ // consecuencias y la política de aceptación las declara el llamador; el
6
+ // componente no sabe de CFDI. Solo una acción primaria por paso (verbo +
7
+ // sustantivo) y, si la acción falla, el modal sigue abierto con el error.
8
+ import { useEffect, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { AlertTriangle, Loader2 } from 'lucide-react';
11
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, RadioGroup, RadioGroupItem, } from '@asteby/metacore-ui/primitives';
12
+ export function CancelWithReason(props) {
13
+ const { open, onOpenChange, reasons, consequences, onConfirm, onDone } = props;
14
+ const { t } = useTranslation();
15
+ const [step, setStep] = useState('reason');
16
+ const [reasonValue, setReasonValue] = useState('');
17
+ const [substitute, setSubstitute] = useState('');
18
+ const [busy, setBusy] = useState(false);
19
+ const [error, setError] = useState(null);
20
+ useEffect(() => {
21
+ if (open) {
22
+ setStep('reason');
23
+ setReasonValue('');
24
+ setSubstitute('');
25
+ setError(null);
26
+ setBusy(false);
27
+ }
28
+ }, [open]);
29
+ const reason = reasons.find((r) => r.value === reasonValue);
30
+ const substituteValid = !reason?.requiresSubstitute ||
31
+ (substitute.trim().length > 0 && (!props.substitutePattern || props.substitutePattern.test(substitute.trim())));
32
+ const notice = reason ? props.notice?.(reason) : undefined;
33
+ const tr = (s) => t(s, { defaultValue: s });
34
+ const confirm = async () => {
35
+ if (!reason)
36
+ return;
37
+ setBusy(true);
38
+ setError(null);
39
+ try {
40
+ await onConfirm({ reason: reason.value, substitute: reason.requiresSubstitute ? substitute.trim() : undefined });
41
+ onDone?.();
42
+ onOpenChange(false);
43
+ }
44
+ catch (e) {
45
+ setError(e?.message || t('document.cancel.failed', { defaultValue: 'No se pudo cancelar. Revisa el detalle e intenta de nuevo.' }));
46
+ setBusy(false);
47
+ }
48
+ };
49
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => (!busy ? onOpenChange(o) : undefined), children: _jsxs(DialogContent, { className: 'sm:max-w-lg', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: props.title ?? t('document.cancel.title', { defaultValue: 'Solicitar cancelación' }) }), _jsxs(DialogDescription, { children: [props.documentLabel, " \u00B7", ' ', step === 'reason'
50
+ ? t('document.cancel.step_reason', { defaultValue: 'Paso 1 de 2: elige el motivo' })
51
+ : t('document.cancel.step_confirm', { defaultValue: 'Paso 2 de 2: confirma' })] })] }), step === 'reason' ? (_jsxs("div", { className: 'flex flex-col gap-4', children: [_jsx(RadioGroup, { value: reasonValue, onValueChange: setReasonValue, className: 'gap-2', children: reasons.map((r) => (_jsxs("label", { className: 'flex cursor-pointer items-start gap-3 rounded-md border p-3 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 has-[[data-state=checked]]:border-primary', children: [_jsx(RadioGroupItem, { value: r.value, disabled: r.disabled, className: 'mt-0.5' }), _jsxs("span", { className: 'flex flex-col', children: [_jsx("span", { className: 'text-sm font-medium', children: tr(r.label) }), r.disabled && r.disabledReason ? (_jsx("span", { className: 'text-xs text-muted-foreground', children: tr(r.disabledReason) })) : r.hint ? (_jsx("span", { className: 'text-xs text-muted-foreground', children: tr(r.hint) })) : null] })] }, r.value))) }), reason?.requiresSubstitute ? (props.substituteSlot ? (props.substituteSlot({ value: substitute, onChange: setSubstitute })) : (_jsxs("div", { className: 'grid gap-2', children: [_jsx(Label, { children: props.substituteLabel ?? t('document.cancel.substitute', { defaultValue: 'Documento sustituto' }) }), _jsx(Input, { value: substitute, onChange: (e) => setSubstitute(e.target.value), className: 'font-mono text-sm' }), props.substituteHelp ? _jsx("p", { className: 'text-xs text-muted-foreground', children: tr(props.substituteHelp) }) : null, substitute && !substituteValid ? (_jsx("p", { className: 'text-xs text-destructive', children: t('document.cancel.substitute_invalid', { defaultValue: 'El formato del identificador no es válido.' }) })) : null] }))) : null, notice ? _jsx("p", { className: 'rounded-md bg-muted p-3 text-sm', children: tr(notice) }) : null] })) : (_jsxs("div", { className: 'flex flex-col gap-3', children: [_jsxs("div", { className: 'flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive', children: [_jsx(AlertTriangle, { className: 'mt-0.5 h-4 w-4 shrink-0' }), _jsx("ul", { className: 'list-disc pl-4', children: consequences.map((c) => (_jsx("li", { children: tr(c) }, c))) })] }), _jsxs("dl", { className: 'grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm', children: [_jsx("dt", { className: 'text-muted-foreground', children: t('document.cancel.reason', { defaultValue: 'Motivo' }) }), _jsx("dd", { className: 'font-medium', children: reason ? tr(reason.label) : '' }), reason?.requiresSubstitute ? (_jsxs(_Fragment, { children: [_jsx("dt", { className: 'text-muted-foreground', children: t('document.cancel.substitute_short', { defaultValue: 'Sustituto' }) }), _jsx("dd", { className: 'break-all font-mono text-xs', children: substitute })] })) : null] }), notice ? _jsx("p", { className: 'text-sm text-muted-foreground', children: tr(notice) }) : null, error ? (_jsx("p", { role: 'alert', className: 'rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive', children: error })) : null] })), _jsx(DialogFooter, { children: step === 'reason' ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: 'button', variant: 'outline', onClick: () => onOpenChange(false), children: t('document.cancel.keep', { defaultValue: 'Conservar documento' }) }), _jsx(Button, { type: 'button', disabled: !reason || !substituteValid, onClick: () => setStep('confirm'), children: t('document.cancel.continue', { defaultValue: 'Continuar' }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Button, { type: 'button', variant: 'outline', disabled: busy, onClick: () => setStep('reason'), children: t('document.cancel.back', { defaultValue: 'Volver al motivo' }) }), _jsxs(Button, { type: 'button', variant: 'destructive', disabled: busy, onClick: confirm, children: [busy ? _jsx(Loader2, { className: 'mr-1.5 h-4 w-4 animate-spin' }) : null, props.confirmLabel ?? t('document.cancel.confirm', { defaultValue: 'Solicitar cancelación' })] })] })) })] }) }));
52
+ }
@@ -0,0 +1,41 @@
1
+ import { type ReactNode } from 'react';
2
+ import { formatFieldValue, type FieldFormatOptions } from './format';
3
+ import type { StatusDef } from './types';
4
+ export interface HeaderBadge {
5
+ id: string;
6
+ label?: string;
7
+ status: StatusDef;
8
+ }
9
+ export interface HeaderMetric {
10
+ label: string;
11
+ value: string;
12
+ tone?: 'default' | 'critical';
13
+ mono?: boolean;
14
+ }
15
+ export interface DocumentHeaderProps {
16
+ title: string;
17
+ /** Texto bajo el título; si `subtitleHref` viene, es un enlace (ficha del cliente). */
18
+ subtitle?: string;
19
+ subtitleHref?: string;
20
+ badges: HeaderBadge[];
21
+ metrics?: HeaderMetric[];
22
+ copyable?: {
23
+ label: string;
24
+ value: string;
25
+ };
26
+ /** Zona de acciones (normalmente `<ActionBar/>`). */
27
+ actions?: ReactNode;
28
+ /** Slot antes del título (botón «Volver»). */
29
+ leading?: ReactNode;
30
+ /** Renderiza el subtítulo como enlace del router del host. */
31
+ renderLink?: (props: {
32
+ href: string;
33
+ children: ReactNode;
34
+ className?: string;
35
+ }) => ReactNode;
36
+ className?: string;
37
+ }
38
+ export declare function DocumentHeader({ title, subtitle, subtitleHref, badges, metrics, copyable, actions, leading, renderLink, className, }: DocumentHeaderProps): import("react").JSX.Element;
39
+ export { formatFieldValue };
40
+ export type { FieldFormatOptions };
41
+ //# sourceMappingURL=document-header.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-header.d.ts","sourceRoot":"","sources":["../../src/document/document-header.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAMhD,OAAO,EAAE,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,CAAA;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,qDAAqD;IACrD,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAA;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,UAAU,EACV,SAAS,GACZ,EAAE,mBAAmB,+BAmFrB;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA;AAC3B,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // DocumentHeader — cabecera estándar (benchmark §5.2): serie/folio, badges de
3
+ // cada máquina de estado, cliente enlazable, métricas (total, saldo, fechas) y
4
+ // un identificador copiable (UUID).
5
+ import { useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Check, Copy } from 'lucide-react';
8
+ import { Button } from '@asteby/metacore-ui/primitives';
9
+ import { cn } from '@asteby/metacore-ui/lib';
10
+ import { StatusBadge } from './status-badge';
11
+ import { formatFieldValue } from './format';
12
+ export function DocumentHeader({ title, subtitle, subtitleHref, badges, metrics, copyable, actions, leading, renderLink, className, }) {
13
+ const { t } = useTranslation();
14
+ const [copied, setCopied] = useState(false);
15
+ const copy = async () => {
16
+ if (!copyable?.value)
17
+ return;
18
+ try {
19
+ await navigator.clipboard.writeText(copyable.value);
20
+ setCopied(true);
21
+ setTimeout(() => setCopied(false), 1500);
22
+ }
23
+ catch {
24
+ /* sin portapapeles (contexto inseguro): el valor sigue seleccionable */
25
+ }
26
+ };
27
+ const subtitleNode = subtitle ? (subtitleHref ? (renderLink ? (renderLink({ href: subtitleHref, children: subtitle, className: 'text-sm text-primary hover:underline' })) : (_jsx("a", { href: subtitleHref, className: 'text-sm text-primary hover:underline', children: subtitle }))) : (_jsx("span", { className: 'text-sm text-muted-foreground', children: subtitle }))) : null;
28
+ return (_jsxs("header", { className: cn('flex flex-col gap-4 rounded-lg border bg-card p-5 text-card-foreground', className), "data-document-header": '', children: [leading, _jsxs("div", { className: 'flex flex-wrap items-start justify-between gap-4', children: [_jsxs("div", { className: 'flex min-w-0 flex-col gap-2', children: [_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [_jsx("h1", { className: 'text-2xl font-bold tracking-tight', children: title }), badges.map((b) => (_jsx(StatusBadge, { status: b.status, srLabel: b.label ? t(b.label, { defaultValue: b.label }) : undefined }, b.id)))] }), subtitleNode] }), actions] }), metrics && metrics.length > 0 ? (_jsx("dl", { className: 'grid grid-cols-2 gap-x-6 gap-y-3 sm:grid-cols-3 lg:grid-cols-5', children: metrics.map((m) => (_jsxs("div", { children: [_jsx("dt", { className: 'text-xs uppercase tracking-wide text-muted-foreground', children: m.label }), _jsx("dd", { className: cn('font-medium tabular-nums', m.mono && 'font-mono text-xs', m.tone === 'critical' && 'text-destructive'), children: m.value })] }, m.label))) })) : null, copyable?.value ? (_jsxs("div", { className: 'flex flex-wrap items-center gap-2 text-xs text-muted-foreground', children: [_jsx("span", { className: 'uppercase tracking-wide', children: copyable.label }), _jsx("code", { className: 'break-all font-mono text-foreground', children: copyable.value }), _jsxs(Button, { type: 'button', size: 'sm', variant: 'ghost', className: 'h-6 px-1.5', onClick: copy, "aria-label": t('document.copy', { defaultValue: 'Copiar' }), children: [copied ? _jsx(Check, { className: 'h-3.5 w-3.5' }) : _jsx(Copy, { className: 'h-3.5 w-3.5' }), _jsx("span", { className: 'ml-1', children: copied ? t('document.copied', { defaultValue: 'Copiado' }) : t('document.copy', { defaultValue: 'Copiar' }) })] })] })) : null] }));
29
+ }
30
+ export { formatFieldValue };
@@ -0,0 +1,37 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type FieldFormatOptions } from './format';
3
+ import { type DocumentPageRegistration } from './registry';
4
+ import type { DocumentContext, DocumentSlotRenderer, ResolvedAction } from './types';
5
+ export interface DocumentPageProps extends FieldFormatOptions {
6
+ registration: DocumentPageRegistration;
7
+ record: Record<string, unknown>;
8
+ /**
9
+ * Ejecuta una acción. El host decide: abrir el modal de la acción del
10
+ * modelo (`def.modelAction ?? def.key`), navegar (`def.href`), etc. La
11
+ * promesa mantiene el botón en `loading`.
12
+ */
13
+ onAction: (action: ResolvedAction, ctx: DocumentContext) => void | Promise<unknown>;
14
+ /** UI del host para pestañas `kind: 'slot'` (historial, archivos, vista previa PDF…). */
15
+ slots?: Record<string, DocumentSlotRenderer>;
16
+ /** Navegación del host (botones inteligentes, cliente). */
17
+ onNavigate?: (href: string) => void;
18
+ renderLink?: (props: {
19
+ href: string;
20
+ children: ReactNode;
21
+ className?: string;
22
+ }) => ReactNode;
23
+ /** Botón «Volver» u otro contenido antes del título. */
24
+ leading?: ReactNode;
25
+ /** Pestaña inicial (`?tab=`). */
26
+ defaultTab?: string;
27
+ /** Pestaña controlada por el host (p. ej. «Ver detalle del error» abre Datos fiscales). */
28
+ tab?: string;
29
+ onTabChange?: (tab: string) => void;
30
+ /** Ruta base para el enlace del subtítulo (`/m/customers/{{customer_id}}`). */
31
+ subtitleHrefTemplate?: string;
32
+ /** Cambia para forzar recarga de las fuentes tras una acción. */
33
+ reloadKey?: number;
34
+ className?: string;
35
+ }
36
+ export declare function DocumentPage(props: DocumentPageProps): import("react").JSX.Element;
37
+ //# sourceMappingURL=document-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-page.d.ts","sourceRoot":"","sources":["../../src/document/document-page.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AASzD,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACpE,OAAO,EAAqB,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE7E,OAAO,KAAK,EAER,eAAe,EAEf,oBAAoB,EAGpB,cAAc,EACjB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD,YAAY,EAAE,wBAAwB,CAAA;IACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACnF,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;IAC5C,2DAA2D;IAC3D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAA;IAC5F,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2FAA2F;IAC3F,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,+EAA+E;IAC/E,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BA4HpD"}
@@ -0,0 +1,137 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // DocumentPage — «página de documento» declarativa (benchmark §5.2 / §6.1):
3
+ // cabecera + badges + barra de acciones por estado + botones inteligentes +
4
+ // pestañas. Se alimenta de un `DocumentSpec` (datos puros) y de un registro;
5
+ // las pestañas de campos y de registros relacionados se resuelven solas, y lo
6
+ // que necesita UI del host (historial, archivos, PDF) entra por `slots`.
7
+ import { useMemo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@asteby/metacore-ui/primitives';
10
+ import { cn } from '@asteby/metacore-ui/lib';
11
+ import { ActionBar } from './action-bar';
12
+ import { DocumentHeader } from './document-header';
13
+ import { SmartButtons } from './smart-buttons';
14
+ import { StatusBadge } from './status-badge';
15
+ import { interpolate, readPath, resolveActions, resolveStatus, resolveTitle } from './evaluate';
16
+ import { formatFieldValue } from './format';
17
+ import { findStatusMachine } from './registry';
18
+ import { useDocumentSources } from './use-document-sources';
19
+ export function DocumentPage(props) {
20
+ const { registration, record, onAction, slots, onNavigate, renderLink, leading, defaultTab, className } = props;
21
+ const { spec } = registration;
22
+ const { t } = useTranslation();
23
+ const fmt = { locale: props.locale, currency: props.currency, timeZone: props.timeZone };
24
+ const recordId = record.id;
25
+ const { sources, reload } = useDocumentSources(spec.sources, recordId);
26
+ const ctx = useMemo(() => {
27
+ const base = { record, sources };
28
+ return { ...base, derived: registration.derive?.(base) ?? {} };
29
+ }, [record, sources, registration]);
30
+ // Recarga las fuentes cuando el host avisa de un cambio (acción ejecutada).
31
+ const [lastReload, setLastReload] = useState(props.reloadKey ?? 0);
32
+ if ((props.reloadKey ?? 0) !== lastReload) {
33
+ setLastReload(props.reloadKey ?? 0);
34
+ reload();
35
+ }
36
+ const handleAction = (a) => {
37
+ const { tab: targetTab, openUrl, href } = a.def;
38
+ if (targetTab)
39
+ return setTab(targetTab);
40
+ if (openUrl) {
41
+ const url = interpolate(openUrl, ctx);
42
+ if (url)
43
+ window.open(url, '_blank', 'noopener,noreferrer');
44
+ return;
45
+ }
46
+ if (href)
47
+ return onNavigate?.(interpolate(href, ctx));
48
+ return onAction(a, ctx);
49
+ };
50
+ const layout = useMemo(() => resolveActions(spec, ctx), [spec, ctx]);
51
+ const badges = spec.statuses
52
+ .map((m) => {
53
+ const status = resolveStatus(m, ctx);
54
+ return status ? { id: m.id, label: m.label, status } : null;
55
+ })
56
+ .filter((b) => !!b);
57
+ const metrics = (spec.metrics ?? [])
58
+ .filter((m) => !m.hideEmpty || readPath(ctx, m.path) != null)
59
+ .map((m) => ({
60
+ label: t(m.label, { defaultValue: m.label }),
61
+ value: formatFieldValue(readPath(ctx, m.path), m.format, fmt),
62
+ tone: m.tone,
63
+ mono: m.format === 'mono',
64
+ }));
65
+ const subtitleRaw = spec.subtitle ? readPath(ctx, spec.subtitle.path) : undefined;
66
+ const subtitleText = subtitleRaw == null || subtitleRaw === '' ? '—' : formatFieldValue(subtitleRaw, 'text', fmt);
67
+ const subtitle = subtitleText === '—' ? undefined : subtitleText;
68
+ const subtitleHref = subtitle && props.subtitleHrefTemplate ? interpolate(props.subtitleHrefTemplate, ctx) : undefined;
69
+ const copyValue = spec.copyable ? readPath(ctx, spec.copyable.path) : undefined;
70
+ const [innerTab, setInnerTab] = useState(defaultTab ?? spec.tabs[0]?.key);
71
+ const tab = props.tab ?? innerTab;
72
+ const setTab = (key) => {
73
+ setInnerTab(key);
74
+ props.onTabChange?.(key);
75
+ };
76
+ const smart = (spec.smartButtons ?? [])
77
+ .map((b) => {
78
+ const count = b.source ? sources[b.source]?.length ?? 0 : undefined;
79
+ const active = b.source ? (count ?? 0) > 0 : b.field ? !!readPath(ctx, b.field) : true;
80
+ if (b.hideWhenEmpty && !active)
81
+ return null;
82
+ const href = b.href ? interpolate(b.href, ctx) : undefined;
83
+ return {
84
+ key: b.key,
85
+ label: b.label,
86
+ icon: b.icon,
87
+ count,
88
+ disabled: !active && !b.tab,
89
+ onClick: () => {
90
+ if (b.tab)
91
+ setTab(b.tab);
92
+ else if (href)
93
+ onNavigate?.(href);
94
+ },
95
+ };
96
+ })
97
+ .filter((b) => !!b);
98
+ return (_jsxs("div", { className: cn('flex flex-col gap-4', className), "data-document-page": spec.model, children: [_jsx(DocumentHeader, { title: resolveTitle(spec.title, ctx) || t('document.untitled', { defaultValue: 'Documento' }), subtitle: subtitle, subtitleHref: subtitleHref, badges: badges, metrics: metrics, copyable: spec.copyable && copyValue
99
+ ? { label: t(spec.copyable.label, { defaultValue: spec.copyable.label }), value: String(copyValue) }
100
+ : undefined, leading: leading, renderLink: renderLink, actions: _jsx(ActionBar, { layout: layout, onAction: (a) => handleAction(a) }) }), _jsx(SmartButtons, { items: smart }), _jsxs(Tabs, { value: tab, onValueChange: setTab, className: 'flex flex-col gap-4', children: [_jsx(TabsList, { className: 'w-fit', children: spec.tabs.map((tb) => (_jsxs(TabsTrigger, { value: tb.key, children: [t(tb.label, { defaultValue: tb.label }), tabCount(tb, ctx) !== undefined ? (_jsx("span", { className: 'ml-1.5 rounded-full bg-muted px-1.5 text-xs tabular-nums', children: tabCount(tb, ctx) })) : null] }, tb.key))) }), spec.tabs.map((tb) => (_jsx(TabsContent, { value: tb.key, className: 'mt-0', children: renderTab(tb, spec, ctx, fmt, slots) }, tb.key)))] })] }));
101
+ }
102
+ function tabCount(tb, ctx) {
103
+ if (tb.kind === 'records')
104
+ return ctx.sources[tb.source]?.length;
105
+ if (tb.kind === 'slot' && tb.count) {
106
+ const v = readPath(ctx, tb.count);
107
+ return typeof v === 'number' ? v : undefined;
108
+ }
109
+ return undefined;
110
+ }
111
+ function renderTab(tb, spec, ctx, fmt, slots) {
112
+ if (tb.kind === 'slot')
113
+ return slots?.[tb.slot]?.(ctx) ?? null;
114
+ if (tb.kind === 'fields') {
115
+ return (_jsx("div", { className: 'flex flex-col gap-4', children: tb.groups.map((g, i) => {
116
+ const fields = g.fields.filter((f) => !f.hideEmpty || readPath(ctx, f.path) != null);
117
+ if (fields.length === 0)
118
+ return null;
119
+ return (_jsxs("section", { className: 'rounded-lg border bg-card p-5', children: [g.title ? _jsx("h2", { className: 'mb-3 text-sm font-semibold', children: g.title }) : null, _jsx("dl", { className: 'grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2 lg:grid-cols-3', children: fields.map((f) => (_jsxs("div", { className: cn(f.wide && 'sm:col-span-2 lg:col-span-3'), children: [_jsx("dt", { className: 'text-xs uppercase tracking-wide text-muted-foreground', children: f.label }), _jsx("dd", { className: cn('break-words font-medium', f.format === 'mono' && 'font-mono text-xs'), children: formatFieldValue(readPath(ctx, f.path), f.format, fmt) })] }, f.path))) })] }, g.title ?? i));
120
+ }) }));
121
+ }
122
+ const rows = ctx.sources[tb.source] ?? [];
123
+ if (rows.length === 0) {
124
+ return (_jsx("p", { className: 'rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground', children: tb.empty ?? 'Sin registros.' }));
125
+ }
126
+ return (_jsx("div", { className: 'overflow-x-auto rounded-lg border bg-card', children: _jsxs("table", { className: 'w-full text-sm', children: [_jsx("thead", { className: 'bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground', children: _jsx("tr", { children: tb.columns.map((c) => (_jsx("th", { className: 'px-3 py-2 font-medium', children: c.label }, c.path))) }) }), _jsx("tbody", { children: rows.map((row, i) => (_jsx("tr", { className: 'border-t', children: tb.columns.map((c) => (_jsx("td", { className: 'px-3 py-2', children: renderCell(c, row, spec, fmt) }, c.path))) }, String(row.id ?? i)))) })] }) }));
127
+ }
128
+ function renderCell(col, row, spec, fmt) {
129
+ const v = row[col.path];
130
+ if (col.status) {
131
+ const machine = findStatusMachine(spec, col.status);
132
+ const st = machine?.states.find((s) => s.value === String(v));
133
+ if (st)
134
+ return _jsx(StatusBadge, { status: st });
135
+ }
136
+ return (_jsx("span", { className: cn(col.format === 'mono' && 'font-mono text-xs', 'tabular-nums'), children: formatFieldValue(v, col.format, fmt) }));
137
+ }
@@ -0,0 +1,20 @@
1
+ import { type FieldFormatOptions } from './format';
2
+ import type { StatusTone } from './types';
3
+ export interface TimelineEntry {
4
+ id: string;
5
+ at: string;
6
+ /** Texto ya redactado («Timbró la factura»). */
7
+ label: string;
8
+ actor?: string;
9
+ detail?: string;
10
+ icon?: string;
11
+ tone?: StatusTone;
12
+ }
13
+ export interface DocumentTimelineProps extends FieldFormatOptions {
14
+ entries: TimelineEntry[];
15
+ /** Texto del estado vacío; lleva `emptyAction` si el host da una acción. */
16
+ emptyText?: string;
17
+ className?: string;
18
+ }
19
+ export declare function DocumentTimeline({ entries, emptyText, className, ...fmt }: DocumentTimelineProps): import("react").JSX.Element;
20
+ //# sourceMappingURL=document-timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-timeline.d.ts","sourceRoot":"","sources":["../../src/document/document-timeline.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC7D,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAWD,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,GAAG,EAAE,EAAE,qBAAqB,+BA2BhG"}