@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.
- package/CHANGELOG.md +29 -0
- package/dist/action-modal-dispatcher.d.ts +29 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +115 -6
- package/dist/activity-diff.d.ts +2 -0
- package/dist/activity-diff.d.ts.map +1 -1
- package/dist/activity-diff.js +2 -2
- package/dist/approval-events.d.ts +16 -0
- package/dist/approval-events.d.ts.map +1 -0
- package/dist/approval-events.js +27 -0
- package/dist/approval-gate.d.ts +39 -0
- package/dist/approval-gate.d.ts.map +1 -0
- package/dist/approval-gate.js +110 -0
- package/dist/approval-pin-dialog.d.ts +25 -0
- package/dist/approval-pin-dialog.d.ts.map +1 -0
- package/dist/approval-pin-dialog.js +66 -0
- package/dist/business/catalog.d.ts +16 -0
- package/dist/business/catalog.d.ts.map +1 -0
- package/dist/business/catalog.js +50 -0
- package/dist/business/customer-picker.d.ts +39 -0
- package/dist/business/customer-picker.d.ts.map +1 -0
- package/dist/business/customer-picker.js +51 -0
- package/dist/business/feedback.d.ts +72 -0
- package/dist/business/feedback.d.ts.map +1 -0
- package/dist/business/feedback.js +52 -0
- package/dist/business/field-errors.d.ts +26 -0
- package/dist/business/field-errors.d.ts.map +1 -0
- package/dist/business/field-errors.js +59 -0
- package/dist/business/format.d.ts +49 -0
- package/dist/business/format.d.ts.map +1 -0
- package/dist/business/format.js +128 -0
- package/dist/business/index.d.ts +16 -0
- package/dist/business/index.d.ts.map +1 -0
- package/dist/business/index.js +15 -0
- package/dist/business/line-items-editor.d.ts +25 -0
- package/dist/business/line-items-editor.d.ts.map +1 -0
- package/dist/business/line-items-editor.js +36 -0
- package/dist/business/line-items.d.ts +69 -0
- package/dist/business/line-items.d.ts.map +1 -0
- package/dist/business/line-items.js +133 -0
- package/dist/business/payment-capture.d.ts +21 -0
- package/dist/business/payment-capture.d.ts.map +1 -0
- package/dist/business/payment-capture.js +24 -0
- package/dist/business/payment.d.ts +61 -0
- package/dist/business/payment.d.ts.map +1 -0
- package/dist/business/payment.js +74 -0
- package/dist/business/product-picker.d.ts +24 -0
- package/dist/business/product-picker.d.ts.map +1 -0
- package/dist/business/product-picker.js +48 -0
- package/dist/business/product-search.d.ts +70 -0
- package/dist/business/product-search.d.ts.map +1 -0
- package/dist/business/product-search.js +51 -0
- package/dist/business/refund-destination.d.ts +19 -0
- package/dist/business/refund-destination.d.ts.map +1 -0
- package/dist/business/refund-destination.js +31 -0
- package/dist/business/refund.d.ts +29 -0
- package/dist/business/refund.d.ts.map +1 -0
- package/dist/business/refund.js +44 -0
- package/dist/business/return-wizard.d.ts +23 -0
- package/dist/business/return-wizard.d.ts.map +1 -0
- package/dist/business/return-wizard.js +59 -0
- package/dist/business/return.d.ts +90 -0
- package/dist/business/return.d.ts.map +1 -0
- package/dist/business/return.js +112 -0
- package/dist/business/use-async-search.d.ts +10 -0
- package/dist/business/use-async-search.d.ts.map +1 -0
- package/dist/business/use-async-search.js +38 -0
- package/dist/document/action-bar.d.ts +12 -0
- package/dist/document/action-bar.d.ts.map +1 -0
- package/dist/document/action-bar.js +39 -0
- package/dist/document/cancel-with-reason.d.ts +47 -0
- package/dist/document/cancel-with-reason.d.ts.map +1 -0
- package/dist/document/cancel-with-reason.js +52 -0
- package/dist/document/document-header.d.ts +41 -0
- package/dist/document/document-header.d.ts.map +1 -0
- package/dist/document/document-header.js +30 -0
- package/dist/document/document-page.d.ts +37 -0
- package/dist/document/document-page.d.ts.map +1 -0
- package/dist/document/document-page.js +137 -0
- package/dist/document/document-timeline.d.ts +20 -0
- package/dist/document/document-timeline.d.ts.map +1 -0
- package/dist/document/document-timeline.js +26 -0
- package/dist/document/evaluate.d.ts +31 -0
- package/dist/document/evaluate.d.ts.map +1 -0
- package/dist/document/evaluate.js +170 -0
- package/dist/document/format.d.ts +9 -0
- package/dist/document/format.d.ts.map +1 -0
- package/dist/document/format.js +52 -0
- package/dist/document/index.d.ts +13 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +12 -0
- package/dist/document/registry.d.ts +20 -0
- package/dist/document/registry.d.ts.map +1 -0
- package/dist/document/registry.js +20 -0
- package/dist/document/smart-buttons.d.ts +16 -0
- package/dist/document/smart-buttons.d.ts.map +1 -0
- package/dist/document/smart-buttons.js +16 -0
- package/dist/document/status-badge.d.ts +16 -0
- package/dist/document/status-badge.d.ts.map +1 -0
- package/dist/document/status-badge.js +23 -0
- package/dist/document/types.d.ts +196 -0
- package/dist/document/types.d.ts.map +1 -0
- package/dist/document/types.js +1 -0
- package/dist/document/use-document-sources.d.ts +14 -0
- package/dist/document/use-document-sources.d.ts.map +1 -0
- package/dist/document/use-document-sources.js +72 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +3 -1
- package/dist/dynamic-relation.d.ts +4 -0
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +10 -1
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +14 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +28 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/reason-prompt.d.ts +59 -0
- package/dist/reason-prompt.d.ts.map +1 -0
- package/dist/reason-prompt.js +111 -0
- package/dist/server-error.d.ts.map +1 -1
- package/dist/server-error.js +19 -0
- package/dist/use-addon-settings.d.ts +17 -3
- package/dist/use-addon-settings.d.ts.map +1 -1
- package/dist/use-addon-settings.js +20 -8
- package/package.json +1 -1
- package/src/__tests__/approval-gate.test.tsx +134 -0
- package/src/__tests__/prefill-from-record.test.ts +69 -0
- package/src/__tests__/reason-prompt.test.tsx +121 -0
- package/src/__tests__/use-addon-settings.test.ts +16 -1
- package/src/action-modal-dispatcher.tsx +145 -3
- package/src/activity-diff.tsx +10 -1
- package/src/approval-events.ts +39 -0
- package/src/approval-gate.tsx +180 -0
- package/src/approval-pin-dialog.tsx +162 -0
- package/src/business/business.test.ts +210 -0
- package/src/business/catalog.ts +68 -0
- package/src/business/customer-picker.tsx +201 -0
- package/src/business/feedback.tsx +141 -0
- package/src/business/field-errors.ts +92 -0
- package/src/business/format.ts +151 -0
- package/src/business/index.ts +15 -0
- package/src/business/line-items-editor.tsx +259 -0
- package/src/business/line-items.ts +190 -0
- package/src/business/payment-capture.tsx +158 -0
- package/src/business/payment.ts +125 -0
- package/src/business/product-picker.tsx +171 -0
- package/src/business/product-search.ts +108 -0
- package/src/business/refund-destination.tsx +121 -0
- package/src/business/refund.ts +72 -0
- package/src/business/return-wizard.tsx +223 -0
- package/src/business/return.ts +181 -0
- package/src/business/use-async-search.ts +43 -0
- package/src/document/__tests__/document-page.test.tsx +107 -0
- package/src/document/__tests__/evaluate.test.ts +140 -0
- package/src/document/action-bar.tsx +140 -0
- package/src/document/cancel-with-reason.tsx +214 -0
- package/src/document/document-header.tsx +140 -0
- package/src/document/document-page.tsx +268 -0
- package/src/document/document-timeline.tsx +66 -0
- package/src/document/evaluate.ts +180 -0
- package/src/document/format.ts +59 -0
- package/src/document/index.ts +28 -0
- package/src/document/registry.ts +39 -0
- package/src/document/smart-buttons.tsx +54 -0
- package/src/document/status-badge.tsx +44 -0
- package/src/document/types.ts +203 -0
- package/src/document/use-document-sources.ts +91 -0
- package/src/dynamic-columns.tsx +5 -1
- package/src/dynamic-relation.tsx +14 -1
- package/src/dynamic-row-actions.tsx +13 -1
- package/src/dynamic-table.tsx +22 -2
- package/src/index.ts +33 -0
- package/src/reason-prompt.tsx +247 -0
- package/src/server-error.ts +20 -0
- 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":"
|
|
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
|
|
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
|
|
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
|
package/dist/activity-diff.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/activity-diff.js
CHANGED
|
@@ -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"}
|