@asteby/metacore-runtime-react 45.0.0 → 45.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/approval-events.d.ts +16 -0
  3. package/dist/approval-events.d.ts.map +1 -0
  4. package/dist/approval-events.js +27 -0
  5. package/dist/approval-gate.d.ts +39 -0
  6. package/dist/approval-gate.d.ts.map +1 -0
  7. package/dist/approval-gate.js +110 -0
  8. package/dist/approval-pin-dialog.d.ts +25 -0
  9. package/dist/approval-pin-dialog.d.ts.map +1 -0
  10. package/dist/approval-pin-dialog.js +66 -0
  11. package/dist/business/catalog.d.ts +16 -0
  12. package/dist/business/catalog.d.ts.map +1 -0
  13. package/dist/business/catalog.js +42 -0
  14. package/dist/business/customer-picker.d.ts +39 -0
  15. package/dist/business/customer-picker.d.ts.map +1 -0
  16. package/dist/business/customer-picker.js +51 -0
  17. package/dist/business/feedback.d.ts +72 -0
  18. package/dist/business/feedback.d.ts.map +1 -0
  19. package/dist/business/feedback.js +52 -0
  20. package/dist/business/field-errors.d.ts +26 -0
  21. package/dist/business/field-errors.d.ts.map +1 -0
  22. package/dist/business/field-errors.js +59 -0
  23. package/dist/business/format.d.ts +49 -0
  24. package/dist/business/format.d.ts.map +1 -0
  25. package/dist/business/format.js +128 -0
  26. package/dist/business/index.d.ts +14 -0
  27. package/dist/business/index.d.ts.map +1 -0
  28. package/dist/business/index.js +13 -0
  29. package/dist/business/line-items-editor.d.ts +25 -0
  30. package/dist/business/line-items-editor.d.ts.map +1 -0
  31. package/dist/business/line-items-editor.js +36 -0
  32. package/dist/business/line-items.d.ts +69 -0
  33. package/dist/business/line-items.d.ts.map +1 -0
  34. package/dist/business/line-items.js +133 -0
  35. package/dist/business/payment-capture.d.ts +21 -0
  36. package/dist/business/payment-capture.d.ts.map +1 -0
  37. package/dist/business/payment-capture.js +24 -0
  38. package/dist/business/payment.d.ts +61 -0
  39. package/dist/business/payment.d.ts.map +1 -0
  40. package/dist/business/payment.js +74 -0
  41. package/dist/business/product-picker.d.ts +24 -0
  42. package/dist/business/product-picker.d.ts.map +1 -0
  43. package/dist/business/product-picker.js +48 -0
  44. package/dist/business/product-search.d.ts +70 -0
  45. package/dist/business/product-search.d.ts.map +1 -0
  46. package/dist/business/product-search.js +51 -0
  47. package/dist/business/refund-destination.d.ts +19 -0
  48. package/dist/business/refund-destination.d.ts.map +1 -0
  49. package/dist/business/refund-destination.js +31 -0
  50. package/dist/business/refund.d.ts +29 -0
  51. package/dist/business/refund.d.ts.map +1 -0
  52. package/dist/business/refund.js +43 -0
  53. package/dist/business/use-async-search.d.ts +10 -0
  54. package/dist/business/use-async-search.d.ts.map +1 -0
  55. package/dist/business/use-async-search.js +38 -0
  56. package/dist/document/action-bar.d.ts +12 -0
  57. package/dist/document/action-bar.d.ts.map +1 -0
  58. package/dist/document/action-bar.js +39 -0
  59. package/dist/document/cancel-with-reason.d.ts +47 -0
  60. package/dist/document/cancel-with-reason.d.ts.map +1 -0
  61. package/dist/document/cancel-with-reason.js +52 -0
  62. package/dist/document/document-header.d.ts +41 -0
  63. package/dist/document/document-header.d.ts.map +1 -0
  64. package/dist/document/document-header.js +30 -0
  65. package/dist/document/document-page.d.ts +37 -0
  66. package/dist/document/document-page.d.ts.map +1 -0
  67. package/dist/document/document-page.js +137 -0
  68. package/dist/document/document-timeline.d.ts +20 -0
  69. package/dist/document/document-timeline.d.ts.map +1 -0
  70. package/dist/document/document-timeline.js +26 -0
  71. package/dist/document/evaluate.d.ts +31 -0
  72. package/dist/document/evaluate.d.ts.map +1 -0
  73. package/dist/document/evaluate.js +170 -0
  74. package/dist/document/format.d.ts +9 -0
  75. package/dist/document/format.d.ts.map +1 -0
  76. package/dist/document/format.js +52 -0
  77. package/dist/document/index.d.ts +13 -0
  78. package/dist/document/index.d.ts.map +1 -0
  79. package/dist/document/index.js +12 -0
  80. package/dist/document/registry.d.ts +20 -0
  81. package/dist/document/registry.d.ts.map +1 -0
  82. package/dist/document/registry.js +20 -0
  83. package/dist/document/smart-buttons.d.ts +16 -0
  84. package/dist/document/smart-buttons.d.ts.map +1 -0
  85. package/dist/document/smart-buttons.js +16 -0
  86. package/dist/document/status-badge.d.ts +16 -0
  87. package/dist/document/status-badge.d.ts.map +1 -0
  88. package/dist/document/status-badge.js +23 -0
  89. package/dist/document/types.d.ts +196 -0
  90. package/dist/document/types.d.ts.map +1 -0
  91. package/dist/document/types.js +1 -0
  92. package/dist/document/use-document-sources.d.ts +14 -0
  93. package/dist/document/use-document-sources.d.ts.map +1 -0
  94. package/dist/document/use-document-sources.js +72 -0
  95. package/dist/index.d.ts +4 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +5 -0
  98. package/dist/server-error.d.ts.map +1 -1
  99. package/dist/server-error.js +19 -0
  100. package/package.json +1 -1
  101. package/src/__tests__/approval-gate.test.tsx +134 -0
  102. package/src/approval-events.ts +39 -0
  103. package/src/approval-gate.tsx +180 -0
  104. package/src/approval-pin-dialog.tsx +162 -0
  105. package/src/business/business.test.ts +164 -0
  106. package/src/business/catalog.ts +60 -0
  107. package/src/business/customer-picker.tsx +201 -0
  108. package/src/business/feedback.tsx +141 -0
  109. package/src/business/field-errors.ts +92 -0
  110. package/src/business/format.ts +151 -0
  111. package/src/business/index.ts +13 -0
  112. package/src/business/line-items-editor.tsx +259 -0
  113. package/src/business/line-items.ts +190 -0
  114. package/src/business/payment-capture.tsx +158 -0
  115. package/src/business/payment.ts +125 -0
  116. package/src/business/product-picker.tsx +171 -0
  117. package/src/business/product-search.ts +108 -0
  118. package/src/business/refund-destination.tsx +121 -0
  119. package/src/business/refund.ts +71 -0
  120. package/src/business/use-async-search.ts +43 -0
  121. package/src/document/__tests__/document-page.test.tsx +107 -0
  122. package/src/document/__tests__/evaluate.test.ts +140 -0
  123. package/src/document/action-bar.tsx +140 -0
  124. package/src/document/cancel-with-reason.tsx +214 -0
  125. package/src/document/document-header.tsx +140 -0
  126. package/src/document/document-page.tsx +268 -0
  127. package/src/document/document-timeline.tsx +66 -0
  128. package/src/document/evaluate.ts +180 -0
  129. package/src/document/format.ts +59 -0
  130. package/src/document/index.ts +28 -0
  131. package/src/document/registry.ts +39 -0
  132. package/src/document/smart-buttons.tsx +54 -0
  133. package/src/document/status-badge.tsx +44 -0
  134. package/src/document/types.ts +203 -0
  135. package/src/document/use-document-sources.ts +91 -0
  136. package/src/index.ts +23 -0
  137. package/src/server-error.ts +20 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 45.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
9
+ ## 45.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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).
14
+ - 2c4c75e: Componentes de negocio compartidos con contrato estable (benchmark Pitsline §7, §6.4-6.7) en `@asteby/metacore-runtime-react`.
15
+
16
+ - 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).
17
+ - 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.
18
+ - `BUSINESS_COMPONENTS`: catálogo legible por máquina (props, eventos, permisos, dónde se usa). Guía en `docs/business-components.md`.
19
+
3
20
  ## 45.0.0
4
21
 
5
22
  ### Minor Changes
@@ -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"}
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // ApprovalPinDialog — the supervisor PIN prompt shared by every sensitive action
3
+ // (POS sell-without-stock, discount above the cap, price below the floor, refund,
4
+ // CFDI cancel, inventory adjust). Presentational: it collects a reason + a PIN and
5
+ // hands them to `onSubmit`, which does the network call, so federated addons that
6
+ // carry their own HTTP client reuse the same dialog as hosts with an ApiProvider.
7
+ //
8
+ // The reason is always required (audit trail: who asked, who authorized, why).
9
+ // A rejected `onSubmit` keeps the dialog open and shows the error inline (wrong
10
+ // PIN, locked, no permission) instead of a toast that can be missed.
11
+ import { useEffect, useState } from 'react';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { Loader2, ShieldCheck } from 'lucide-react';
14
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Textarea, } from '@asteby/metacore-ui/primitives';
15
+ /** Best human message out of an axios-style error carrying the kernel envelope. */
16
+ export function approvalErrorMessage(err, t) {
17
+ const data = err?.response?.data;
18
+ switch (data?.error?.code) {
19
+ case 'approval_pin_invalid':
20
+ return t('approvals.pin_invalid', { defaultValue: 'PIN incorrecto o sin permiso para autorizar esto.' });
21
+ case 'approval_pin_locked':
22
+ return t('approvals.pin_locked', { defaultValue: 'Demasiados intentos. Espera unos minutos.' });
23
+ case 'approval_pin_unavailable':
24
+ return t('approvals.pin_unavailable', { defaultValue: 'La autorización con PIN no está disponible.' });
25
+ case 'approval_reason_required':
26
+ return t('approvals.reason_required', { defaultValue: 'Escribe el motivo.' });
27
+ }
28
+ return data?.error?.message || data?.message || err?.message || t('common.error', { defaultValue: 'Algo salió mal' });
29
+ }
30
+ export function ApprovalPinDialog({ open, title, description, onSubmit, onCancel, secondaryAction }) {
31
+ const { t } = useTranslation();
32
+ const [pin, setPin] = useState('');
33
+ const [reason, setReason] = useState('');
34
+ const [busy, setBusy] = useState(false);
35
+ const [error, setError] = useState(null);
36
+ useEffect(() => {
37
+ if (open) {
38
+ setPin('');
39
+ setReason('');
40
+ setError(null);
41
+ setBusy(false);
42
+ }
43
+ }, [open]);
44
+ const ready = /^\d{4,8}$/.test(pin) && reason.trim().length > 0;
45
+ const submit = async () => {
46
+ if (!ready || busy)
47
+ return;
48
+ setBusy(true);
49
+ setError(null);
50
+ try {
51
+ await onSubmit({ pin, reason: reason.trim() });
52
+ }
53
+ catch (err) {
54
+ setError(approvalErrorMessage(err, t));
55
+ setPin('');
56
+ }
57
+ finally {
58
+ setBusy(false);
59
+ }
60
+ };
61
+ return (_jsx(Dialog, { open: open, onOpenChange: (o) => { if (!o && !busy)
62
+ onCancel(); }, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(ShieldCheck, { className: "h-5 w-5" }), title] }), _jsx(DialogDescription, { asChild: true, children: _jsx("div", { children: description ?? t('approvals.pin_prompt', { defaultValue: 'Requiere autorización de un supervisor.' }) }) })] }), _jsxs("form", { className: "space-y-3", onSubmit: (e) => {
63
+ e.preventDefault();
64
+ void submit();
65
+ }, children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-sm font-medium", htmlFor: "approval-reason", children: t('approvals.reason', { defaultValue: 'Motivo' }) }), _jsx(Textarea, { id: "approval-reason", value: reason, onChange: (e) => setReason(e.target.value), rows: 2, placeholder: t('approvals.reason_placeholder', { defaultValue: 'Por qué se autoriza' }) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-sm font-medium", htmlFor: "approval-pin", children: t('approvals.pin', { defaultValue: 'PIN del supervisor' }) }), _jsx(Input, { id: "approval-pin", type: "password", inputMode: "numeric", autoComplete: "off", maxLength: 8, value: pin, onChange: (e) => setPin(e.target.value.replace(/\D/g, '')), "aria-invalid": error ? true : undefined })] }), error ? (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })) : null, _jsxs(DialogFooter, { className: "gap-2 sm:gap-2", children: [secondaryAction ? (_jsx(Button, { type: "button", variant: "ghost", onClick: secondaryAction.onClick, disabled: busy, children: secondaryAction.label })) : null, _jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: busy, children: t('common.cancel', { defaultValue: 'Cancelar' }) }), _jsxs(Button, { type: "submit", disabled: !ready || busy, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, t('approvals.authorize', { defaultValue: 'Autorizar' })] })] })] })] }) }));
66
+ }
@@ -0,0 +1,16 @@
1
+ export interface BusinessComponentSpec {
2
+ /** Nombre exportado desde `@asteby/metacore-runtime-react`. */
3
+ name: string;
4
+ /** Prop de datos principal y su tipo. */
5
+ value: string;
6
+ /** Eventos (props `on*`) que emite. */
7
+ events: string[];
8
+ /** Capabilities que consulta con `useCan` (formato `<modelo>.<acción>`). */
9
+ permissions: string[];
10
+ /** Módulos donde el benchmark dice que se usa. */
11
+ usedBy: string[];
12
+ /** Utilidades puras exportadas junto al componente. */
13
+ helpers: string[];
14
+ }
15
+ export declare const BUSINESS_COMPONENTS: readonly BusinessComponentSpec[];
16
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +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,EAyCtD,CAAA"}
@@ -0,0 +1,42 @@
1
+ export const BUSINESS_COMPONENTS = [
2
+ {
3
+ name: 'CustomerPicker',
4
+ value: 'value: CustomerResult | null',
5
+ events: ['onChange'],
6
+ permissions: ['<model>.create (alta rápida; default Customer → customer.create)'],
7
+ usedBy: ['POS', 'Cotización', 'Pedido', 'Apartado', 'Factura', 'OT', 'RMA', 'Cobranza'],
8
+ helpers: [],
9
+ },
10
+ {
11
+ name: 'ProductPicker',
12
+ value: '(sin estado: search + onSelect)',
13
+ events: ['onSelect'],
14
+ permissions: [],
15
+ usedBy: ['POS', 'Cotización', 'Pedido', 'OC', 'Recepción', 'Traspaso', 'Ajuste', 'OT'],
16
+ helpers: ['parseProductQuery', 'parseTireSize', 'availableStock', 'productToLine'],
17
+ },
18
+ {
19
+ name: 'LineItemsEditor',
20
+ value: 'value: LineItem[]',
21
+ events: ['onChange', 'onValidate', 'onRequestProduct'],
22
+ permissions: ['editPermission (prop, opcional)'],
23
+ usedBy: ['Cotización', 'Pedido', 'Factura', 'OC', 'Recepción', 'OT', 'RMA', 'NC'],
24
+ helpers: ['makeLine', 'computeLine', 'computeTotals', 'validateLineItems', 'serializeLineItems', 'parseLineItems'],
25
+ },
26
+ {
27
+ name: 'PaymentCapture',
28
+ value: 'value: PaymentTender[]',
29
+ events: ['onChange', 'onSubmit'],
30
+ permissions: ['submitPermission (prop, opcional)'],
31
+ usedBy: ['POS/Caja', 'Apartado', 'Cobranza', 'Factura (Registrar pago)', 'OT'],
32
+ helpers: ['newTender', 'summarizePayment', 'validatePayment', 'serializePayment'],
33
+ },
34
+ {
35
+ name: 'RefundDestination',
36
+ value: 'value: RefundAllocation[]',
37
+ events: ['onChange', 'onValidate'],
38
+ permissions: [],
39
+ usedBy: ['ReturnWizard', 'Cancelación de apartado', 'Anticipos'],
40
+ helpers: ['validateRefund', 'singleDestination', 'DEFAULT_REFUND_DESTINATIONS'],
41
+ },
42
+ ];
@@ -0,0 +1,39 @@
1
+ export interface CustomerResult {
2
+ id: string;
3
+ name: string;
4
+ /** Identificador fiscal (RFC u homólogo): dato de la org, sin formato hardcodeado. */
5
+ tax_id?: string;
6
+ phone?: string;
7
+ email?: string;
8
+ plate?: string;
9
+ vin?: string;
10
+ /** Saldo por cobrar (adeudo). */
11
+ balance?: number;
12
+ credit_limit?: number;
13
+ /** Alertas visibles: «Cliente con adeudo vencido», «Lista negra». */
14
+ alerts?: string[];
15
+ }
16
+ export interface CustomerPickerProps {
17
+ value: CustomerResult | null;
18
+ /** Evento único de cambio: cliente elegido/creado, o `null` al limpiar. */
19
+ onChange: (customer: CustomerResult | null) => void;
20
+ /** Búsqueda (nombre, ID fiscal, teléfono, placa, VIN). Estable (useCallback). */
21
+ search: (q: string, signal: AbortSignal) => Promise<CustomerResult[]>;
22
+ /** Modelo del kernel para el alta rápida. Default `Customer`. */
23
+ model?: string;
24
+ /** Endpoint CRUD del alta. Default `/data/<model>/me`. */
25
+ endpoint?: string;
26
+ /** Convierte el registro guardado a `CustomerResult`. Default: id + name. */
27
+ mapRecord?: (rec: Record<string, unknown>) => CustomerResult;
28
+ /** Valida el ID fiscal según la config de la org; devuelve el mensaje o undefined. */
29
+ validateTaxId?: (taxId: string) => string | undefined;
30
+ /** Overrides de permiso (default `<model>.create` con useCan). */
31
+ canCreate?: boolean;
32
+ /** Valores iniciales del alta rápida (p. ej. el texto buscado como nombre). */
33
+ createDefaults?: Record<string, unknown>;
34
+ currency?: string;
35
+ disabled?: boolean;
36
+ placeholder?: string;
37
+ }
38
+ export declare function CustomerPicker({ value, onChange, search, model, endpoint, mapRecord, validateTaxId, canCreate, createDefaults, currency, disabled, placeholder, }: CustomerPickerProps): import("react").JSX.Element;
39
+ //# sourceMappingURL=customer-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-picker.d.ts","sourceRoot":"","sources":["../../src/business/customer-picker.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,iFAAiF;IACjF,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IACrE,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,cAAc,CAAA;IAC5D,sFAAsF;IACtF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACrD,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAkB,EAClB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,WAAW,GACd,EAAE,mBAAmB,+BAkIrB"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // CustomerPicker + alta rápida — busca por nombre, RFC/ID fiscal, teléfono, placa
3
+ // o VIN; muestra saldo, crédito y alertas (benchmark §7). El alta rápida reutiliza
4
+ // el <CreateRecordDialog> dinámico del modelo (formulario desde metadata).
5
+ import { useCallback, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Plus, Search, X } from 'lucide-react';
8
+ import { Badge, Button, Input } from '@asteby/metacore-ui';
9
+ import { CreateRecordDialog } from '../dialogs/create-record-dialog';
10
+ import { useApi } from '../api-context';
11
+ import { useCan } from '../permissions-context';
12
+ import { useFormatter } from './format';
13
+ import { useAsyncSearch } from './use-async-search';
14
+ import { EmptyState } from './feedback';
15
+ function snake(model) {
16
+ return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
17
+ }
18
+ export function CustomerPicker({ value, onChange, search, model = 'Customer', endpoint, mapRecord, validateTaxId, canCreate, createDefaults, currency, disabled, placeholder, }) {
19
+ const { t } = useTranslation();
20
+ const api = useApi();
21
+ const can = useCan();
22
+ const fmt = useFormatter({ currency });
23
+ const [text, setText] = useState('');
24
+ const [creating, setCreating] = useState(false);
25
+ const { results, loading, error } = useAsyncSearch(text, search, { minChars: 2 });
26
+ const mayCreate = canCreate ?? can(`${snake(model)}.create`);
27
+ const base = endpoint ?? `/data/${model}/me`;
28
+ const taxIdError = value?.tax_id && validateTaxId ? validateTaxId(value.tax_id) : undefined;
29
+ const pick = (c) => {
30
+ onChange(c);
31
+ setText('');
32
+ };
33
+ const toResult = useCallback((rec) => mapRecord
34
+ ? mapRecord(rec)
35
+ : {
36
+ id: String(rec.id ?? ''),
37
+ name: String(rec.name ?? ''),
38
+ tax_id: typeof rec.tax_id === 'string' ? rec.tax_id : undefined,
39
+ phone: typeof rec.phone === 'string' ? rec.phone : undefined,
40
+ }, [mapRecord]);
41
+ if (value) {
42
+ const overLimit = value.credit_limit != null && (value.balance ?? 0) > value.credit_limit;
43
+ return (_jsxs("div", { "data-slot": "customer-picker", className: "space-y-1 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: value.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: [value.tax_id, value.phone, value.plate, value.vin].filter(Boolean).join(' · ') })] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => onChange(null), "aria-label": t('customerPicker.clear', { defaultValue: 'Quitar cliente' }), children: _jsx(X, { className: "size-4" }) }))] }), _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [value.balance != null && value.balance !== 0 && (_jsxs(Badge, { variant: overLimit ? 'danger' : 'warning', children: [t('customerPicker.balance', { defaultValue: 'Adeudo' }), " ", fmt.money(value.balance)] })), value.credit_limit != null && (_jsxs(Badge, { variant: "muted", children: [t('customerPicker.credit', { defaultValue: 'Crédito' }), " ", fmt.money(value.credit_limit)] })), value.alerts?.map((a) => (_jsx(Badge, { variant: "danger", children: a }, a)))] }), taxIdError && _jsx("p", { role: "alert", className: "text-xs text-destructive", children: taxIdError })] }));
44
+ }
45
+ return (_jsxs("div", { "data-slot": "customer-picker", 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", disabled: disabled, value: text, placeholder: placeholder ?? t('customerPicker.placeholder', { defaultValue: 'Nombre, RFC, teléfono, placa o VIN' }), onChange: (e) => setText(e.target.value) })] }), mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: () => setCreating(true), "aria-label": t('customerPicker.create', { defaultValue: 'Nuevo cliente' }), children: _jsx(Plus, { className: "size-4" }) }))] }), 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('customerPicker.error', { defaultValue: 'No se pudo buscar clientes. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('customerPicker.empty', { defaultValue: 'No encontramos a ese cliente' }), action: mayCreate ? { label: t('customerPicker.createAction', { defaultValue: 'Crear cliente' }), onClick: () => setCreating(true) } : undefined })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((c) => (_jsx("li", { role: "option", "aria-selected": false, children: _jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left hover:bg-accent", onClick: () => pick(c), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: c.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: [c.tax_id, c.phone, c.plate, c.vin].filter(Boolean).join(' · ') })] }), c.balance != null && c.balance > 0 && _jsx(Badge, { variant: "warning", children: fmt.money(c.balance) })] }) }, c.id))) })), creating && (_jsx(CreateRecordDialog, { modelKey: model, open: creating, onOpenChange: setCreating, endpoint: base, defaults: createDefaults ?? (text.trim() ? { name: text.trim() } : undefined), onCreate: async (data) => {
46
+ const res = await api.post(base, data);
47
+ const rec = (res.data?.data ?? res.data);
48
+ pick(toResult(rec));
49
+ return rec.id != null ? { id: String(rec.id) } : undefined;
50
+ } }))] }));
51
+ }
@@ -0,0 +1,72 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type LucideIcon } from 'lucide-react';
3
+ /**
4
+ * Props recomendadas para `<Toaster>` de metacore-ui: los toasts viven arriba a
5
+ * la derecha (el área de acciones primarias suele estar abajo) y dejan hueco
6
+ * bajo la cabecera. Se propagan tal cual: `<Toaster {...ACTION_SAFE_TOASTER_PROPS} />`.
7
+ */
8
+ export declare const ACTION_SAFE_TOASTER_PROPS: {
9
+ position: "top-right";
10
+ offset: number;
11
+ mobileOffset: number;
12
+ visibleToasts: number;
13
+ };
14
+ /** Duración de un toast de éxito (ms): confirma y se va. */
15
+ export declare const SUCCESS_TOAST_MS = 3000;
16
+ export declare const notify: {
17
+ /** Confirmación breve de un éxito («Pago registrado»). Admite «Deshacer». */
18
+ success(message: string, opts?: {
19
+ undo?: {
20
+ label?: string;
21
+ onUndo: () => void;
22
+ };
23
+ description?: string;
24
+ }): string | number;
25
+ /**
26
+ * Error inesperado (red, 5xx) sin campo al que atribuirlo. Los errores de
27
+ * validación NO van aquí: usa `mapApiError` + `<FormErrorBanner>`/error del input.
28
+ */
29
+ error(message: string, description?: string): string | number;
30
+ };
31
+ export interface SystemNoticeBannerProps {
32
+ message: ReactNode;
33
+ /** Acción opcional («Actualizar»). */
34
+ action?: {
35
+ label: string;
36
+ onClick: () => void;
37
+ };
38
+ onDismiss?: () => void;
39
+ dismissLabel?: string;
40
+ className?: string;
41
+ }
42
+ /** Aviso del sistema en flujo normal de la página (no flota sobre botones). */
43
+ export declare function SystemNoticeBanner({ message, action, onDismiss, dismissLabel, className }: SystemNoticeBannerProps): import("react").JSX.Element;
44
+ export interface FormErrorBannerProps {
45
+ /** Qué pasó y cómo corregirlo (típicamente `mapApiError(...).form`). */
46
+ message?: string;
47
+ /** Botón a la solución («Ir al campo», «Reintentar»). */
48
+ action?: {
49
+ label: string;
50
+ onClick: () => void;
51
+ };
52
+ className?: string;
53
+ }
54
+ /** Error persistente de formulario; no renderiza nada sin `message`. */
55
+ export declare function FormErrorBanner({ message, action, className }: FormErrorBannerProps): import("react").JSX.Element | null;
56
+ export interface EmptyStateProps {
57
+ icon?: LucideIcon;
58
+ /** Qué falta («Aún no hay movimientos para este producto»). */
59
+ title: string;
60
+ description?: string;
61
+ /** Acción primaria («Registrar saldo inicial»). Se oculta si `hidden`. */
62
+ action?: {
63
+ label: string;
64
+ onClick?: () => void;
65
+ href?: string;
66
+ hidden?: boolean;
67
+ };
68
+ className?: string;
69
+ }
70
+ /** Estado vacío accionable: explicación + una acción primaria. */
71
+ export declare function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps): import("react").JSX.Element;
72
+ //# sourceMappingURL=feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/business/feedback.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAGpE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;;;;CAKrC,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,eAAO,MAAM,MAAM;IACf,6EAA6E;qBAC5D,MAAM,SAAS;QAAE,IAAI,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,IAAI,CAAA;SAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IASvG;;;OAGG;mBACY,MAAM,gBAAgB,MAAM;CAG9C,CAAA;AAED,MAAM,WAAW,uBAAuB;IACpC,OAAO,EAAE,SAAS,CAAA;IAClB,sCAAsC;IACtC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IAC/C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAA0B,EAAE,SAAS,EAAE,EAAE,uBAAuB,+BAqBhI;AAED,MAAM,WAAW,oBAAoB;IACjC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,oBAAoB,sCAiBnF;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,eAAe,+BAsBhG"}
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { toast } from 'sonner';
3
+ import { AlertCircle, Info, X } from 'lucide-react';
4
+ import { Button } from '@asteby/metacore-ui';
5
+ /**
6
+ * Props recomendadas para `<Toaster>` de metacore-ui: los toasts viven arriba a
7
+ * la derecha (el área de acciones primarias suele estar abajo) y dejan hueco
8
+ * bajo la cabecera. Se propagan tal cual: `<Toaster {...ACTION_SAFE_TOASTER_PROPS} />`.
9
+ */
10
+ export const ACTION_SAFE_TOASTER_PROPS = {
11
+ position: 'top-right',
12
+ offset: 72,
13
+ mobileOffset: 72,
14
+ visibleToasts: 3,
15
+ };
16
+ /** Duración de un toast de éxito (ms): confirma y se va. */
17
+ export const SUCCESS_TOAST_MS = 3000;
18
+ export const notify = {
19
+ /** Confirmación breve de un éxito («Pago registrado»). Admite «Deshacer». */
20
+ success(message, opts) {
21
+ return toast.success(message, {
22
+ duration: SUCCESS_TOAST_MS,
23
+ description: opts?.description,
24
+ action: opts?.undo
25
+ ? { label: opts.undo.label ?? 'Deshacer', onClick: opts.undo.onUndo }
26
+ : undefined,
27
+ });
28
+ },
29
+ /**
30
+ * Error inesperado (red, 5xx) sin campo al que atribuirlo. Los errores de
31
+ * validación NO van aquí: usa `mapApiError` + `<FormErrorBanner>`/error del input.
32
+ */
33
+ error(message, description) {
34
+ return toast.error(message, { description, duration: 8000 });
35
+ },
36
+ };
37
+ /** Aviso del sistema en flujo normal de la página (no flota sobre botones). */
38
+ export function SystemNoticeBanner({ message, action, onDismiss, dismissLabel = 'Descartar', className }) {
39
+ return (_jsxs("div", { role: "status", "data-slot": "system-notice", className: `flex items-center gap-3 rounded-md border bg-muted px-3 py-2 text-sm ${className ?? ''}`, children: [_jsx(Info, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "flex-1", children: message }), action && (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: action.onClick, children: action.label })), onDismiss && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: onDismiss, "aria-label": dismissLabel, children: _jsx(X, { className: "size-4" }) }))] }));
40
+ }
41
+ /** Error persistente de formulario; no renderiza nada sin `message`. */
42
+ export function FormErrorBanner({ message, action, className }) {
43
+ if (!message)
44
+ return null;
45
+ return (_jsxs("div", { role: "alert", "data-slot": "form-error", className: `flex items-start gap-3 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive ${className ?? ''}`, children: [_jsx(AlertCircle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }), _jsx("span", { className: "flex-1", children: message }), action && (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: action.onClick, children: action.label }))] }));
46
+ }
47
+ /** Estado vacío accionable: explicación + una acción primaria. */
48
+ export function EmptyState({ icon: Icon, title, description, action, className }) {
49
+ const showAction = action && !action.hidden;
50
+ return (_jsxs("div", { "data-slot": "empty-state", className: `flex flex-col items-center gap-2 px-4 py-10 text-center ${className ?? ''}`, children: [Icon && _jsx(Icon, { className: "size-8 text-muted-foreground", "aria-hidden": true }), _jsx("p", { className: "text-sm font-medium", children: title }), description && _jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: description }), showAction &&
51
+ (action.href ? (_jsx(Button, { asChild: true, size: "sm", className: "mt-2", children: _jsx("a", { href: action.href, children: action.label }) })) : (_jsx(Button, { type: "button", size: "sm", className: "mt-2", onClick: action.onClick, children: action.label })))] }));
52
+ }