@asteby/metacore-runtime-react 46.4.0 → 47.1.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 +38 -0
- package/dist/action-modal-dispatcher.d.ts +10 -1
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +107 -19
- package/dist/audit-info.d.ts +54 -0
- package/dist/audit-info.d.ts.map +1 -0
- package/dist/audit-info.js +142 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +24 -0
- package/dist/business/index.d.ts +3 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +3 -0
- package/dist/business/print-send-dialog.d.ts +27 -0
- package/dist/business/print-send-dialog.d.ts.map +1 -0
- package/dist/business/print-send-dialog.js +55 -0
- package/dist/business/relate-documents.d.ts +41 -0
- package/dist/business/relate-documents.d.ts.map +1 -0
- package/dist/business/relate-documents.js +42 -0
- package/dist/business/vehicle-picker.d.ts +36 -0
- package/dist/business/vehicle-picker.d.ts.map +1 -0
- package/dist/business/vehicle-picker.js +61 -0
- package/dist/cfdi-stamp-panel.d.ts +29 -0
- package/dist/cfdi-stamp-panel.d.ts.map +1 -0
- package/dist/cfdi-stamp-panel.js +53 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +10 -1
- package/dist/document-form-dialog.d.ts +23 -0
- package/dist/document-form-dialog.d.ts.map +1 -0
- package/dist/document-form-dialog.js +196 -0
- package/dist/dynamic-crud-page.d.ts +6 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +4 -2
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +3 -0
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +11 -2
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +3 -0
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +2 -0
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +4 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +6 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/nav-permissions.d.ts +19 -0
- package/dist/nav-permissions.d.ts.map +1 -0
- package/dist/nav-permissions.js +51 -0
- package/dist/navigation-builder.d.ts +6 -0
- package/dist/navigation-builder.d.ts.map +1 -1
- package/dist/navigation-builder.js +26 -1
- package/dist/option-filter.d.ts +31 -0
- package/dist/option-filter.d.ts.map +1 -0
- package/dist/option-filter.js +49 -0
- package/dist/permissions-manager.d.ts +9 -0
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +50 -17
- package/dist/print-document-button.d.ts.map +1 -1
- package/dist/print-document-button.js +9 -1
- package/dist/record-mutation-events.d.ts +14 -0
- package/dist/record-mutation-events.d.ts.map +1 -0
- package/dist/record-mutation-events.js +37 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-options-resolver.d.ts +8 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +8 -2
- package/dist/use-print-document.d.ts +15 -1
- package/dist/use-print-document.d.ts.map +1 -1
- package/dist/use-print-document.js +52 -1
- package/dist/use-record-mutation-tick.d.ts +6 -0
- package/dist/use-record-mutation-tick.d.ts.map +1 -0
- package/dist/use-record-mutation-tick.js +11 -0
- package/package.json +3 -3
- package/src/__tests__/action-modal-feedback.test.tsx +116 -0
- package/src/__tests__/action-modal-grid-layout.test.tsx +1 -1
- package/src/__tests__/apply-sidebar-layout.test.ts +1 -1
- package/src/__tests__/audit-info.test.tsx +103 -0
- package/src/__tests__/business-pickers.test.tsx +157 -0
- package/src/__tests__/dependent-options.test.tsx +12 -5
- package/src/__tests__/document-form-dialog.test.tsx +190 -0
- package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +4 -0
- package/src/__tests__/dynamic-table-mutation-refresh.test.tsx +116 -0
- package/src/__tests__/line-items-lock-rows.test.tsx +2 -1
- package/src/__tests__/nav-permissions.test.tsx +76 -0
- package/src/__tests__/option-filter.test.tsx +115 -0
- package/src/__tests__/permissions-manager.test.tsx +21 -4
- package/src/__tests__/structured-view-value.test.tsx +5 -1
- package/src/__tests__/use-print-document-feedback.test.tsx +58 -0
- package/src/__tests__/wizard-action-modal.test.tsx +1 -1
- package/src/action-modal-dispatcher.tsx +124 -6
- package/src/audit-info.tsx +219 -0
- package/src/business/catalog.ts +24 -0
- package/src/business/index.ts +3 -0
- package/src/business/print-send-dialog.tsx +175 -0
- package/src/business/relate-documents.tsx +182 -0
- package/src/business/vehicle-picker.tsx +188 -0
- package/src/cfdi-stamp-panel.tsx +146 -0
- package/src/dialogs/dynamic-record.tsx +18 -0
- package/src/document-form-dialog.tsx +357 -0
- package/src/dropdown-menu-entry.test.ts +23 -0
- package/src/dynamic-crud-page.tsx +21 -2
- package/src/dynamic-form.tsx +3 -0
- package/src/dynamic-kanban.tsx +10 -4
- package/src/dynamic-line-items.tsx +3 -0
- package/src/dynamic-multi-select-field.tsx +2 -0
- package/src/dynamic-select-field.tsx +4 -0
- package/src/dynamic-table.tsx +6 -2
- package/src/index.ts +41 -0
- package/src/nav-permissions.ts +58 -0
- package/src/navigation-builder.tsx +27 -1
- package/src/option-filter.ts +88 -0
- package/src/permissions-manager.tsx +39 -26
- package/src/print-document-button.tsx +9 -1
- package/src/record-mutation-events.ts +50 -0
- package/src/types.ts +90 -0
- package/src/use-options-resolver.ts +19 -1
- package/src/use-print-document.ts +66 -3
- package/src/use-record-mutation-tick.ts +12 -0
- package/vitest.config.ts +1 -0
- package/vitest.setup.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 47.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1614e02: `TableMetadata.audit` (columnas de auditoría que sirve el kernel >= v0.188) y nuevo `<AuditInfo>`: «Creado por X · fecha», «Modificado por Y · fecha» y «Eliminado por Z · fecha», colapsado al pie del detalle/edición de `DynamicRecordDialog`. El nombre sale del host (`resolveActor`), del hermano expandido del registro (`created_by`) o, si no, un id corto; el actor de sistema se muestra como «Sistema».
|
|
8
|
+
- 0ee83dd: Filtro de opciones en selectores de relación: el campo (columna o campo de acción) acepta `option_filter` / `optionFilter` (una regla o lista con `field` y `equals` / `not_equals` / `in` / `not_in`) para ocultar opciones, p. ej. facturas con status `cancelada` en el selector de abonos (PIT-059). Se aplica en cliente sobre las columnas extra que devuelve `/options`, nunca oculta la selección actual y es retrocompatible (sin la propiedad no cambia nada). Nuevos exports: `getOptionFilter`, `applyOptionFilter`, `optionPassesRule`, tipos `OptionFilter` y `OptionFilterRule`; `useOptionsResolver` acepta `optionFilter` y `keepValue`.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- ad1e1b0: Kanban: el menú «…» de la tarjeta ya no tira la app con «`MenuItem` must be used within `Menu`». El `DropdownMenu` de la tarjeta se importa ahora desde la entrada raíz de `@asteby/metacore-ui`, la misma de `RowActionMenuItem`. Así no se mezclan dos contextos de Radix cuando el host comparte la raíz por Module Federation y empaqueta `/primitives` aparte. Se agrega un test que lo vigila.
|
|
13
|
+
- Updated dependencies [66d82f5]
|
|
14
|
+
- @asteby/metacore-ui@2.24.1
|
|
15
|
+
|
|
16
|
+
## 47.0.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 4371f2c: Diálogos de acción: resultado de timbrado y errores del servidor.
|
|
21
|
+
|
|
22
|
+
- #1024 (FAC-3 / PIT-051): nuevo `CfdiStampPanel` / `CfdiStampResultDialog` / `extractStampResult`. Cuando la acción responde con `fiscal_uuid`, `pdf_url` y/o `xml_url`, el diálogo muestra el UUID como texto y enlaces «Descargar PDF» / «Descargar XML» en lugar del JSON. Sin esas claves no se inventa un resultado fiscal.
|
|
23
|
+
- #1023 (PIT-046): los diálogos de acción (genérico, wizard y confirmación) pintan un `FormErrorBanner` dentro del diálogo con el 422 (`errors` por campo) o el mensaje y causa de un 500, y permanecen abiertos.
|
|
24
|
+
|
|
25
|
+
- 4371f2c: FAC-12 (#1022): alta guiada por tipo de documento. Nuevo `DocumentFormDialog` y `resolveDocumentForms`: cuando el metadata de la tabla (o la prop `documentForms` de `DynamicCRUDPage`) trae `document_forms`, «Crear» muestra tarjetas de tipo (Factura / Nota de crédito / REP / Global / Traslado…) y, al elegir, los campos de ese tipo y un paso de renglones con `DocumentLinesGrid`; un solo tipo se salta el selector. Escribe el valor del tipo en `type_field`, serializa los renglones con `serializeLineItems`, muestra los 422 (banner + campos) dentro del diálogo y emite el evento de mutación para refrescar la lista. Sin manifest, el alta genérica no cambia. El SDK no embebe catálogos SAT: campos y valores los sirve el kernel o el addon. Nuevos tipos: `DocumentFormsManifest`, `DocumentFormType`, `DocumentFormLines`, `TableMetadata.document_forms`.
|
|
26
|
+
- 4371f2c: PIT-044: el menú lateral se filtra por los permisos efectivos del rol. `@asteby/metacore-ui` agrega `filterNavGroups`, `NavLinkItem.requires` y la prop `isItemVisible` de `AppSidebar`; `@asteby/metacore-runtime-react` agrega `useNavItemVisible`, `isNavItemAllowed`, `capabilityForNavItem` (una entrada `/m/<modelo>` exige `<modelo>.index`; las demás rutas no se bloquean) y `useNavigation` respeta `NavItem.requires`; el `AppSidebar` de starter-core lo aplica. Sin `PermissionsProvider` (o con `isAdmin`) no se oculta nada.
|
|
27
|
+
- 4371f2c: Las listas se refrescan tras crear, editar o borrar (#1020). `DynamicRecordDialog` y `ActionModalDispatcher` (componente federado, wizard, formulario genérico y confirmación) emiten el evento `metacore:record-mutated` con el modelo; `DynamicTable` y `DynamicKanban` vuelven a pedir los datos cuando el modelo coincide (sin distinguir mayúsculas). Nuevos exports: `emitRecordMutation`, `subscribeRecordMutations`, `useRecordMutationTick`.
|
|
28
|
+
- 4371f2c: Tres componentes de negocio compartidos:
|
|
29
|
+
|
|
30
|
+
- `VehiclePicker` (#1025): selector controlado de vehículo con búsqueda por placa o VIN y alta rápida (`CreateRecordDialog`), mismo estilo que `CustomerPicker`; sin `PermissionsProvider` no bloquea la UI.
|
|
31
|
+
- `RelateDocuments` (#1026): relaciona uno o más documentos origen con un tipo de relación (`related_document_id`, `relation_type`), muestra UUID/folio; los tipos llegan por prop (sin catálogos fiscales embebidos).
|
|
32
|
+
- `PrintSendDialog` (#1027): imprimir (URL de PDF del host) o enviar por `onSend(canal, destino, documento)`; sin integración de mensajería.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 4371f2c: Roles y permisos (#1021): «Permisos generales» deja de ser una columna de tarjetas junto al rol y pasa a ser un grupo sintético del combobox de módulos; al elegirlo se editan en la misma grilla de dos columnas que las acciones de un módulo, y el conteo de concedidos los incluye. Nuevo `generalPermissionsModule`.
|
|
37
|
+
- 4371f2c: Auditoría Ronda D: «Descargar Factura CFDI» no daba ninguna señal. `usePrintDocument` ahora muestra un toast de progreso, uno de éxito y, si falla, el motivo que responde el servidor (también cuando el cuerpo llega como Blob); la promesa sigue rechazando. `feedback: false` lo silencia y `PrintDocumentButton` deja el toast de error al llamador cuando pasa `onError`.
|
|
38
|
+
- Updated dependencies [4371f2c]
|
|
39
|
+
- @asteby/metacore-ui@2.24.0
|
|
40
|
+
|
|
3
41
|
## 46.4.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Translate } from './server-error';
|
|
1
2
|
import type { ActionFieldDef } from './types';
|
|
2
3
|
import { type ActionMetadata, type ActionModalProps } from '@asteby/metacore-sdk';
|
|
3
4
|
export type { ActionMetadata, ActionModalProps };
|
|
@@ -66,7 +67,15 @@ export declare function unwrapRecordScalar(value: unknown): unknown;
|
|
|
66
67
|
export declare function readRecordPath(record: any, path: string): unknown;
|
|
67
68
|
/** Scalar seed for one action field from the row being acted on. */
|
|
68
69
|
export declare function scalarDefaultFromRecord(field: ActionFieldDef, record: any): unknown;
|
|
69
|
-
export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }: ActionModalProps): import("react").JSX.Element | null;
|
|
70
|
+
export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }: ActionModalProps): import("react").JSX.Element | null;
|
|
70
71
|
/** How long an action with `modal` waits for its federated component. */
|
|
71
72
|
export declare const FEDERATED_ACTION_MODAL_TIMEOUT_MS = 20000;
|
|
73
|
+
/** Text for the in-dialog error banner of a failed action (PIT-046, #1023): a
|
|
74
|
+
* 422 with per-field `errors` -> "Revisa los campos" + each localized line; any
|
|
75
|
+
* other failure (500, network) -> the server's own message and cause. Unlike
|
|
76
|
+
* the toast it stays on screen until the next attempt. */
|
|
77
|
+
export declare function actionErrorBannerMessage(err: unknown, fields: readonly ActionFieldDef[] | undefined, t: Translate, language?: string): string;
|
|
78
|
+
export declare function buildFieldDefaults(fields: ActionFieldDef[], record: any): Record<string, any>;
|
|
79
|
+
/** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
|
|
80
|
+
export declare function renderField(field: ActionFieldDef, value: any, onChange: (value: any) => void, formValues?: Record<string, any>, record?: Record<string, any>, fieldErrors?: Record<string, string>): import("react").JSX.Element;
|
|
72
81
|
//# sourceMappingURL=action-modal-dispatcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"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"}
|
|
@@ -15,13 +15,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
//
|
|
16
16
|
// The host injects its axios-like client via <ApiProvider>; we no longer
|
|
17
17
|
// depend on a bundler alias to `@/lib/api`.
|
|
18
|
-
import {
|
|
18
|
+
import { emitRecordMutation } from './record-mutation-events';
|
|
19
|
+
import { useState, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
19
20
|
import { useTranslation } from 'react-i18next';
|
|
20
21
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
|
|
21
22
|
import { Loader2 } from 'lucide-react';
|
|
22
23
|
import { ProcessStepper } from '@asteby/metacore-ui/wizard';
|
|
23
24
|
import { toast } from 'sonner';
|
|
24
|
-
import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
|
|
25
|
+
import { toastServerError, toastServerSuccess, extractFieldErrors, extractServerError, localizeFieldErrorMap } from './server-error';
|
|
26
|
+
import { FormErrorBanner } from './business/feedback';
|
|
27
|
+
import { CfdiStampResultDialog, extractStampResult } from './cfdi-stamp-panel';
|
|
25
28
|
import { useBranchCreateGate } from './branch-create-gate';
|
|
26
29
|
import { validateValues, bagHasErrors } from './validator';
|
|
27
30
|
import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
|
|
@@ -233,7 +236,14 @@ export function scalarDefaultFromRecord(field, record) {
|
|
|
233
236
|
}
|
|
234
237
|
return undefined;
|
|
235
238
|
}
|
|
236
|
-
export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }) {
|
|
239
|
+
export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }) {
|
|
240
|
+
// Every successful action may have created/updated/deleted a record of
|
|
241
|
+
// `model` (federated component, wizard, generic form or confirmation), so
|
|
242
|
+
// announce it: mounted lists of the model reload on their own (#1020).
|
|
243
|
+
const onSuccess = useCallback(() => {
|
|
244
|
+
emitRecordMutation(model, 'update');
|
|
245
|
+
onSuccessProp?.();
|
|
246
|
+
}, [model, onSuccessProp]);
|
|
237
247
|
// Reactive read: a federated remote usually registers AFTER the first
|
|
238
248
|
// render, and a memoized lookup would pin the fallback forever.
|
|
239
249
|
const readComponent = () => getActionComponent(model, action.key);
|
|
@@ -518,10 +528,28 @@ function toastActionError(err, fields, t, language) {
|
|
|
518
528
|
}
|
|
519
529
|
toastServerError(err, { t, language });
|
|
520
530
|
}
|
|
531
|
+
/** Text for the in-dialog error banner of a failed action (PIT-046, #1023): a
|
|
532
|
+
* 422 with per-field `errors` -> "Revisa los campos" + each localized line; any
|
|
533
|
+
* other failure (500, network) -> the server's own message and cause. Unlike
|
|
534
|
+
* the toast it stays on screen until the next attempt. */
|
|
535
|
+
export function actionErrorBannerMessage(err, fields, t, language) {
|
|
536
|
+
const localized = localizeActionFieldErrors(err, fields, t, language);
|
|
537
|
+
if (localized) {
|
|
538
|
+
const head = t('validation.failed', { defaultValue: validationCatalog(language).failed });
|
|
539
|
+
const lines = Object.values(localized).filter(Boolean);
|
|
540
|
+
return lines.length ? `${head}: ${lines.join(' · ')}` : head;
|
|
541
|
+
}
|
|
542
|
+
const fallback = t('common.error', { defaultValue: 'Algo salió mal. Intenta de nuevo.' });
|
|
543
|
+
const ex = extractServerError(err, fallback);
|
|
544
|
+
const title = t(ex.title, { defaultValue: ex.title });
|
|
545
|
+
return ex.description && ex.description !== title ? `${title} — ${ex.description}` : title;
|
|
546
|
+
}
|
|
521
547
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
522
548
|
const { t, i18n } = useTranslation();
|
|
523
549
|
const api = useApi();
|
|
524
550
|
const [executing, setExecuting] = useState(false);
|
|
551
|
+
const [formError, setFormError] = useState();
|
|
552
|
+
const [stamp, setStamp] = useState();
|
|
525
553
|
// Actions listed in the model's `reason_required.actions` (cancel, void…)
|
|
526
554
|
// answer 422 errors.reason without a motive; the prompt asks and retries.
|
|
527
555
|
const reasonPrompt = useReasonPrompt();
|
|
@@ -532,6 +560,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
532
560
|
const label = t(action.label, { defaultValue: action.label });
|
|
533
561
|
const execute = async () => {
|
|
534
562
|
setExecuting(true);
|
|
563
|
+
setFormError(undefined);
|
|
535
564
|
try {
|
|
536
565
|
const url = buildActionUrl(endpoint, model, record.id, action.key);
|
|
537
566
|
// Supervisor PIN first (action.supervisorPolicy); null = cancelled.
|
|
@@ -548,21 +577,37 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
548
577
|
return;
|
|
549
578
|
if (res.data.success) {
|
|
550
579
|
toastServerSuccess(res.data, { t });
|
|
580
|
+
const result = extractStampResult(res.data);
|
|
581
|
+
if (result) {
|
|
582
|
+
setStamp(result);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
551
585
|
onOpenChange(false);
|
|
552
586
|
onSuccess();
|
|
553
587
|
}
|
|
554
588
|
else {
|
|
589
|
+
setFormError(actionErrorBannerMessage({ response: { data: res.data } }, action.fields, t, i18n.language));
|
|
555
590
|
toastActionError({ response: { data: res.data } }, action.fields, t, i18n.language);
|
|
556
591
|
}
|
|
557
592
|
}
|
|
558
593
|
catch (err) {
|
|
594
|
+
setFormError(actionErrorBannerMessage(err, action.fields, t, i18n.language));
|
|
559
595
|
toastActionError(err, action.fields, t, i18n.language);
|
|
560
596
|
}
|
|
561
597
|
finally {
|
|
562
598
|
setExecuting(false);
|
|
563
599
|
}
|
|
564
600
|
};
|
|
565
|
-
|
|
601
|
+
if (stamp) {
|
|
602
|
+
return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: label, onOpenChange: (o) => {
|
|
603
|
+
if (o)
|
|
604
|
+
return;
|
|
605
|
+
setStamp(undefined);
|
|
606
|
+
onOpenChange(false);
|
|
607
|
+
onSuccess();
|
|
608
|
+
} }));
|
|
609
|
+
}
|
|
610
|
+
return (_jsxs(_Fragment, { children: [reasonPrompt.dialog, _jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsx(FormErrorBanner, { message: formError }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) })] }));
|
|
566
611
|
}
|
|
567
612
|
function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
568
613
|
const { t, i18n } = useTranslation();
|
|
@@ -578,6 +623,10 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
578
623
|
const [executing, setExecuting] = useState(false);
|
|
579
624
|
// Per-field validation errors (localized), shown inline under each input.
|
|
580
625
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
626
|
+
// Persistent in-dialog banner for a failed submit (422 / 500) — the toast
|
|
627
|
+
// alone vanished and, for some failures, only the console had the cause.
|
|
628
|
+
const [formError, setFormError] = useState();
|
|
629
|
+
const [stamp, setStamp] = useState();
|
|
581
630
|
// Related records to surface BELOW the form, as read-only context for the
|
|
582
631
|
// record being acted on — e.g. the reception history of a transfer while
|
|
583
632
|
// receiving against it. Sourced from the model's metadata.relations (the
|
|
@@ -610,6 +659,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
610
659
|
if (open && action.fields) {
|
|
611
660
|
setFormData(buildFieldDefaults(action.fields, record));
|
|
612
661
|
setFieldErrors({});
|
|
662
|
+
setFormError(undefined);
|
|
613
663
|
}
|
|
614
664
|
}, [open, action.fields, record]);
|
|
615
665
|
// Lines the list row does not carry: load them from the declared relation
|
|
@@ -696,6 +746,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
696
746
|
};
|
|
697
747
|
const lang = i18n.language;
|
|
698
748
|
const handleActionError = (err) => {
|
|
749
|
+
setFormError(actionErrorBannerMessage(err, action.fields, t, lang));
|
|
699
750
|
const labels = labelsForValidationFields(action.fields, t);
|
|
700
751
|
const localized = localizeActionFieldErrors(err, action.fields, t, lang);
|
|
701
752
|
if (localized) {
|
|
@@ -733,6 +784,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
733
784
|
}
|
|
734
785
|
}
|
|
735
786
|
setFieldErrors({});
|
|
787
|
+
setFormError(undefined);
|
|
736
788
|
setExecuting(true);
|
|
737
789
|
try {
|
|
738
790
|
let payload = { ...formData };
|
|
@@ -765,6 +817,11 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
765
817
|
return;
|
|
766
818
|
if (res.data.success) {
|
|
767
819
|
toastServerSuccess(res.data, { t });
|
|
820
|
+
const result = extractStampResult(res.data);
|
|
821
|
+
if (result) {
|
|
822
|
+
setStamp(result);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
768
825
|
onOpenChange(false);
|
|
769
826
|
onSuccess();
|
|
770
827
|
}
|
|
@@ -794,17 +851,26 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
794
851
|
: hasLineItems
|
|
795
852
|
? '820px'
|
|
796
853
|
: undefined;
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
854
|
+
if (stamp) {
|
|
855
|
+
return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: tl(action.label), onOpenChange: (o) => {
|
|
856
|
+
if (o)
|
|
857
|
+
return;
|
|
858
|
+
setStamp(undefined);
|
|
859
|
+
onOpenChange(false);
|
|
860
|
+
onSuccess();
|
|
861
|
+
} }));
|
|
862
|
+
}
|
|
863
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: tl(action.confirmMessage) })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: [_jsx(FormErrorBanner, { message: formError, className: "mb-3" }), _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
|
|
864
|
+
const fullWidth = isLineItemsField(field) ||
|
|
865
|
+
resolveWidget(field) === 'textarea' ||
|
|
866
|
+
resolveWidget(field) === 'richtext';
|
|
867
|
+
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
|
|
868
|
+
}), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] })] }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] }), reasonPrompt.dialog] }) }));
|
|
803
869
|
}
|
|
804
870
|
// buildFieldDefaults seeds formData for a set of action fields, honoring the
|
|
805
871
|
// same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
|
|
806
872
|
// wizard steps and single-page forms initialize identically.
|
|
807
|
-
function buildFieldDefaults(fields, record) {
|
|
873
|
+
export function buildFieldDefaults(fields, record) {
|
|
808
874
|
const defaults = {};
|
|
809
875
|
for (const field of fields) {
|
|
810
876
|
if (isLineItemsField(field)) {
|
|
@@ -851,6 +917,8 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
851
917
|
const [stepIndex, setStepIndex] = useState(0);
|
|
852
918
|
const [formData, setFormData] = useState({});
|
|
853
919
|
const [executing, setExecuting] = useState(false);
|
|
920
|
+
const [formError, setFormError] = useState();
|
|
921
|
+
const [stamp, setStamp] = useState();
|
|
854
922
|
// Reset to the first step and seed defaults for EVERY step's fields whenever
|
|
855
923
|
// the modal (re)opens, so accumulated values from a prior run don't leak.
|
|
856
924
|
useEffect(() => {
|
|
@@ -859,6 +927,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
859
927
|
const allFields = steps.flatMap((s) => s.fields ?? []);
|
|
860
928
|
setFormData(buildFieldDefaults(allFields, record));
|
|
861
929
|
setStepIndex(0);
|
|
930
|
+
setFormError(undefined);
|
|
862
931
|
}, [open, action.steps, record]);
|
|
863
932
|
const updateField = (key, value) => setFormData((prev) => ({ ...prev, [key]: value }));
|
|
864
933
|
const step = steps[stepIndex];
|
|
@@ -890,6 +959,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
890
959
|
}
|
|
891
960
|
}
|
|
892
961
|
setExecuting(true);
|
|
962
|
+
setFormError(undefined);
|
|
893
963
|
try {
|
|
894
964
|
const url = buildActionUrl(endpoint, model, record?.id, action.key);
|
|
895
965
|
const auth = await supervisor.authorize({ label: tl(action.label), supervisorPolicy: action.supervisorPolicy }, { model, recordId: record?.id != null ? String(record.id) : undefined });
|
|
@@ -902,16 +972,24 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
902
972
|
});
|
|
903
973
|
if (!res)
|
|
904
974
|
return;
|
|
975
|
+
const allFields = steps.flatMap(s => s.fields ?? []);
|
|
905
976
|
if (res.data.success) {
|
|
906
977
|
toastServerSuccess(res.data, { t });
|
|
978
|
+
const result = extractStampResult(res.data);
|
|
979
|
+
if (result) {
|
|
980
|
+
setStamp(result);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
907
983
|
onOpenChange(false);
|
|
908
984
|
onSuccess();
|
|
909
985
|
}
|
|
910
986
|
else {
|
|
911
|
-
|
|
987
|
+
setFormError(actionErrorBannerMessage({ response: { data: res.data } }, allFields, t, i18n.language));
|
|
988
|
+
toastActionError({ response: { data: res.data } }, allFields, t, i18n.language);
|
|
912
989
|
}
|
|
913
990
|
}
|
|
914
991
|
catch (err) {
|
|
992
|
+
setFormError(actionErrorBannerMessage(err, steps.flatMap(s => s.fields ?? []), t, i18n.language));
|
|
915
993
|
toastActionError(err, steps.flatMap(s => s.fields ?? []), t, i18n.language);
|
|
916
994
|
}
|
|
917
995
|
finally {
|
|
@@ -920,12 +998,21 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
920
998
|
};
|
|
921
999
|
if (steps.length === 0)
|
|
922
1000
|
return null;
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1001
|
+
if (stamp) {
|
|
1002
|
+
return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: tl(action.label), onOpenChange: (o) => {
|
|
1003
|
+
if (o)
|
|
1004
|
+
return;
|
|
1005
|
+
setStamp(undefined);
|
|
1006
|
+
onOpenChange(false);
|
|
1007
|
+
onSuccess();
|
|
1008
|
+
} }));
|
|
1009
|
+
}
|
|
1010
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), _jsxs("div", { className: "pt-1", children: [_jsx(ProcessStepper, { steps: steps.map((s, i) => ({ key: String(i), label: s.title ? tl(s.title) : `${t('common.step', { defaultValue: 'Paso' })} ${i + 1}` })), activeIndex: stepIndex, onStepClick: i => setStepIndex(i) }), _jsxs(DialogDescription, { className: "pt-3", children: [t('common.step', { defaultValue: 'Paso' }), " ", stepIndex + 1, "/", steps.length, step?.title ? ` · ${tl(step.title)}` : ''] }), step?.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: tl(step.description) }))] })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: [_jsx(FormErrorBanner, { message: formError, className: "mb-3" }), _jsx(FieldGrid, { children: stepFields.map((field) => {
|
|
1011
|
+
const fullWidth = isLineItemsField(field) ||
|
|
1012
|
+
resolveWidget(field) === 'textarea' ||
|
|
1013
|
+
resolveWidget(field) === 'richtext';
|
|
1014
|
+
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record)] }, field.key));
|
|
1015
|
+
}) })] }), _jsxs(DialogFooter, { className: "shrink-0", children: [stepIndex > 0 ? (_jsx(Button, { variant: "outline", onClick: goBack, disabled: executing, children: t('common.back', { defaultValue: 'Atrás' }) })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') })), isLast ? (_jsxs(Button, { onClick: submit, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })) : (_jsx(Button, { onClick: goNext, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: t('common.next', { defaultValue: 'Siguiente' }) }))] }), reasonPrompt.dialog] }) }));
|
|
929
1016
|
}
|
|
930
1017
|
function seedOptionFromRecord(field, value, record) {
|
|
931
1018
|
if (!record || !field.key.endsWith('_id'))
|
|
@@ -948,7 +1035,8 @@ function seedOptionFromRecord(field, value, record) {
|
|
|
948
1035
|
icon: sib.icon,
|
|
949
1036
|
};
|
|
950
1037
|
}
|
|
951
|
-
|
|
1038
|
+
/** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
|
|
1039
|
+
export function renderField(field, value, onChange,
|
|
952
1040
|
// Full current form values — lets a line-items grid (and any cascading
|
|
953
1041
|
// header picker) resolve a `dependsOn` reference against sibling header
|
|
954
1042
|
// fields. Omitted by callers that have no surrounding form (the field is
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit-info.tsx
|
|
3
|
+
*
|
|
4
|
+
* <AuditInfo> — discreet "when and who" footer for a record: created, modified
|
|
5
|
+
* and (if tombstoned) deleted, each with the actor and a relative + absolute
|
|
6
|
+
* date. Driven only by the kernel's `TableMetadata.audit` (column names) and
|
|
7
|
+
* the record row — manifests declare nothing.
|
|
8
|
+
*
|
|
9
|
+
* Actor resolution (no extra requests), in order:
|
|
10
|
+
* 1. the host's `resolveActor(id)` when it returns a name;
|
|
11
|
+
* 2. the record's expanded sibling (`created_by_id` → `row.created_by`,
|
|
12
|
+
* `{ label | name | title }` or a plain string);
|
|
13
|
+
* 3. the system actor id → "Sistema" / "System";
|
|
14
|
+
* 4. a short id (first 8 chars) — never a long uuid.
|
|
15
|
+
*
|
|
16
|
+
* Renders nothing when `audit` is absent or the record has none of its values.
|
|
17
|
+
*/
|
|
18
|
+
import * as React from 'react';
|
|
19
|
+
import type { AuditMeta } from './types';
|
|
20
|
+
/** The kernel's actor for unattended work (schedules, webhooks, connectors). */
|
|
21
|
+
export declare const SYSTEM_ACTOR_ID = "00000000-0000-0000-0000-000000000001";
|
|
22
|
+
/**
|
|
23
|
+
* Defensive reader for the served `audit` block: keeps only known keys whose
|
|
24
|
+
* value is a non-empty string; returns `undefined` when nothing usable remains.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readAuditMeta(raw: unknown): AuditMeta | undefined;
|
|
27
|
+
export interface AuditActor {
|
|
28
|
+
/** Display text. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** True when the name is a real resolved label (not a short id). */
|
|
31
|
+
resolved: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Resolves the actor stored in `row[idColumn]` to something human. */
|
|
34
|
+
export declare function resolveAuditActor(row: Record<string, unknown>, idColumn: string | undefined, opts: {
|
|
35
|
+
resolveActor?: (id: string) => string | undefined;
|
|
36
|
+
systemLabel: string;
|
|
37
|
+
}): AuditActor | null;
|
|
38
|
+
export interface AuditInfoProps {
|
|
39
|
+
/** The record row (keys are the column names `audit` points at). */
|
|
40
|
+
record: Record<string, unknown> | null | undefined;
|
|
41
|
+
/** `TableMetadata.audit`. Absent → renders nothing. */
|
|
42
|
+
audit: AuditMeta | null | undefined;
|
|
43
|
+
/** Optional host resolver uuid → display name (e.g. from a users cache). */
|
|
44
|
+
resolveActor?: (id: string) => string | undefined;
|
|
45
|
+
/** IANA timezone; defaults to the org runtime timezone. */
|
|
46
|
+
timeZone?: string;
|
|
47
|
+
/** BCP-47 locale; defaults to the i18n language. */
|
|
48
|
+
locale?: string;
|
|
49
|
+
/** Start expanded. Default: collapsed. */
|
|
50
|
+
defaultOpen?: boolean;
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function AuditInfo({ record, audit, resolveActor, timeZone, locale, defaultOpen, className, }: AuditInfoProps): React.JSX.Element | null;
|
|
54
|
+
//# sourceMappingURL=audit-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-info.d.ts","sourceRoot":"","sources":["../src/audit-info.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,gFAAgF;AAChF,eAAO,MAAM,eAAe,yCAAyC,CAAA;AAWrE;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CASjE;AAED,MAAM,WAAW,UAAU;IACvB,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAA;CACpB;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE;IAAE,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACjF,UAAU,GAAG,IAAI,CAmBnB;AAyBD,MAAM,WAAW,cAAc;IAC3B,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAA;IAClD,uDAAuD;IACvD,KAAK,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAAA;IACnC,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACjD,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAUD,wBAAgB,SAAS,CAAC,EACtB,MAAM,EACN,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,WAAmB,EACnB,SAAS,GACZ,EAAE,cAAc,4BA0EhB"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* audit-info.tsx
|
|
4
|
+
*
|
|
5
|
+
* <AuditInfo> — discreet "when and who" footer for a record: created, modified
|
|
6
|
+
* and (if tombstoned) deleted, each with the actor and a relative + absolute
|
|
7
|
+
* date. Driven only by the kernel's `TableMetadata.audit` (column names) and
|
|
8
|
+
* the record row — manifests declare nothing.
|
|
9
|
+
*
|
|
10
|
+
* Actor resolution (no extra requests), in order:
|
|
11
|
+
* 1. the host's `resolveActor(id)` when it returns a name;
|
|
12
|
+
* 2. the record's expanded sibling (`created_by_id` → `row.created_by`,
|
|
13
|
+
* `{ label | name | title }` or a plain string);
|
|
14
|
+
* 3. the system actor id → "Sistema" / "System";
|
|
15
|
+
* 4. a short id (first 8 chars) — never a long uuid.
|
|
16
|
+
*
|
|
17
|
+
* Renders nothing when `audit` is absent or the record has none of its values.
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
import { useTranslation } from 'react-i18next';
|
|
21
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
22
|
+
import { es, enUS } from 'date-fns/locale';
|
|
23
|
+
import { ChevronDown, ChevronRight, History } from 'lucide-react';
|
|
24
|
+
import { cn } from '@asteby/metacore-ui/lib';
|
|
25
|
+
import { formatDateCell } from './dynamic-columns';
|
|
26
|
+
import { objectLabel } from './dynamic-relation-helpers';
|
|
27
|
+
import { isNilUuid } from './nil-uuid';
|
|
28
|
+
import { useTimeZone } from './org-runtime-context';
|
|
29
|
+
/** The kernel's actor for unattended work (schedules, webhooks, connectors). */
|
|
30
|
+
export const SYSTEM_ACTOR_ID = '00000000-0000-0000-0000-000000000001';
|
|
31
|
+
const AUDIT_KEYS = [
|
|
32
|
+
'created_at',
|
|
33
|
+
'created_by',
|
|
34
|
+
'updated_at',
|
|
35
|
+
'updated_by',
|
|
36
|
+
'deleted_at',
|
|
37
|
+
'deleted_by',
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Defensive reader for the served `audit` block: keeps only known keys whose
|
|
41
|
+
* value is a non-empty string; returns `undefined` when nothing usable remains.
|
|
42
|
+
*/
|
|
43
|
+
export function readAuditMeta(raw) {
|
|
44
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
45
|
+
return undefined;
|
|
46
|
+
const src = raw;
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const k of AUDIT_KEYS) {
|
|
49
|
+
const v = src[k];
|
|
50
|
+
if (typeof v === 'string' && v.trim() !== '')
|
|
51
|
+
out[k] = v;
|
|
52
|
+
}
|
|
53
|
+
return Object.keys(out).length ? out : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Resolves the actor stored in `row[idColumn]` to something human. */
|
|
56
|
+
export function resolveAuditActor(row, idColumn, opts) {
|
|
57
|
+
if (!idColumn)
|
|
58
|
+
return null;
|
|
59
|
+
const raw = row[idColumn];
|
|
60
|
+
// The column itself may already be an expanded object.
|
|
61
|
+
const own = objectLabel(raw);
|
|
62
|
+
const id = typeof raw === 'string' ? raw.trim() : '';
|
|
63
|
+
if (!own && (!id || isNilUuid(id)))
|
|
64
|
+
return null;
|
|
65
|
+
if (id.toLowerCase() === SYSTEM_ACTOR_ID)
|
|
66
|
+
return { name: opts.systemLabel, resolved: true };
|
|
67
|
+
if (id) {
|
|
68
|
+
const fromHost = opts.resolveActor?.(id);
|
|
69
|
+
if (fromHost)
|
|
70
|
+
return { name: fromHost, resolved: true };
|
|
71
|
+
}
|
|
72
|
+
if (own)
|
|
73
|
+
return { name: own, resolved: true };
|
|
74
|
+
const base = idColumn.endsWith('_id') ? idColumn.slice(0, -3) : idColumn;
|
|
75
|
+
const sibling = base === idColumn ? undefined : row[base];
|
|
76
|
+
const siblingName = objectLabel(sibling) ?? (typeof sibling === 'string' && sibling.trim() !== '' ? sibling : undefined);
|
|
77
|
+
if (siblingName)
|
|
78
|
+
return { name: siblingName, resolved: true };
|
|
79
|
+
return { name: id.length > 8 ? id.slice(0, 8) : id, resolved: false };
|
|
80
|
+
}
|
|
81
|
+
const COPY = {
|
|
82
|
+
es: {
|
|
83
|
+
title: 'Historial del registro',
|
|
84
|
+
created: 'Creado por {{who}}',
|
|
85
|
+
updated: 'Modificado por {{who}}',
|
|
86
|
+
deleted: 'Eliminado por {{who}}',
|
|
87
|
+
createdAt: 'Creado',
|
|
88
|
+
updatedAt: 'Modificado',
|
|
89
|
+
deletedAt: 'Eliminado',
|
|
90
|
+
system: 'Sistema',
|
|
91
|
+
},
|
|
92
|
+
en: {
|
|
93
|
+
title: 'Record history',
|
|
94
|
+
created: 'Created by {{who}}',
|
|
95
|
+
updated: 'Modified by {{who}}',
|
|
96
|
+
deleted: 'Deleted by {{who}}',
|
|
97
|
+
createdAt: 'Created',
|
|
98
|
+
updatedAt: 'Modified',
|
|
99
|
+
deletedAt: 'Deleted',
|
|
100
|
+
system: 'System',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
export function AuditInfo({ record, audit, resolveActor, timeZone, locale, defaultOpen = false, className, }) {
|
|
104
|
+
const { t, i18n } = useTranslation();
|
|
105
|
+
const ctxTz = useTimeZone();
|
|
106
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
107
|
+
const meta = readAuditMeta(audit);
|
|
108
|
+
if (!meta || !record)
|
|
109
|
+
return null;
|
|
110
|
+
const lang = (locale || i18n?.language || 'es').toLowerCase().startsWith('en') ? 'en' : 'es';
|
|
111
|
+
const copy = COPY[lang];
|
|
112
|
+
const dfLocale = lang === 'en' ? enUS : es;
|
|
113
|
+
const tz = timeZone ?? ctxTz;
|
|
114
|
+
const tt = (key, vars) => t(`audit.${key}`, { defaultValue: copy[key], ...vars });
|
|
115
|
+
const build = (key, atCol, byCol, whoKey, whenKey) => {
|
|
116
|
+
const f = atCol ? formatDateCell(record[atCol], undefined, dfLocale, tz) : null;
|
|
117
|
+
if (!f)
|
|
118
|
+
return null;
|
|
119
|
+
const actor = resolveAuditActor(record, byCol, { resolveActor, systemLabel: tt('system') });
|
|
120
|
+
const date = new Date(record[atCol]);
|
|
121
|
+
const relative = formatDistanceToNow(date, { addSuffix: true, locale: dfLocale });
|
|
122
|
+
return {
|
|
123
|
+
key,
|
|
124
|
+
text: actor ? tt(whoKey, { who: actor.name }) : tt(whenKey),
|
|
125
|
+
absolute: f.display,
|
|
126
|
+
title: f.title,
|
|
127
|
+
relative,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const lines = [
|
|
131
|
+
build('created', meta.created_at, meta.created_by, 'created', 'createdAt'),
|
|
132
|
+
// Hide "modified" when it is the creation instant itself (never edited).
|
|
133
|
+
meta.updated_at && record[meta.updated_at] !== record[meta.created_at ?? '']
|
|
134
|
+
? build('updated', meta.updated_at, meta.updated_by, 'updated', 'updatedAt')
|
|
135
|
+
: null,
|
|
136
|
+
build('deleted', meta.deleted_at, meta.deleted_by, 'deleted', 'deletedAt'),
|
|
137
|
+
].filter((l) => l !== null);
|
|
138
|
+
if (lines.length === 0)
|
|
139
|
+
return null;
|
|
140
|
+
const Chevron = open ? ChevronDown : ChevronRight;
|
|
141
|
+
return (_jsxs("div", { className: cn('text-xs text-muted-foreground', className), "data-testid": "audit-info", children: [_jsxs("button", { type: "button", className: "flex items-center gap-1.5 hover:text-foreground", "aria-expanded": open, onClick: () => setOpen(o => !o), children: [_jsx(Chevron, { className: "h-3.5 w-3.5" }), _jsx(History, { className: "h-3.5 w-3.5" }), _jsx("span", { children: tt('title') })] }), open && (_jsx("ul", { className: "mt-2 space-y-1 pl-6", children: lines.map(l => (_jsxs("li", { "data-audit-line": l.key, children: [_jsx("span", { children: l.text }), ' · ', _jsx("time", { title: l.title, children: l.absolute }), _jsxs("span", { className: "opacity-70", children: [" (", l.relative, ")"] })] }, l.key))) }))] }));
|
|
142
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAsGtD,CAAA"}
|
package/dist/business/catalog.js
CHANGED
|
@@ -76,4 +76,28 @@ export const BUSINESS_COMPONENTS = [
|
|
|
76
76
|
'recommendationsToLines',
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: 'VehiclePicker',
|
|
81
|
+
value: 'value: VehicleResult | null',
|
|
82
|
+
events: ['onChange'],
|
|
83
|
+
permissions: ['<model>.create (alta rápida; default Vehicle → vehicle.create)'],
|
|
84
|
+
usedBy: ['POS', 'Recepción de vehículo', 'OT', 'Cotización', 'Pedido'],
|
|
85
|
+
helpers: ['describeVehicle'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'RelateDocuments',
|
|
89
|
+
value: 'value: RelatedDocument[] (related_document_id + relation_type)',
|
|
90
|
+
events: ['onChange'],
|
|
91
|
+
permissions: [],
|
|
92
|
+
usedBy: ['Nota de crédito', 'Devolución', 'Sustitución de CFDI', 'REP'],
|
|
93
|
+
helpers: ['serializeRelatedDocuments'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'PrintSendDialog',
|
|
97
|
+
value: 'document: PrintableDocument (+ open)',
|
|
98
|
+
events: ['onSend', 'onPrint', 'onOpenChange'],
|
|
99
|
+
permissions: [],
|
|
100
|
+
usedBy: ['Cotización', 'Factura', 'Nota de crédito', 'Pedido', 'OC'],
|
|
101
|
+
helpers: [],
|
|
102
|
+
},
|
|
79
103
|
];
|
package/dist/business/index.d.ts
CHANGED
|
@@ -16,4 +16,7 @@ export * from './return';
|
|
|
16
16
|
export * from './return-wizard';
|
|
17
17
|
export * from './inspection';
|
|
18
18
|
export * from './inspection-checklist';
|
|
19
|
+
export * from './vehicle-picker';
|
|
20
|
+
export * from './relate-documents';
|
|
21
|
+
export * from './print-send-dialog';
|
|
19
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
|