@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 45.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ce3dd3b: Action modal: `PrefillSpec.fromField` — a line-items grid can be seeded from the document picked in a sibling ref field. A `create`-placed action (no record) such as «Recibir OC» now fills its rows as soon as the user selects the purchase order: `$prefillFromRecord` names a one_to_many relation of the referenced model, `map`/`remaining`/`lock` work as in row actions, and clearing the selection empties the grid. Exports `prefillFromFieldRequests` and `prefillFromFieldRelationRequest`.
8
+ - 6ffdaa9: Motivo obligatorio al eliminar/cancelar (PER-4). Un modelo que declara `reason_required` en su manifest responde 422 `errors.reason` a un DELETE o acción sin motivo; `useReasonPrompt().run({ request })` prueba la petición, y si el servidor lo exige pregunta una vez (`ReasonPromptDialog`) y reintenta con `reason`. Ya cableado en la eliminación de fila, la eliminación masiva (un motivo para el lote) y los modales de acción (confirmación y formulario). `ActivityEvent.reason` se pinta en `ActivityDiff` («Motivo: …»). `useAddonSettings`/`useUpdateAddonSettings` aceptan `branchId` para leer/escribir los ajustes por sucursal (`settings[].scope: branch`, POS-1); `addonSettingsKey`/`addonSettingsUrl` lo respetan.
9
+ - cfafe4e: `ReturnWizard` («Devolver mercancía», benchmark DEV-2): un solo asistente para POS, Pedido, Factura, RMA y OT. Renglones con tope vendido − devuelto, condición vendible/defectuoso → stock/merma/garantía, paso «Recibir» dentro del asistente cuando no hay almacén (PIT-021), reembolso con `RefundDestination` (nuevo destino `transfer`), estados de RMA con motivo obligatorio al rechazar/cancelar (`canTransitionRma`) y NC CFDI con relación 01/03 automática (`creditNoteRelation`). Helpers puros: `computeReturnTotals`, `validateReturnChoices`, `serializeReturn`, `returnSteps`.
10
+
11
+ ### Patch Changes
12
+
13
+ - 0eb49fb: DynamicRelation: una sub-tabla cuya carga falla (403/500) ya no se muestra como «No hay registros relacionados»; muestra «Sin permiso…» o un aviso de error (strings opcionales `forbiddenState` / `errorState`). La columna `tags` con objetos usa su `label`/`name`/`key` en lugar de «[object Object]» (PIT-045, PIT-049).
14
+
15
+ ## 45.2.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 19870d6: Patrón «página de documento» (benchmark Pitsline §5/§6/§7, FAC-1/FAC-5/FAC-6). Un `DocumentSpec` de datos puros declara, por modelo, badges de varias máquinas de estado independientes (`StatusMachine`, catálogo semántico neutral/info/success/caution/warning/critical con ícono y texto: el color nunca es la única señal), la distribución de acciones por estado (1 primaria + hasta 3 secundarias + «Más…» + zona destructiva separada, con bloqueo explicado `blockedWhen`), botones inteligentes con contador, pestañas (`fields` / `records` / `slot`) y fuentes relacionadas (con relación de dos saltos `via`). Componentes: `DocumentPage`, `DocumentHeader`, `ActionBar`, `SmartButtons`, `StatusBadge`, `DocumentTimeline`, `CancelWithReason` (cancelación guiada de 2 pasos con catálogo de motivos, documento sustituto y consecuencias). Registro por modelo con `registerDocumentPage` / `getDocumentPage`; `pruneUnavailableActions` quita las acciones que el backend no declara.
20
+
21
+ ## 45.1.0
22
+
23
+ ### Minor Changes
24
+
25
+ - 610500d: Aprobación de supervisor con PIN: `ApprovalPinDialog` (motivo + PIN, error inline), `ApprovalGateProvider` / `useApprovalGate` (`requestApproval({policy, label, context})`: quien tiene `general.approve_<policy>` pasa sin prompt; los demás piden PIN a `/approvals/pin-grant`) y `approvalCapability`. `toastServerError` ya no pinta como error una escritura parada con `approval_required`: avisa "Enviado a aprobación" y, con el gate montado, ofrece "Aprobar con PIN" (`/approvals/:id/approve-pin`). Cierra el pendiente de docs/APPROVALS.md (los dispatchers del SDK apilaban el toast de error).
26
+ - 2c4c75e: Componentes de negocio compartidos con contrato estable (benchmark Pitsline §7, §6.4-6.7) en `@asteby/metacore-runtime-react`.
27
+
28
+ - Componentes: `CustomerPicker`, `ProductPicker` (medida de llanta, código de barras, SKU/clave de proveedor, variantes, existencia por almacén), `LineItemsEditor` (secciones/notas, política de sobreventa, `serializeLineItems` como única salida hacia el backend — corrige el guardado de renglones PIT-018), `PaymentCapture` (cobro mixto, cambio solo en efectivo, referencia y cuenta) y `RefundDestination` (efectivo, tarjeta, monedero, saldo a favor, NC CFDI).
29
+ - Utilidades: formateador único `formatMoney` / `formatDate` / `formatDateTime` / `createFormatter` / `useFormatter` (es-MX, moneda y zona de la org, sin mezclar locale — PIT-023/025); `mapApiError` (409/422 → mensaje por campo + banner, PIT-002/003/013/021/029); `notify`, `SystemNoticeBanner`, `FormErrorBanner`, `ACTION_SAFE_TOASTER_PROPS` (avisos que no tapan acciones, PIT-005) y `EmptyState` accionable.
30
+ - `BUSINESS_COMPONENTS`: catálogo legible por máquina (props, eventos, permisos, dónde se usa). Guía en `docs/business-components.md`.
31
+
3
32
  ## 45.0.0
4
33
 
5
34
  ### Minor Changes
@@ -16,6 +16,16 @@ export interface PrefillSpec {
16
16
  * flow (which carries no prefill spec); only the prefilled action locks them.
17
17
  */
18
18
  lock?: string[];
19
+ /**
20
+ * Key of a SIBLING action field (a dynamic_select with a `ref`) whose selected
21
+ * value is the id of the document the rows are seeded from. Without it the
22
+ * rows come from the record the action was opened on (row actions); with it
23
+ * a `create`-placed action — which has no record — seeds its grid as soon as
24
+ * the user picks the source document (e.g. a goods receipt picking its
25
+ * purchase order). `$prefillFromRecord` then names a one_to_many relation of
26
+ * the REFERENCED model. Clearing the selection empties the grid.
27
+ */
28
+ fromField?: string;
19
29
  }
20
30
  export declare function isPrefillSpec(v: unknown): v is PrefillSpec;
21
31
  export declare function applyPrefillLock(field: ActionFieldDef): ActionFieldDef;
@@ -33,6 +43,25 @@ export declare function prefillRelationRequests(fields: ActionFieldDef[] | undef
33
43
  foreign_key?: string;
34
44
  scope?: Record<string, unknown>;
35
45
  }>): PrefillRelationRequest[];
46
+ export interface PrefillFromFieldRequest {
47
+ fieldKey: string;
48
+ spec: PrefillSpec;
49
+ /** Model key the sibling field references (its `ref`). */
50
+ refModel: string;
51
+ /** Selected document id; '' when nothing is selected (grid must clear). */
52
+ refId: string;
53
+ }
54
+ export declare function prefillFromFieldRequests(fields: ActionFieldDef[] | undefined, formData: Record<string, any>): PrefillFromFieldRequest[];
55
+ export declare function prefillFromFieldRelationRequest(req: PrefillFromFieldRequest, relations: Array<{
56
+ name?: string;
57
+ kind?: string;
58
+ through?: string;
59
+ foreign_key?: string;
60
+ scope?: Record<string, unknown>;
61
+ }>): {
62
+ endpoint: string;
63
+ params: Record<string, string | number>;
64
+ } | null;
36
65
  export declare function unwrapRecordScalar(value: unknown): unknown;
37
66
  export declare function readRecordPath(record: any, path: string): unknown;
38
67
  /** Scalar seed for one action field from the row being acted on. */
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4EA,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;CAClB;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,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,GACZ,EAAE,gBAAgB,sCAmFlB;AAED,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,QAAS,CAAA"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // ActionModalDispatcher — renders the right modal for a custom action:
3
3
  // 1) Custom component from the SDK registry → use it
4
4
  // 2) action.modal set but no registered component → loading state while the
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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, useSyncExternalStore } from 'react';
18
+ import { useState, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
19
19
  import { useTranslation } from 'react-i18next';
20
20
  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
21
  import { Loader2 } from 'lucide-react';
@@ -27,6 +27,7 @@ import { validateValues, bagHasErrors } from './validator';
27
27
  import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
28
28
  import { validationCatalog } from './validation-catalog';
29
29
  import { useApi } from './api-context';
30
+ import { useReasonPrompt } from './reason-prompt';
30
31
  import { buildRelationFilterParams } from './dynamic-relation-helpers';
31
32
  import { DynamicIcon } from './dynamic-icon';
32
33
  import { DynamicLineItems } from './dynamic-line-items';
@@ -130,6 +131,45 @@ export function prefillRelationRequests(fields, record, relations) {
130
131
  }
131
132
  return out;
132
133
  }
134
+ export function prefillFromFieldRequests(fields, formData) {
135
+ const out = [];
136
+ if (!fields)
137
+ return out;
138
+ for (const field of fields) {
139
+ if (!isLineItemsField(field))
140
+ continue;
141
+ const spec = lineItemsDefault(field);
142
+ if (!isPrefillSpec(spec) || !spec.fromField)
143
+ continue;
144
+ const source = fields.find((f) => f.key === spec.fromField);
145
+ const refModel = source ? getFieldRef(source) : undefined;
146
+ if (!refModel)
147
+ continue;
148
+ const raw = unwrapRecordScalar(formData?.[spec.fromField]);
149
+ const refId = raw === undefined || raw === null ? '' : String(raw);
150
+ out.push({ fieldKey: field.key, spec, refModel, refId });
151
+ }
152
+ return out;
153
+ }
154
+ // prefillFromFieldRelationRequest resolves the child-rows request for a
155
+ // selected document, from the referenced model's declared relations. Null when
156
+ // the relation is missing or not a plain one_to_many.
157
+ export function prefillFromFieldRelationRequest(req, relations) {
158
+ if (!req.refId)
159
+ return null;
160
+ const rel = (relations || []).find((r) => r?.name === req.spec.$prefillFromRecord && (r.kind ?? 'one_to_many') === 'one_to_many');
161
+ if (!rel?.through || !rel.foreign_key)
162
+ return null;
163
+ const scope = {};
164
+ for (const [k, v] of Object.entries(rel.scope ?? {})) {
165
+ if (v !== undefined && v !== null)
166
+ scope[k] = String(v);
167
+ }
168
+ return {
169
+ endpoint: `/data/${rel.through}`,
170
+ params: { ...buildRelationFilterParams(rel.foreign_key, req.refId, scope), per_page: 200 },
171
+ };
172
+ }
133
173
  // ---- scalar prefill from the acted-on record --------------------------------
134
174
  //
135
175
  // Row actions (stamp / refactura / cancel-with-reason) should open with the
@@ -481,6 +521,9 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
481
521
  const { t, i18n } = useTranslation();
482
522
  const api = useApi();
483
523
  const [executing, setExecuting] = useState(false);
524
+ // Actions listed in the model's `reason_required.actions` (cancel, void…)
525
+ // answer 422 errors.reason without a motive; the prompt asks and retries.
526
+ const reasonPrompt = useReasonPrompt();
484
527
  // `action.label` is an addon-contributed i18n key; its locale bundle loads
485
528
  // asynchronously, so translate at render (defaultValue keeps an already
486
529
  // localized label unchanged).
@@ -489,7 +532,14 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
489
532
  setExecuting(true);
490
533
  try {
491
534
  const url = buildActionUrl(endpoint, model, record.id, action.key);
492
- const res = await api.post(url, {});
535
+ const res = await reasonPrompt.run({
536
+ title: label,
537
+ description: t('actions.reason_desc', { defaultValue: 'Esta acción pide el motivo. Queda en la bitácora del documento.' }),
538
+ request: (reason) => api.post(url, reason ? { reason } : {}),
539
+ });
540
+ // Operator cancelled the reason prompt: nothing ran.
541
+ if (!res)
542
+ return;
493
543
  if (res.data.success) {
494
544
  toastServerSuccess(res.data, { t });
495
545
  onOpenChange(false);
@@ -506,7 +556,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
506
556
  setExecuting(false);
507
557
  }
508
558
  };
509
- return (_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] })] })] }) }));
559
+ 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] })] })] }) })] }));
510
560
  }
511
561
  function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
512
562
  const { t, i18n } = useTranslation();
@@ -515,6 +565,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
515
565
  // defaultValue keeps an already-localized string unchanged.
516
566
  const tl = (s) => t(s, { defaultValue: s });
517
567
  const api = useApi();
568
+ const reasonPrompt = useReasonPrompt();
518
569
  const branchGate = useBranchCreateGate();
519
570
  const [formData, setFormData] = useState({});
520
571
  const [executing, setExecuting] = useState(false);
@@ -580,6 +631,58 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
580
631
  cancelled = true;
581
632
  };
582
633
  }, [open, action.fields, record, relations, api]);
634
+ // Lines seeded from a document picked in a sibling field (PrefillSpec.fromField).
635
+ // Re-runs only when the picked id changes; each pick loads the referenced
636
+ // model's relations (cached per model for the life of the modal) and then
637
+ // the child rows.
638
+ const fromFieldReqs = useMemo(() => prefillFromFieldRequests(action.fields, formData), [action.fields, formData]);
639
+ const fromFieldSig = fromFieldReqs.map((r) => `${r.fieldKey}=${r.refModel}:${r.refId}`).join('|');
640
+ const refRelationsCache = useRef(new Map());
641
+ useEffect(() => {
642
+ if (!open || fromFieldReqs.length === 0)
643
+ return;
644
+ let cancelled = false;
645
+ for (const req of fromFieldReqs) {
646
+ if (!req.refId) {
647
+ setFormData((prev) => Array.isArray(prev[req.fieldKey]) && prev[req.fieldKey].length === 0
648
+ ? prev
649
+ : { ...prev, [req.fieldKey]: [] });
650
+ continue;
651
+ }
652
+ let relsPromise = refRelationsCache.current.get(req.refModel);
653
+ if (!relsPromise) {
654
+ relsPromise = api
655
+ .get(`/metadata/table/${req.refModel}`)
656
+ .then((res) => {
657
+ const rels = res?.data?.relations ?? res?.data?.data?.relations ?? [];
658
+ return Array.isArray(rels) ? rels : [];
659
+ })
660
+ .catch(() => []);
661
+ refRelationsCache.current.set(req.refModel, relsPromise);
662
+ }
663
+ relsPromise
664
+ .then((rels) => {
665
+ const call = prefillFromFieldRelationRequest(req, rels);
666
+ if (!call)
667
+ return;
668
+ return api.get(call.endpoint, { params: call.params }).then((res) => {
669
+ if (cancelled)
670
+ return;
671
+ const rows = res?.data?.data ?? res?.data ?? [];
672
+ if (!Array.isArray(rows))
673
+ return;
674
+ const built = buildPrefillRows(req.spec, { [req.spec.$prefillFromRecord]: rows });
675
+ setFormData((prev) => ({ ...prev, [req.fieldKey]: built }));
676
+ });
677
+ })
678
+ .catch(() => { });
679
+ }
680
+ return () => {
681
+ cancelled = true;
682
+ };
683
+ // fromFieldSig captures every dependency of fromFieldReqs that matters.
684
+ // eslint-disable-next-line react-hooks/exhaustive-deps
685
+ }, [open, fromFieldSig, api]);
583
686
  const updateField = (key, value) => {
584
687
  setFormData((prev) => ({ ...prev, [key]: value }));
585
688
  setFieldErrors((prev) => clearFieldErrorTree(prev, key));
@@ -643,7 +746,13 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
643
746
  payload = { ...payload, branch_id: branchId };
644
747
  }
645
748
  const url = buildActionUrl(endpoint, model, record?.id, action.key);
646
- const res = await api.post(url, payload);
749
+ const res = await reasonPrompt.run({
750
+ title: tl(action.label),
751
+ description: t('actions.reason_desc', { defaultValue: 'Esta acción pide el motivo. Queda en la bitácora del documento.' }),
752
+ request: (reason) => api.post(url, reason ? { ...payload, reason } : payload),
753
+ });
754
+ if (!res)
755
+ return;
647
756
  if (res.data.success) {
648
757
  toastServerSuccess(res.data, { t });
649
758
  onOpenChange(false);
@@ -680,7 +789,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
680
789
  resolveWidget(field) === 'textarea' ||
681
790
  resolveWidget(field) === 'richtext';
682
791
  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));
683
- }), 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)] })] })] }) }));
792
+ }), 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] }) }));
684
793
  }
685
794
  // buildFieldDefaults seeds formData for a set of action fields, honoring the
686
795
  // same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
@@ -43,6 +43,8 @@ export interface ActivityEvent {
43
43
  to: unknown;
44
44
  }> | null;
45
45
  summary?: string | null;
46
+ /** Motive the operator stated for a delete / reason_required action (PER-4). */
47
+ reason?: string | null;
46
48
  occurred_at: string;
47
49
  }
48
50
  export interface ActivityDiffProps {
@@ -1 +1 @@
1
- {"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AA8GD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA4KpD,CAAA"}
1
+ {"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AA8GD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmLpD,CAAA"}
@@ -142,10 +142,10 @@ export const ActivityDiff = ({ event, columns, timeZone, currency, locale = 'es'
142
142
  const isCreated = variant === 'created';
143
143
  const isDeleted = variant === 'deleted';
144
144
  const variantBadge = VARIANT_BADGE[variant] ?? VARIANT_BADGE.other;
145
- if (allKeys.length === 0 && !event.summary) {
145
+ if (allKeys.length === 0 && !event.summary && !event.reason) {
146
146
  return (_jsx("div", { className: cn('text-sm text-muted-foreground italic py-1', className), children: "Sin campos registrados." }));
147
147
  }
148
- return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: "outline", className: cn('text-xs font-medium px-2 py-0.5', variantBadge.className), children: variantBadge.label || event.action }), changed.size > 0 && variant === 'updated' && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [changed.size, " campo", changed.size !== 1 ? 's' : '', " modificado", changed.size !== 1 ? 's' : ''] })), allKeys.length > 0 && variant === 'updated' && (_jsx("button", { type: "button", onClick: () => setShowOnlyChanged((v) => !v), className: "ml-auto text-xs text-primary hover:underline", children: showOnlyChanged ? `Ver todos (${allKeys.length})` : 'Solo cambios' }))] }), event.summary && (_jsx("p", { className: "text-sm text-muted-foreground italic", children: event.summary })), displayedKeys.length > 0 && (_jsxs("div", { className: "rounded-lg border border-border/60 overflow-hidden text-sm", children: [isCreated || isDeleted ? (_jsxs("div", { className: "grid grid-cols-[1fr_2fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: isDeleted ? 'Valor anterior' : 'Valor' })] })) : (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: "Antes" }), _jsx("span", { children: "Despu\u00E9s" })] })), displayedKeys.map((key, idx) => {
148
+ return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: "outline", className: cn('text-xs font-medium px-2 py-0.5', variantBadge.className), children: variantBadge.label || event.action }), changed.size > 0 && variant === 'updated' && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [changed.size, " campo", changed.size !== 1 ? 's' : '', " modificado", changed.size !== 1 ? 's' : ''] })), allKeys.length > 0 && variant === 'updated' && (_jsx("button", { type: "button", onClick: () => setShowOnlyChanged((v) => !v), className: "ml-auto text-xs text-primary hover:underline", children: showOnlyChanged ? `Ver todos (${allKeys.length})` : 'Solo cambios' }))] }), event.reason && (_jsxs("p", { className: "text-sm", "data-testid": "activity-reason", children: [_jsx("span", { className: "font-medium", children: "Motivo:" }), " ", event.reason] })), event.summary && (_jsx("p", { className: "text-sm text-muted-foreground italic", children: event.summary })), displayedKeys.length > 0 && (_jsxs("div", { className: "rounded-lg border border-border/60 overflow-hidden text-sm", children: [isCreated || isDeleted ? (_jsxs("div", { className: "grid grid-cols-[1fr_2fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: isDeleted ? 'Valor anterior' : 'Valor' })] })) : (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: "Antes" }), _jsx("span", { children: "Despu\u00E9s" })] })), displayedKeys.map((key, idx) => {
149
149
  const col = resolveColumn(key, columns);
150
150
  const label = resolveLabel(key, columns);
151
151
  const isChanged = changed.has(key);
@@ -0,0 +1,16 @@
1
+ /** Window event toastServerError fires for a parked `approval_required` write. */
2
+ export declare const APPROVAL_PARKED_EVENT = "metacore:approval-parked";
3
+ /** Fired after a parked request was approved (and replayed) with a PIN. */
4
+ export declare const APPROVAL_DECIDED_EVENT = "metacore:approval-decided";
5
+ /** @internal provider bookkeeping. */
6
+ export declare function gateMountedDelta(d: number): void;
7
+ /** True while an <ApprovalGateProvider> is mounted (toastServerError offers the PIN action only then). */
8
+ export declare function approvalGateAvailable(): boolean;
9
+ export interface ApprovalRequiredInfo {
10
+ requestId: string;
11
+ label?: string;
12
+ roles: string[];
13
+ }
14
+ /** Extracts the parked-request info from an axios error / response body, or null. */
15
+ export declare function approvalRequiredInfo(err: unknown): ApprovalRequiredInfo | null;
16
+ //# sourceMappingURL=approval-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-events.d.ts","sourceRoot":"","sources":["../src/approval-events.ts"],"names":[],"mappings":"AAIA,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,6BAA6B,CAAA;AAC/D,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,8BAA8B,CAAA;AAGjE,sCAAsC;AACtC,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED,MAAM,WAAW,oBAAoB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAU9E"}
@@ -0,0 +1,27 @@
1
+ // approval-events — dependency-free glue between server-error.ts (which sees a
2
+ // parked `approval_required` write) and <ApprovalGateProvider> (which owns the
3
+ // PIN dialog). Kept apart so the error toaster never imports React/UI code.
4
+ /** Window event toastServerError fires for a parked `approval_required` write. */
5
+ export const APPROVAL_PARKED_EVENT = 'metacore:approval-parked';
6
+ /** Fired after a parked request was approved (and replayed) with a PIN. */
7
+ export const APPROVAL_DECIDED_EVENT = 'metacore:approval-decided';
8
+ let gateMounted = 0;
9
+ /** @internal provider bookkeeping. */
10
+ export function gateMountedDelta(d) {
11
+ gateMounted += d;
12
+ }
13
+ /** True while an <ApprovalGateProvider> is mounted (toastServerError offers the PIN action only then). */
14
+ export function approvalGateAvailable() {
15
+ return gateMounted > 0;
16
+ }
17
+ /** Extracts the parked-request info from an axios error / response body, or null. */
18
+ export function approvalRequiredInfo(err) {
19
+ const data = (err?.response?.data ?? err);
20
+ if (data?.error?.code !== 'approval_required')
21
+ return null;
22
+ return {
23
+ requestId: data.meta?.approval_request_id ?? '',
24
+ label: data.meta?.approval_label,
25
+ roles: data.meta?.approval_roles ?? [],
26
+ };
27
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ export { APPROVAL_DECIDED_EVENT, APPROVAL_PARKED_EVENT, approvalGateAvailable, approvalRequiredInfo, type ApprovalRequiredInfo, } from './approval-events';
3
+ /** Capability that authorizes `policy` (and lets its holder skip the prompt). */
4
+ export declare function approvalCapability(policy: string): string;
5
+ export interface ApprovalGrant {
6
+ /** Audit row id (ApprovalRequest.kind=pin); null when the caller bypassed. */
7
+ id: string | null;
8
+ policy: string;
9
+ bypassed: boolean;
10
+ reason?: string;
11
+ }
12
+ export interface RequestApprovalOptions {
13
+ /** Policy key: oversell | discount | price_below_min | refund | cancel_cfdi | inventory_adjust | <addon-declared>. */
14
+ policy: string;
15
+ label: string;
16
+ description?: React.ReactNode;
17
+ /** Free-form audit data stored with the grant (product, qty, before → after…). */
18
+ context?: Record<string, unknown>;
19
+ model?: string;
20
+ recordId?: string;
21
+ addonKey?: string;
22
+ }
23
+ export interface ApprovalGateValue {
24
+ /** Resolves the grant, or null when the operator cancelled. */
25
+ requestApproval: (opts: RequestApprovalOptions) => Promise<ApprovalGrant | null>;
26
+ /** True when the current user may do `policy` without asking anyone. */
27
+ canSelfApprove: (policy: string) => boolean;
28
+ }
29
+ export declare function ApprovalGateProvider({ children }: {
30
+ children: React.ReactNode;
31
+ }): React.JSX.Element;
32
+ /**
33
+ * Access the gate. Without a provider the gate fails CLOSED for inline requests
34
+ * (resolves null) — never silently authorizes — but honours the capability
35
+ * bypass, and `canSelfApprove` follows `useCan()` (always-true with no
36
+ * <PermissionsProvider>, i.e. legacy hosts keep today's behaviour).
37
+ */
38
+ export declare function useApprovalGate(): ApprovalGateValue;
39
+ //# sourceMappingURL=approval-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-gate.d.ts","sourceRoot":"","sources":["../src/approval-gate.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAuF,MAAM,OAAO,CAAA;AAa3G,OAAO,EACH,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,oBAAoB,GAC5B,MAAM,mBAAmB,CAAA;AAE1B,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,MAAM,WAAW,aAAa;IAC1B,8EAA8E;IAC9E,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACnC,sHAAsH;IACtH,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,EAAE,CAAC,IAAI,EAAE,sBAAsB,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAChF,wEAAwE;IACxE,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CAC9C;AAQD,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,qBAuF/E;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,iBAAiB,CAYnD"}
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // ApprovalGate — one place for "this needs a supervisor" in hosts that mount an
3
+ // ApiProvider. It answers two situations with the same PIN dialog:
4
+ //
5
+ // 1. INLINE policy (POS oversell, discount over the cap, refund…): the client
6
+ // calls `requestApproval({policy, …})`. A user who already holds the policy's
7
+ // `general.approve_<policy>` capability passes straight through (`bypassed`);
8
+ // everyone else gets the dialog, which posts `/approvals/pin-grant`.
9
+ // 2. PARKED request: a write answered 422 `approval_required`. toastServerError
10
+ // shows "Enviado a aprobación" with an "Aprobar con PIN" action that opens the
11
+ // dialog against `/approvals/:id/approve-pin` (kernel approvals_pin.go).
12
+ //
13
+ // Federated addons with their own HTTP client use <ApprovalPinDialog> directly
14
+ // (approval-pin-dialog.tsx) and post to the same endpoints.
15
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { useTranslation } from 'react-i18next';
17
+ import { toast } from 'sonner';
18
+ import { useApi } from './api-context';
19
+ import { useCan } from './permissions-context';
20
+ import { ApprovalPinDialog } from './approval-pin-dialog';
21
+ import { APPROVAL_DECIDED_EVENT, APPROVAL_PARKED_EVENT, gateMountedDelta, } from './approval-events';
22
+ export { APPROVAL_DECIDED_EVENT, APPROVAL_PARKED_EVENT, approvalGateAvailable, approvalRequiredInfo, } from './approval-events';
23
+ /** Capability that authorizes `policy` (and lets its holder skip the prompt). */
24
+ export function approvalCapability(policy) {
25
+ return `general.approve_${policy}`;
26
+ }
27
+ const ApprovalGateContext = createContext(null);
28
+ export function ApprovalGateProvider({ children }) {
29
+ const { t } = useTranslation();
30
+ const api = useApi();
31
+ const can = useCan();
32
+ const [pending, setPending] = useState(null);
33
+ const pendingRef = useRef(null);
34
+ pendingRef.current = pending;
35
+ useEffect(() => {
36
+ gateMountedDelta(1);
37
+ const onParked = (e) => {
38
+ const info = e.detail;
39
+ if (info?.requestId)
40
+ setPending({ kind: 'parked', info });
41
+ };
42
+ window.addEventListener(APPROVAL_PARKED_EVENT, onParked);
43
+ return () => {
44
+ gateMountedDelta(-1);
45
+ window.removeEventListener(APPROVAL_PARKED_EVENT, onParked);
46
+ };
47
+ }, []);
48
+ const canSelfApprove = useCallback((policy) => can(approvalCapability(policy)), [can]);
49
+ const requestApproval = useCallback((opts) => {
50
+ if (can(approvalCapability(opts.policy))) {
51
+ return Promise.resolve({ id: null, policy: opts.policy, bypassed: true });
52
+ }
53
+ return new Promise((resolve) => {
54
+ pendingRef.current?.kind === 'inline' && pendingRef.current.resolve(null);
55
+ setPending({ kind: 'inline', opts, resolve });
56
+ });
57
+ }, [can]);
58
+ const close = () => {
59
+ const p = pendingRef.current;
60
+ if (p?.kind === 'inline')
61
+ p.resolve(null);
62
+ setPending(null);
63
+ };
64
+ const submit = async ({ pin, reason }) => {
65
+ const p = pendingRef.current;
66
+ if (!p)
67
+ return;
68
+ if (p.kind === 'inline') {
69
+ const res = await api.post('/approvals/pin-grant', {
70
+ policy: p.opts.policy,
71
+ label: p.opts.label,
72
+ pin,
73
+ reason,
74
+ context: p.opts.context,
75
+ model: p.opts.model,
76
+ record_id: p.opts.recordId,
77
+ addon_key: p.opts.addonKey,
78
+ });
79
+ const row = res.data?.data;
80
+ p.resolve({ id: row?.id ?? null, policy: p.opts.policy, bypassed: false, reason });
81
+ }
82
+ else {
83
+ await api.post(`/approvals/${p.info.requestId}/approve-pin`, { pin, reason });
84
+ toast.success(t('approvals.approved_applied', { defaultValue: 'Aprobado y aplicado' }));
85
+ window.dispatchEvent(new CustomEvent(APPROVAL_DECIDED_EVENT, { detail: p.info }));
86
+ }
87
+ setPending(null);
88
+ };
89
+ const value = useMemo(() => ({ requestApproval, canSelfApprove }), [requestApproval, canSelfApprove]);
90
+ const title = pending?.kind === 'inline'
91
+ ? pending.opts.label
92
+ : pending?.kind === 'parked'
93
+ ? pending.info.label || t('approvals.parked_title', { defaultValue: 'Requiere aprobación' })
94
+ : '';
95
+ return (_jsxs(ApprovalGateContext.Provider, { value: value, children: [children, _jsx(ApprovalPinDialog, { open: pending !== null, title: title, description: pending?.kind === 'inline' ? pending.opts.description : undefined, onSubmit: submit, onCancel: close })] }));
96
+ }
97
+ /**
98
+ * Access the gate. Without a provider the gate fails CLOSED for inline requests
99
+ * (resolves null) — never silently authorizes — but honours the capability
100
+ * bypass, and `canSelfApprove` follows `useCan()` (always-true with no
101
+ * <PermissionsProvider>, i.e. legacy hosts keep today's behaviour).
102
+ */
103
+ export function useApprovalGate() {
104
+ const ctx = useContext(ApprovalGateContext);
105
+ const can = useCan();
106
+ return useMemo(() => ctx ?? {
107
+ canSelfApprove: (policy) => can(approvalCapability(policy)),
108
+ requestApproval: async (opts) => can(approvalCapability(opts.policy)) ? { id: null, policy: opts.policy, bypassed: true } : null,
109
+ }, [ctx, can]);
110
+ }
@@ -0,0 +1,25 @@
1
+ export interface ApprovalPinSubmit {
2
+ pin: string;
3
+ reason: string;
4
+ }
5
+ export interface ApprovalPinDialogProps {
6
+ open: boolean;
7
+ /** Headline, e.g. "Venta sin existencias". */
8
+ title: string;
9
+ /** What is being authorized, in the operator's words. */
10
+ description?: React.ReactNode;
11
+ /** Called with the typed PIN + reason. Throw (or reject) to keep the dialog open. */
12
+ onSubmit: (v: ApprovalPinSubmit) => Promise<void>;
13
+ onCancel: () => void;
14
+ /** Optional secondary action, e.g. "Enviar a la bandeja" (async request). */
15
+ secondaryAction?: {
16
+ label: string;
17
+ onClick: () => void;
18
+ };
19
+ }
20
+ /** Best human message out of an axios-style error carrying the kernel envelope. */
21
+ export declare function approvalErrorMessage(err: unknown, t: (k: string, o?: {
22
+ defaultValue?: string;
23
+ }) => string): string;
24
+ export declare function ApprovalPinDialog({ open, title, description, onSubmit, onCancel, secondaryAction }: ApprovalPinDialogProps): import("react").JSX.Element;
25
+ //# sourceMappingURL=approval-pin-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-pin-dialog.d.ts","sourceRoot":"","sources":["../src/approval-pin-dialog.tsx"],"names":[],"mappings":"AAwBA,MAAM,WAAW,iBAAiB;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,qFAAqF;IACrF,QAAQ,EAAE,CAAC,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,6EAA6E;IAC7E,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;CAC3D;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,GAAG,MAAM,CAalH;AAED,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,eAAe,EAAE,EAAE,sBAAsB,+BAuG1H"}