@asteby/metacore-runtime-react 46.3.0 → 47.0.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 (99) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/action-modal-dispatcher.d.ts +10 -1
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +107 -19
  5. package/dist/business/catalog.d.ts.map +1 -1
  6. package/dist/business/catalog.js +24 -0
  7. package/dist/business/index.d.ts +3 -0
  8. package/dist/business/index.d.ts.map +1 -1
  9. package/dist/business/index.js +3 -0
  10. package/dist/business/print-send-dialog.d.ts +27 -0
  11. package/dist/business/print-send-dialog.d.ts.map +1 -0
  12. package/dist/business/print-send-dialog.js +55 -0
  13. package/dist/business/relate-documents.d.ts +41 -0
  14. package/dist/business/relate-documents.d.ts.map +1 -0
  15. package/dist/business/relate-documents.js +42 -0
  16. package/dist/business/vehicle-picker.d.ts +36 -0
  17. package/dist/business/vehicle-picker.d.ts.map +1 -0
  18. package/dist/business/vehicle-picker.js +61 -0
  19. package/dist/cfdi-stamp-panel.d.ts +29 -0
  20. package/dist/cfdi-stamp-panel.d.ts.map +1 -0
  21. package/dist/cfdi-stamp-panel.js +53 -0
  22. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  23. package/dist/dialogs/dynamic-record.js +5 -0
  24. package/dist/document-form-dialog.d.ts +23 -0
  25. package/dist/document-form-dialog.d.ts.map +1 -0
  26. package/dist/document-form-dialog.js +196 -0
  27. package/dist/dynamic-crud-page.d.ts +6 -0
  28. package/dist/dynamic-crud-page.d.ts.map +1 -1
  29. package/dist/dynamic-crud-page.js +17 -7
  30. package/dist/dynamic-kanban.d.ts.map +1 -1
  31. package/dist/dynamic-kanban.js +3 -1
  32. package/dist/dynamic-table.d.ts.map +1 -1
  33. package/dist/dynamic-table.js +6 -2
  34. package/dist/index.d.ts +6 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -0
  37. package/dist/list-primary-action.d.ts +31 -0
  38. package/dist/list-primary-action.d.ts.map +1 -0
  39. package/dist/list-primary-action.js +35 -0
  40. package/dist/model-action-toolbar.d.ts +7 -1
  41. package/dist/model-action-toolbar.d.ts.map +1 -1
  42. package/dist/model-action-toolbar.js +10 -3
  43. package/dist/model-extension-registry.d.ts +7 -0
  44. package/dist/model-extension-registry.d.ts.map +1 -1
  45. package/dist/nav-permissions.d.ts +19 -0
  46. package/dist/nav-permissions.d.ts.map +1 -0
  47. package/dist/nav-permissions.js +51 -0
  48. package/dist/navigation-builder.d.ts +6 -0
  49. package/dist/navigation-builder.d.ts.map +1 -1
  50. package/dist/navigation-builder.js +26 -1
  51. package/dist/permissions-manager.d.ts +9 -0
  52. package/dist/permissions-manager.d.ts.map +1 -1
  53. package/dist/permissions-manager.js +50 -17
  54. package/dist/print-document-button.d.ts.map +1 -1
  55. package/dist/print-document-button.js +9 -1
  56. package/dist/record-mutation-events.d.ts +14 -0
  57. package/dist/record-mutation-events.d.ts.map +1 -0
  58. package/dist/record-mutation-events.js +37 -0
  59. package/dist/types.d.ts +60 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/use-print-document.d.ts +15 -1
  62. package/dist/use-print-document.d.ts.map +1 -1
  63. package/dist/use-print-document.js +52 -1
  64. package/dist/use-record-mutation-tick.d.ts +6 -0
  65. package/dist/use-record-mutation-tick.d.ts.map +1 -0
  66. package/dist/use-record-mutation-tick.js +11 -0
  67. package/package.json +3 -3
  68. package/src/__tests__/action-modal-feedback.test.tsx +116 -0
  69. package/src/__tests__/business-pickers.test.tsx +157 -0
  70. package/src/__tests__/document-form-dialog.test.tsx +190 -0
  71. package/src/__tests__/dynamic-table-mutation-refresh.test.tsx +116 -0
  72. package/src/__tests__/list-primary-action.test.tsx +157 -0
  73. package/src/__tests__/nav-permissions.test.tsx +76 -0
  74. package/src/__tests__/permissions-manager.test.tsx +21 -4
  75. package/src/__tests__/use-print-document-feedback.test.tsx +58 -0
  76. package/src/action-modal-dispatcher.tsx +124 -6
  77. package/src/business/catalog.ts +24 -0
  78. package/src/business/index.ts +3 -0
  79. package/src/business/print-send-dialog.tsx +175 -0
  80. package/src/business/relate-documents.tsx +182 -0
  81. package/src/business/vehicle-picker.tsx +188 -0
  82. package/src/cfdi-stamp-panel.tsx +146 -0
  83. package/src/dialogs/dynamic-record.tsx +5 -0
  84. package/src/document-form-dialog.tsx +357 -0
  85. package/src/dynamic-crud-page.tsx +36 -7
  86. package/src/dynamic-kanban.tsx +3 -1
  87. package/src/dynamic-table.tsx +6 -2
  88. package/src/index.ts +36 -0
  89. package/src/list-primary-action.ts +71 -0
  90. package/src/model-action-toolbar.tsx +18 -3
  91. package/src/model-extension-registry.tsx +7 -0
  92. package/src/nav-permissions.ts +58 -0
  93. package/src/navigation-builder.tsx +27 -1
  94. package/src/permissions-manager.tsx +39 -26
  95. package/src/print-document-button.tsx +9 -1
  96. package/src/record-mutation-events.ts +50 -0
  97. package/src/types.ts +63 -0
  98. package/src/use-print-document.ts +66 -3
  99. package/src/use-record-mutation-tick.ts +12 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 47.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4371f2c: Diálogos de acción: resultado de timbrado y errores del servidor.
8
+
9
+ - #1024 (FAC-3 / PIT-051): nuevo `CfdiStampPanel` / `CfdiStampResultDialog` / `extractStampResult`. Cuando la acción responde con `fiscal_uuid`, `pdf_url` y/o `xml_url`, el diálogo muestra el UUID como texto y enlaces «Descargar PDF» / «Descargar XML» en lugar del JSON. Sin esas claves no se inventa un resultado fiscal.
10
+ - #1023 (PIT-046): los diálogos de acción (genérico, wizard y confirmación) pintan un `FormErrorBanner` dentro del diálogo con el 422 (`errors` por campo) o el mensaje y causa de un 500, y permanecen abiertos.
11
+
12
+ - 4371f2c: FAC-12 (#1022): alta guiada por tipo de documento. Nuevo `DocumentFormDialog` y `resolveDocumentForms`: cuando el metadata de la tabla (o la prop `documentForms` de `DynamicCRUDPage`) trae `document_forms`, «Crear» muestra tarjetas de tipo (Factura / Nota de crédito / REP / Global / Traslado…) y, al elegir, los campos de ese tipo y un paso de renglones con `DocumentLinesGrid`; un solo tipo se salta el selector. Escribe el valor del tipo en `type_field`, serializa los renglones con `serializeLineItems`, muestra los 422 (banner + campos) dentro del diálogo y emite el evento de mutación para refrescar la lista. Sin manifest, el alta genérica no cambia. El SDK no embebe catálogos SAT: campos y valores los sirve el kernel o el addon. Nuevos tipos: `DocumentFormsManifest`, `DocumentFormType`, `DocumentFormLines`, `TableMetadata.document_forms`.
13
+ - 4371f2c: PIT-044: el menú lateral se filtra por los permisos efectivos del rol. `@asteby/metacore-ui` agrega `filterNavGroups`, `NavLinkItem.requires` y la prop `isItemVisible` de `AppSidebar`; `@asteby/metacore-runtime-react` agrega `useNavItemVisible`, `isNavItemAllowed`, `capabilityForNavItem` (una entrada `/m/<modelo>` exige `<modelo>.index`; las demás rutas no se bloquean) y `useNavigation` respeta `NavItem.requires`; el `AppSidebar` de starter-core lo aplica. Sin `PermissionsProvider` (o con `isAdmin`) no se oculta nada.
14
+ - 4371f2c: Las listas se refrescan tras crear, editar o borrar (#1020). `DynamicRecordDialog` y `ActionModalDispatcher` (componente federado, wizard, formulario genérico y confirmación) emiten el evento `metacore:record-mutated` con el modelo; `DynamicTable` y `DynamicKanban` vuelven a pedir los datos cuando el modelo coincide (sin distinguir mayúsculas). Nuevos exports: `emitRecordMutation`, `subscribeRecordMutations`, `useRecordMutationTick`.
15
+ - 4371f2c: Tres componentes de negocio compartidos:
16
+
17
+ - `VehiclePicker` (#1025): selector controlado de vehículo con búsqueda por placa o VIN y alta rápida (`CreateRecordDialog`), mismo estilo que `CustomerPicker`; sin `PermissionsProvider` no bloquea la UI.
18
+ - `RelateDocuments` (#1026): relaciona uno o más documentos origen con un tipo de relación (`related_document_id`, `relation_type`), muestra UUID/folio; los tipos llegan por prop (sin catálogos fiscales embebidos).
19
+ - `PrintSendDialog` (#1027): imprimir (URL de PDF del host) o enviar por `onSend(canal, destino, documento)`; sin integración de mensajería.
20
+
21
+ ### Patch Changes
22
+
23
+ - 4371f2c: Roles y permisos (#1021): «Permisos generales» deja de ser una columna de tarjetas junto al rol y pasa a ser un grupo sintético del combobox de módulos; al elegirlo se editan en la misma grilla de dos columnas que las acciones de un módulo, y el conteo de concedidos los incluye. Nuevo `generalPermissionsModule`.
24
+ - 4371f2c: Auditoría Ronda D: «Descargar Factura CFDI» no daba ninguna señal. `usePrintDocument` ahora muestra un toast de progreso, uno de éxito y, si falla, el motivo que responde el servidor (también cuando el cuerpo llega como Blob); la promesa sigue rechazando. `feedback: false` lo silencia y `PrintDocumentButton` deja el toast de error al llamador cuando pasa `onError`.
25
+ - Updated dependencies [4371f2c]
26
+ - @asteby/metacore-ui@2.24.0
27
+
28
+ ## 46.4.0
29
+
30
+ ### Minor Changes
31
+
32
+ - e47bca8: Una sola acción primaria de alta por pantalla. `resolveListPrimaryAction` oculta el Crear genérico cuando hay `placement: create`, `replaces_create`, `create_mode: hidden|action`, `canCreate: false` o `ModelExtension.primaryActionKey`. Si dos acciones reclaman el alta, solo una lleva `data-primary`.
33
+
3
34
  ## 46.3.0
4
35
 
5
36
  ### Minor Changes
@@ -1,3 +1,4 @@
1
+ import type { Translate } from './server-error';
1
2
  import type { ActionFieldDef } from './types';
2
3
  import { type ActionMetadata, type ActionModalProps } from '@asteby/metacore-sdk';
3
4
  export type { ActionMetadata, ActionModalProps };
@@ -66,7 +67,15 @@ export declare function unwrapRecordScalar(value: unknown): unknown;
66
67
  export declare function readRecordPath(record: any, path: string): unknown;
67
68
  /** Scalar seed for one action field from the row being acted on. */
68
69
  export declare function scalarDefaultFromRecord(field: ActionFieldDef, record: any): unknown;
69
- export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }: ActionModalProps): import("react").JSX.Element | null;
70
+ export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }: ActionModalProps): import("react").JSX.Element | null;
70
71
  /** How long an action with `modal` waits for its federated component. */
71
72
  export declare const FEDERATED_ACTION_MODAL_TIMEOUT_MS = 20000;
73
+ /** Text for the in-dialog error banner of a failed action (PIT-046, #1023): a
74
+ * 422 with per-field `errors` -> "Revisa los campos" + each localized line; any
75
+ * other failure (500, network) -> the server's own message and cause. Unlike
76
+ * the toast it stays on screen until the next attempt. */
77
+ export declare function actionErrorBannerMessage(err: unknown, fields: readonly ActionFieldDef[] | undefined, t: Translate, language?: string): string;
78
+ export declare function buildFieldDefaults(fields: ActionFieldDef[], record: any): Record<string, any>;
79
+ /** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
80
+ export declare function renderField(field: ActionFieldDef, value: any, onChange: (value: any) => void, formValues?: Record<string, any>, record?: Record<string, any>, fieldErrors?: Record<string, string>): import("react").JSX.Element;
72
81
  //# sourceMappingURL=action-modal-dispatcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AA8B/C,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAGxB,MAAM,sBAAsB,CAAA;AAG7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AAYD,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CAC1C;AAED,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,cAAc,EAAE,GAAG,SAAS,EACpC,MAAM,EAAE,GAAG,EACX,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC5H,sBAAsB,EAAE,CAyB1B;AASD,MAAM,WAAW,uBAAuB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,WAAW,CAAA;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,cAAc,EAAE,GAAG,SAAS,EACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC9B,uBAAuB,EAAE,CAe3B;AAKD,wBAAgB,+BAA+B,CAC3C,GAAG,EAAE,uBAAuB,EAC5B,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC5H;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CActE;AASD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CASjE;AAUD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAgBnF;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EAAE,aAAa,GAC3B,EAAE,gBAAgB,sCA0FlB;AAED,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,QAAS,CAAA;AA6VvD;;;2DAG2D;AAC3D,wBAAgB,wBAAwB,CACpC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EAC7C,CAAC,EAAE,SAAS,EACZ,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CAWR;AA+bD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAoB7F;AAyPD,oGAAoG;AACpG,wBAAgB,WAAW,CACvB,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,GAAG,EACV,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,EAK9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,+BAwEvC"}
@@ -15,13 +15,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
15
15
  //
16
16
  // The host injects its axios-like client via <ApiProvider>; we no longer
17
17
  // depend on a bundler alias to `@/lib/api`.
18
- import { useState, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
18
+ import { emitRecordMutation } from './record-mutation-events';
19
+ import { useState, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
19
20
  import { useTranslation } from 'react-i18next';
20
21
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
21
22
  import { Loader2 } from 'lucide-react';
22
23
  import { ProcessStepper } from '@asteby/metacore-ui/wizard';
23
24
  import { toast } from 'sonner';
24
- import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
25
+ import { toastServerError, toastServerSuccess, extractFieldErrors, extractServerError, localizeFieldErrorMap } from './server-error';
26
+ import { FormErrorBanner } from './business/feedback';
27
+ import { CfdiStampResultDialog, extractStampResult } from './cfdi-stamp-panel';
25
28
  import { useBranchCreateGate } from './branch-create-gate';
26
29
  import { validateValues, bagHasErrors } from './validator';
27
30
  import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
@@ -233,7 +236,14 @@ export function scalarDefaultFromRecord(field, record) {
233
236
  }
234
237
  return undefined;
235
238
  }
236
- export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }) {
239
+ export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }) {
240
+ // Every successful action may have created/updated/deleted a record of
241
+ // `model` (federated component, wizard, generic form or confirmation), so
242
+ // announce it: mounted lists of the model reload on their own (#1020).
243
+ const onSuccess = useCallback(() => {
244
+ emitRecordMutation(model, 'update');
245
+ onSuccessProp?.();
246
+ }, [model, onSuccessProp]);
237
247
  // Reactive read: a federated remote usually registers AFTER the first
238
248
  // render, and a memoized lookup would pin the fallback forever.
239
249
  const readComponent = () => getActionComponent(model, action.key);
@@ -518,10 +528,28 @@ function toastActionError(err, fields, t, language) {
518
528
  }
519
529
  toastServerError(err, { t, language });
520
530
  }
531
+ /** Text for the in-dialog error banner of a failed action (PIT-046, #1023): a
532
+ * 422 with per-field `errors` -> "Revisa los campos" + each localized line; any
533
+ * other failure (500, network) -> the server's own message and cause. Unlike
534
+ * the toast it stays on screen until the next attempt. */
535
+ export function actionErrorBannerMessage(err, fields, t, language) {
536
+ const localized = localizeActionFieldErrors(err, fields, t, language);
537
+ if (localized) {
538
+ const head = t('validation.failed', { defaultValue: validationCatalog(language).failed });
539
+ const lines = Object.values(localized).filter(Boolean);
540
+ return lines.length ? `${head}: ${lines.join(' · ')}` : head;
541
+ }
542
+ const fallback = t('common.error', { defaultValue: 'Algo salió mal. Intenta de nuevo.' });
543
+ const ex = extractServerError(err, fallback);
544
+ const title = t(ex.title, { defaultValue: ex.title });
545
+ return ex.description && ex.description !== title ? `${title} — ${ex.description}` : title;
546
+ }
521
547
  function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
522
548
  const { t, i18n } = useTranslation();
523
549
  const api = useApi();
524
550
  const [executing, setExecuting] = useState(false);
551
+ const [formError, setFormError] = useState();
552
+ const [stamp, setStamp] = useState();
525
553
  // Actions listed in the model's `reason_required.actions` (cancel, void…)
526
554
  // answer 422 errors.reason without a motive; the prompt asks and retries.
527
555
  const reasonPrompt = useReasonPrompt();
@@ -532,6 +560,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
532
560
  const label = t(action.label, { defaultValue: action.label });
533
561
  const execute = async () => {
534
562
  setExecuting(true);
563
+ setFormError(undefined);
535
564
  try {
536
565
  const url = buildActionUrl(endpoint, model, record.id, action.key);
537
566
  // Supervisor PIN first (action.supervisorPolicy); null = cancelled.
@@ -548,21 +577,37 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
548
577
  return;
549
578
  if (res.data.success) {
550
579
  toastServerSuccess(res.data, { t });
580
+ const result = extractStampResult(res.data);
581
+ if (result) {
582
+ setStamp(result);
583
+ return;
584
+ }
551
585
  onOpenChange(false);
552
586
  onSuccess();
553
587
  }
554
588
  else {
589
+ setFormError(actionErrorBannerMessage({ response: { data: res.data } }, action.fields, t, i18n.language));
555
590
  toastActionError({ response: { data: res.data } }, action.fields, t, i18n.language);
556
591
  }
557
592
  }
558
593
  catch (err) {
594
+ setFormError(actionErrorBannerMessage(err, action.fields, t, i18n.language));
559
595
  toastActionError(err, action.fields, t, i18n.language);
560
596
  }
561
597
  finally {
562
598
  setExecuting(false);
563
599
  }
564
600
  };
565
- return (_jsxs(_Fragment, { children: [reasonPrompt.dialog, _jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) })] }));
601
+ if (stamp) {
602
+ return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: label, onOpenChange: (o) => {
603
+ if (o)
604
+ return;
605
+ setStamp(undefined);
606
+ onOpenChange(false);
607
+ onSuccess();
608
+ } }));
609
+ }
610
+ return (_jsxs(_Fragment, { children: [reasonPrompt.dialog, _jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsx(FormErrorBanner, { message: formError }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) })] }));
566
611
  }
567
612
  function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
568
613
  const { t, i18n } = useTranslation();
@@ -578,6 +623,10 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
578
623
  const [executing, setExecuting] = useState(false);
579
624
  // Per-field validation errors (localized), shown inline under each input.
580
625
  const [fieldErrors, setFieldErrors] = useState({});
626
+ // Persistent in-dialog banner for a failed submit (422 / 500) — the toast
627
+ // alone vanished and, for some failures, only the console had the cause.
628
+ const [formError, setFormError] = useState();
629
+ const [stamp, setStamp] = useState();
581
630
  // Related records to surface BELOW the form, as read-only context for the
582
631
  // record being acted on — e.g. the reception history of a transfer while
583
632
  // receiving against it. Sourced from the model's metadata.relations (the
@@ -610,6 +659,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
610
659
  if (open && action.fields) {
611
660
  setFormData(buildFieldDefaults(action.fields, record));
612
661
  setFieldErrors({});
662
+ setFormError(undefined);
613
663
  }
614
664
  }, [open, action.fields, record]);
615
665
  // Lines the list row does not carry: load them from the declared relation
@@ -696,6 +746,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
696
746
  };
697
747
  const lang = i18n.language;
698
748
  const handleActionError = (err) => {
749
+ setFormError(actionErrorBannerMessage(err, action.fields, t, lang));
699
750
  const labels = labelsForValidationFields(action.fields, t);
700
751
  const localized = localizeActionFieldErrors(err, action.fields, t, lang);
701
752
  if (localized) {
@@ -733,6 +784,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
733
784
  }
734
785
  }
735
786
  setFieldErrors({});
787
+ setFormError(undefined);
736
788
  setExecuting(true);
737
789
  try {
738
790
  let payload = { ...formData };
@@ -765,6 +817,11 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
765
817
  return;
766
818
  if (res.data.success) {
767
819
  toastServerSuccess(res.data, { t });
820
+ const result = extractStampResult(res.data);
821
+ if (result) {
822
+ setStamp(result);
823
+ return;
824
+ }
768
825
  onOpenChange(false);
769
826
  onSuccess();
770
827
  }
@@ -794,17 +851,26 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
794
851
  : hasLineItems
795
852
  ? '820px'
796
853
  : undefined;
797
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: tl(action.confirmMessage) })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
798
- const fullWidth = isLineItemsField(field) ||
799
- resolveWidget(field) === 'textarea' ||
800
- resolveWidget(field) === 'richtext';
801
- return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
802
- }), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] }), reasonPrompt.dialog] }) }));
854
+ if (stamp) {
855
+ return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: tl(action.label), onOpenChange: (o) => {
856
+ if (o)
857
+ return;
858
+ setStamp(undefined);
859
+ onOpenChange(false);
860
+ onSuccess();
861
+ } }));
862
+ }
863
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: tl(action.confirmMessage) })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: [_jsx(FormErrorBanner, { message: formError, className: "mb-3" }), _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
864
+ const fullWidth = isLineItemsField(field) ||
865
+ resolveWidget(field) === 'textarea' ||
866
+ resolveWidget(field) === 'richtext';
867
+ return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
868
+ }), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] })] }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] }), reasonPrompt.dialog] }) }));
803
869
  }
804
870
  // buildFieldDefaults seeds formData for a set of action fields, honoring the
805
871
  // same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
806
872
  // wizard steps and single-page forms initialize identically.
807
- function buildFieldDefaults(fields, record) {
873
+ export function buildFieldDefaults(fields, record) {
808
874
  const defaults = {};
809
875
  for (const field of fields) {
810
876
  if (isLineItemsField(field)) {
@@ -851,6 +917,8 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
851
917
  const [stepIndex, setStepIndex] = useState(0);
852
918
  const [formData, setFormData] = useState({});
853
919
  const [executing, setExecuting] = useState(false);
920
+ const [formError, setFormError] = useState();
921
+ const [stamp, setStamp] = useState();
854
922
  // Reset to the first step and seed defaults for EVERY step's fields whenever
855
923
  // the modal (re)opens, so accumulated values from a prior run don't leak.
856
924
  useEffect(() => {
@@ -859,6 +927,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
859
927
  const allFields = steps.flatMap((s) => s.fields ?? []);
860
928
  setFormData(buildFieldDefaults(allFields, record));
861
929
  setStepIndex(0);
930
+ setFormError(undefined);
862
931
  }, [open, action.steps, record]);
863
932
  const updateField = (key, value) => setFormData((prev) => ({ ...prev, [key]: value }));
864
933
  const step = steps[stepIndex];
@@ -890,6 +959,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
890
959
  }
891
960
  }
892
961
  setExecuting(true);
962
+ setFormError(undefined);
893
963
  try {
894
964
  const url = buildActionUrl(endpoint, model, record?.id, action.key);
895
965
  const auth = await supervisor.authorize({ label: tl(action.label), supervisorPolicy: action.supervisorPolicy }, { model, recordId: record?.id != null ? String(record.id) : undefined });
@@ -902,16 +972,24 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
902
972
  });
903
973
  if (!res)
904
974
  return;
975
+ const allFields = steps.flatMap(s => s.fields ?? []);
905
976
  if (res.data.success) {
906
977
  toastServerSuccess(res.data, { t });
978
+ const result = extractStampResult(res.data);
979
+ if (result) {
980
+ setStamp(result);
981
+ return;
982
+ }
907
983
  onOpenChange(false);
908
984
  onSuccess();
909
985
  }
910
986
  else {
911
- toastActionError({ response: { data: res.data } }, steps.flatMap(s => s.fields ?? []), t, i18n.language);
987
+ setFormError(actionErrorBannerMessage({ response: { data: res.data } }, allFields, t, i18n.language));
988
+ toastActionError({ response: { data: res.data } }, allFields, t, i18n.language);
912
989
  }
913
990
  }
914
991
  catch (err) {
992
+ setFormError(actionErrorBannerMessage(err, steps.flatMap(s => s.fields ?? []), t, i18n.language));
915
993
  toastActionError(err, steps.flatMap(s => s.fields ?? []), t, i18n.language);
916
994
  }
917
995
  finally {
@@ -920,12 +998,21 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
920
998
  };
921
999
  if (steps.length === 0)
922
1000
  return null;
923
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), _jsxs("div", { className: "pt-1", children: [_jsx(ProcessStepper, { steps: steps.map((s, i) => ({ key: String(i), label: s.title ? tl(s.title) : `${t('common.step', { defaultValue: 'Paso' })} ${i + 1}` })), activeIndex: stepIndex, onStepClick: i => setStepIndex(i) }), _jsxs(DialogDescription, { className: "pt-3", children: [t('common.step', { defaultValue: 'Paso' }), " ", stepIndex + 1, "/", steps.length, step?.title ? ` · ${tl(step.title)}` : ''] }), step?.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: tl(step.description) }))] })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsx(FieldGrid, { children: stepFields.map((field) => {
924
- const fullWidth = isLineItemsField(field) ||
925
- resolveWidget(field) === 'textarea' ||
926
- resolveWidget(field) === 'richtext';
927
- return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record)] }, field.key));
928
- }) }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [stepIndex > 0 ? (_jsx(Button, { variant: "outline", onClick: goBack, disabled: executing, children: t('common.back', { defaultValue: 'Atrás' }) })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') })), isLast ? (_jsxs(Button, { onClick: submit, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })) : (_jsx(Button, { onClick: goNext, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: t('common.next', { defaultValue: 'Siguiente' }) }))] }), reasonPrompt.dialog] }) }));
1001
+ if (stamp) {
1002
+ return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: tl(action.label), onOpenChange: (o) => {
1003
+ if (o)
1004
+ return;
1005
+ setStamp(undefined);
1006
+ onOpenChange(false);
1007
+ onSuccess();
1008
+ } }));
1009
+ }
1010
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), _jsxs("div", { className: "pt-1", children: [_jsx(ProcessStepper, { steps: steps.map((s, i) => ({ key: String(i), label: s.title ? tl(s.title) : `${t('common.step', { defaultValue: 'Paso' })} ${i + 1}` })), activeIndex: stepIndex, onStepClick: i => setStepIndex(i) }), _jsxs(DialogDescription, { className: "pt-3", children: [t('common.step', { defaultValue: 'Paso' }), " ", stepIndex + 1, "/", steps.length, step?.title ? ` · ${tl(step.title)}` : ''] }), step?.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: tl(step.description) }))] })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: [_jsx(FormErrorBanner, { message: formError, className: "mb-3" }), _jsx(FieldGrid, { children: stepFields.map((field) => {
1011
+ const fullWidth = isLineItemsField(field) ||
1012
+ resolveWidget(field) === 'textarea' ||
1013
+ resolveWidget(field) === 'richtext';
1014
+ return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record)] }, field.key));
1015
+ }) })] }), _jsxs(DialogFooter, { className: "shrink-0", children: [stepIndex > 0 ? (_jsx(Button, { variant: "outline", onClick: goBack, disabled: executing, children: t('common.back', { defaultValue: 'Atrás' }) })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') })), isLast ? (_jsxs(Button, { onClick: submit, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })) : (_jsx(Button, { onClick: goNext, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: t('common.next', { defaultValue: 'Siguiente' }) }))] }), reasonPrompt.dialog] }) }));
929
1016
  }
930
1017
  function seedOptionFromRecord(field, value, record) {
931
1018
  if (!record || !field.key.endsWith('_id'))
@@ -948,7 +1035,8 @@ function seedOptionFromRecord(field, value, record) {
948
1035
  icon: sib.icon,
949
1036
  };
950
1037
  }
951
- function renderField(field, value, onChange,
1038
+ /** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
1039
+ export function renderField(field, value, onChange,
952
1040
  // Full current form values — lets a line-items grid (and any cascading
953
1041
  // header picker) resolve a `dependsOn` reference against sibling header
954
1042
  // fields. Omitted by callers that have no surrounding form (the field is
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EA8EtD,CAAA"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAsGtD,CAAA"}
@@ -76,4 +76,28 @@ export const BUSINESS_COMPONENTS = [
76
76
  'recommendationsToLines',
77
77
  ],
78
78
  },
79
+ {
80
+ name: 'VehiclePicker',
81
+ value: 'value: VehicleResult | null',
82
+ events: ['onChange'],
83
+ permissions: ['<model>.create (alta rápida; default Vehicle → vehicle.create)'],
84
+ usedBy: ['POS', 'Recepción de vehículo', 'OT', 'Cotización', 'Pedido'],
85
+ helpers: ['describeVehicle'],
86
+ },
87
+ {
88
+ name: 'RelateDocuments',
89
+ value: 'value: RelatedDocument[] (related_document_id + relation_type)',
90
+ events: ['onChange'],
91
+ permissions: [],
92
+ usedBy: ['Nota de crédito', 'Devolución', 'Sustitución de CFDI', 'REP'],
93
+ helpers: ['serializeRelatedDocuments'],
94
+ },
95
+ {
96
+ name: 'PrintSendDialog',
97
+ value: 'document: PrintableDocument (+ open)',
98
+ events: ['onSend', 'onPrint', 'onOpenChange'],
99
+ permissions: [],
100
+ usedBy: ['Cotización', 'Factura', 'Nota de crédito', 'Pedido', 'OC'],
101
+ helpers: [],
102
+ },
79
103
  ];
@@ -16,4 +16,7 @@ export * from './return';
16
16
  export * from './return-wizard';
17
17
  export * from './inspection';
18
18
  export * from './inspection-checklist';
19
+ export * from './vehicle-picker';
20
+ export * from './relate-documents';
21
+ export * from './print-send-dialog';
19
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
@@ -16,3 +16,6 @@ export * from './return';
16
16
  export * from './return-wizard';
17
17
  export * from './inspection';
18
18
  export * from './inspection-checklist';
19
+ export * from './vehicle-picker';
20
+ export * from './relate-documents';
21
+ export * from './print-send-dialog';
@@ -0,0 +1,27 @@
1
+ export type SendChannel = 'email' | 'whatsapp';
2
+ export interface PrintableDocument {
3
+ folio: string;
4
+ /** URL del PDF ya generado. Sin ella no hay «Imprimir». */
5
+ pdfUrl?: string;
6
+ /** Texto de cabecera («Factura», «Cotización»). */
7
+ title?: string;
8
+ /** Destinos sugeridos (datos del cliente). */
9
+ email?: string;
10
+ phone?: string;
11
+ }
12
+ export interface PrintSendDialogProps {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ document: PrintableDocument;
16
+ /** Canales de envío disponibles. Default: ambos. Vacío = solo imprimir. */
17
+ channels?: SendChannel[];
18
+ /**
19
+ * Envío a cargo del host. Si rechaza, el mensaje se muestra en el diálogo.
20
+ * Sin `onSend` no se ofrece enviar.
21
+ */
22
+ onSend?: (channel: SendChannel, destination: string, document: PrintableDocument) => Promise<void> | void;
23
+ /** Reemplaza la impresión por defecto (abrir `pdfUrl` en otra pestaña). */
24
+ onPrint?: (document: PrintableDocument) => void;
25
+ }
26
+ export declare function PrintSendDialog({ open, onOpenChange, document: doc, channels, onSend, onPrint, }: PrintSendDialogProps): import("react").JSX.Element;
27
+ //# sourceMappingURL=print-send-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"print-send-dialog.d.ts","sourceRoot":"","sources":["../../src/business/print-send-dialog.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzG,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAA;CAClD;AAOD,wBAAgB,eAAe,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,QAAQ,EAAE,GAAG,EACb,QAAgC,EAChC,MAAM,EACN,OAAO,GACV,EAAE,oBAAoB,+BAmHtB"}
@@ -0,0 +1,55 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // PrintSendDialog — imprimir o enviar un documento ya emitido (cotización,
3
+ // factura, nota de crédito…). El SDK NO llama a un proveedor de mensajería: el
4
+ // host recibe `onSend(channel, destination, document)` y decide qué hacer.
5
+ // Imprimir usa la URL del PDF que pasa el host (o su propio `onPrint`).
6
+ import { useEffect, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Mail, MessageCircle, Printer, Loader2 } from 'lucide-react';
9
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, } from '@asteby/metacore-ui/primitives';
10
+ import { FormErrorBanner } from './feedback';
11
+ function defaultPrint(doc) {
12
+ if (!doc.pdfUrl || typeof window === 'undefined')
13
+ return;
14
+ window.open(doc.pdfUrl, '_blank', 'noopener,noreferrer');
15
+ }
16
+ export function PrintSendDialog({ open, onOpenChange, document: doc, channels = ['email', 'whatsapp'], onSend, onPrint, }) {
17
+ const { t } = useTranslation();
18
+ const [channel, setChannel] = useState(channels[0] ?? 'email');
19
+ const [destination, setDestination] = useState('');
20
+ const [sending, setSending] = useState(false);
21
+ const [sent, setSent] = useState(false);
22
+ const [error, setError] = useState();
23
+ const suggested = channel === 'email' ? doc.email : doc.phone;
24
+ useEffect(() => {
25
+ if (open) {
26
+ setDestination(suggested ?? '');
27
+ setSent(false);
28
+ setError(undefined);
29
+ }
30
+ }, [open, channel, suggested]);
31
+ const canSend = !!onSend && channels.length > 0;
32
+ const send = async () => {
33
+ const dest = destination.trim();
34
+ if (!onSend || !dest)
35
+ return;
36
+ setSending(true);
37
+ setError(undefined);
38
+ setSent(false);
39
+ try {
40
+ await onSend(channel, dest, doc);
41
+ setSent(true);
42
+ }
43
+ catch (e) {
44
+ const msg = e?.message;
45
+ setError(typeof msg === 'string' && msg
46
+ ? msg
47
+ : t('printSend.error', { defaultValue: 'No se pudo enviar el documento. Inténtalo de nuevo.' }));
48
+ }
49
+ finally {
50
+ setSending(false);
51
+ }
52
+ };
53
+ const channelLabel = (c) => c === 'email' ? t('printSend.email', { defaultValue: 'Correo' }) : t('printSend.whatsapp', { defaultValue: 'WhatsApp' });
54
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: [t('printSend.title', { defaultValue: 'Imprimir o enviar' }), doc.title ? ` · ${doc.title}` : ''] }), _jsx(DialogDescription, { children: doc.folio })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-start", disabled: !doc.pdfUrl && !onPrint, onClick: () => (onPrint ?? defaultPrint)(doc), children: [_jsx(Printer, { className: "mr-2 size-4", "aria-hidden": true }), t('printSend.print', { defaultValue: 'Imprimir' })] }), canSend && (_jsxs("div", { className: "space-y-2", children: [channels.length > 1 && (_jsx("div", { className: "flex gap-2", role: "group", "aria-label": t('printSend.channel', { defaultValue: 'Canal de envío' }), children: channels.map((c) => (_jsxs(Button, { type: "button", size: "sm", variant: c === channel ? 'default' : 'outline', "aria-pressed": c === channel, onClick: () => setChannel(c), children: [c === 'email' ? _jsx(Mail, { className: "mr-1.5 size-4", "aria-hidden": true }) : _jsx(MessageCircle, { className: "mr-1.5 size-4", "aria-hidden": true }), channelLabel(c)] }, c))) })), _jsx(Input, { value: destination, type: channel === 'email' ? 'email' : 'tel', "aria-label": t('printSend.destination', { defaultValue: 'Destino' }), placeholder: channel === 'email' ? 'cliente@ejemplo.com' : '+52 55 0000 0000', onChange: (e) => setDestination(e.target.value) }), _jsx(FormErrorBanner, { message: error }), sent && (_jsx("p", { role: "status", className: "text-sm text-emerald-600 dark:text-emerald-400", children: t('printSend.sent', { defaultValue: 'Enviado.' }) }))] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t('common.close', { defaultValue: 'Cerrar' }) }), canSend && (_jsxs(Button, { type: "button", onClick: send, disabled: sending || !destination.trim(), children: [sending && _jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }), t('printSend.send', { defaultValue: 'Enviar' })] }))] })] }) }));
55
+ }
@@ -0,0 +1,41 @@
1
+ export interface RelatableDocument {
2
+ id: string;
3
+ /** UUID fiscal (CFDI), cuando ya está timbrado. */
4
+ uuid?: string;
5
+ folio?: string;
6
+ /** Texto libre para mostrar («Factura A-120 · Cliente X»). */
7
+ label?: string;
8
+ total?: number;
9
+ }
10
+ export interface RelationTypeOption {
11
+ /** Clave que viaja en `relation_type` (p. ej. `"01"`). */
12
+ value: string;
13
+ label: string;
14
+ }
15
+ /** Un documento relacionado: lo que el formulario guarda. */
16
+ export interface RelatedDocument {
17
+ related_document_id: string;
18
+ relation_type: string;
19
+ /** Solo para mostrar. */
20
+ uuid?: string;
21
+ folio?: string;
22
+ label?: string;
23
+ }
24
+ export interface RelateDocumentsProps {
25
+ value: RelatedDocument[];
26
+ onChange: (value: RelatedDocument[]) => void;
27
+ /** Catálogo de tipos de relación (lo aporta el addon). */
28
+ relationTypes: RelationTypeOption[];
29
+ /** Búsqueda de documentos origen. Estable (useCallback). */
30
+ search: (q: string, signal: AbortSignal) => Promise<RelatableDocument[]>;
31
+ /** Tipo preseleccionado al agregar. Default: el primero del catálogo. */
32
+ defaultRelationType?: string;
33
+ /** Máximo de documentos relacionados. Sin tope por defecto. */
34
+ max?: number;
35
+ disabled?: boolean;
36
+ placeholder?: string;
37
+ }
38
+ export declare function RelateDocuments({ value, onChange, relationTypes, search, defaultRelationType, max, disabled, placeholder, }: RelateDocumentsProps): import("react").JSX.Element;
39
+ /** Valor a enviar al servidor: solo los datos, sin los campos de visualización. */
40
+ export declare function serializeRelatedDocuments(value: RelatedDocument[]): Array<Pick<RelatedDocument, 'related_document_id' | 'relation_type'>>;
41
+ //# sourceMappingURL=relate-documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relate-documents.d.ts","sourceRoot":"","sources":["../../src/business/relate-documents.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,4DAA4D;IAC5D,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACxE,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,GAAG,EACH,QAAQ,EACR,WAAW,GACd,EAAE,oBAAoB,+BA+GtB;AAED,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,qBAAqB,GAAG,eAAe,CAAC,CAAC,CAEzI"}
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // RelateDocuments — relaciona uno o más documentos origen (CFDI) desde un
3
+ // formulario: nota de crédito, devolución, sustitución. El valor es DATO
4
+ // (`related_document_id`, `relation_type`, UUID/folio para mostrar), no un flujo
5
+ // de un solo módulo. El SDK no embebe catálogos fiscales: los tipos de relación
6
+ // (p. ej. SAT 01/03/04) llegan por la prop `relationTypes`.
7
+ import { useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Link2, Search, X } from 'lucide-react';
10
+ import { Badge, Button, Input } from '@asteby/metacore-ui';
11
+ import { useAsyncSearch } from './use-async-search';
12
+ function docTitle(d) {
13
+ return d.label || d.folio || d.uuid || d.id || '';
14
+ }
15
+ export function RelateDocuments({ value, onChange, relationTypes, search, defaultRelationType, max, disabled, placeholder, }) {
16
+ const { t } = useTranslation();
17
+ const [text, setText] = useState('');
18
+ const [pendingType, setPendingType] = useState(defaultRelationType ?? relationTypes[0]?.value ?? '');
19
+ const { results, loading, error } = useAsyncSearch(text, search, { minChars: 2 });
20
+ const full = max != null && value.length >= max;
21
+ const typeLabel = (v) => relationTypes.find((r) => r.value === v)?.label ?? v;
22
+ const add = (d) => {
23
+ if (full || value.some((x) => x.related_document_id === d.id))
24
+ return;
25
+ onChange([
26
+ ...value,
27
+ { related_document_id: d.id, relation_type: pendingType, uuid: d.uuid, folio: d.folio, label: d.label },
28
+ ]);
29
+ setText('');
30
+ };
31
+ const remove = (id) => onChange(value.filter((x) => x.related_document_id !== id));
32
+ const setType = (id, relation_type) => onChange(value.map((x) => (x.related_document_id === id ? { ...x, relation_type } : x)));
33
+ const selectCls = 'h-9 rounded-md border bg-background px-2 text-sm';
34
+ return (_jsxs("div", { "data-slot": "relate-documents", className: "space-y-2", children: [value.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border", children: value.map((d) => (_jsxs("li", { className: "flex flex-wrap items-center gap-2 px-2 py-1.5", children: [_jsx(Link2, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: docTitle({ folio: d.folio, label: d.label, id: d.related_document_id }) }), d.uuid && _jsx("p", { className: "break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), _jsxs("select", { className: selectCls, disabled: disabled, value: d.relation_type, "aria-label": t('relateDocuments.type', { defaultValue: 'Tipo de relación' }), onChange: (e) => setType(d.related_document_id, e.target.value), children: [!relationTypes.some((r) => r.value === d.relation_type) && (_jsx("option", { value: d.relation_type, children: typeLabel(d.relation_type) })), relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value)))] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(d.related_document_id), "aria-label": t('relateDocuments.remove', { defaultValue: 'Quitar relación' }), children: _jsx(X, { className: "size-4" }) }))] }, d.related_document_id))) })), !full && !disabled && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", value: text, placeholder: placeholder ?? t('relateDocuments.placeholder', { defaultValue: 'Buscar por folio o UUID' }), onChange: (e) => setText(e.target.value) })] }), _jsx("select", { className: selectCls, value: pendingType, "aria-label": t('relateDocuments.newType', { defaultValue: 'Tipo para el documento nuevo' }), onChange: (e) => setPendingType(e.target.value), children: relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value))) })] }), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('relateDocuments.error', { defaultValue: 'No se pudo buscar documentos. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: t('relateDocuments.empty', { defaultValue: 'No encontramos ese documento.' }) })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((d) => {
35
+ const already = value.some((x) => x.related_document_id === d.id);
36
+ return (_jsx("li", { role: "option", "aria-selected": already, children: _jsxs("button", { type: "button", disabled: already, className: "flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left hover:bg-accent disabled:opacity-50", onClick: () => add(d), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: docTitle(d) }), d.uuid && _jsx("span", { className: "block break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), already && _jsx(Badge, { variant: "muted", children: t('relateDocuments.added', { defaultValue: 'Agregado' }) })] }) }, d.id));
37
+ }) }))] }))] }));
38
+ }
39
+ /** Valor a enviar al servidor: solo los datos, sin los campos de visualización. */
40
+ export function serializeRelatedDocuments(value) {
41
+ return value.map(({ related_document_id, relation_type }) => ({ related_document_id, relation_type }));
42
+ }