@asteby/metacore-runtime-react 46.4.0 → 47.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -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/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 +5 -0
- 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-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +3 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +6 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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/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 +46 -0
- package/dist/types.d.ts.map +1 -1
- 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__/business-pickers.test.tsx +157 -0
- package/src/__tests__/document-form-dialog.test.tsx +190 -0
- package/src/__tests__/dynamic-table-mutation-refresh.test.tsx +116 -0
- package/src/__tests__/nav-permissions.test.tsx +76 -0
- package/src/__tests__/permissions-manager.test.tsx +21 -4
- package/src/__tests__/use-print-document-feedback.test.tsx +58 -0
- package/src/action-modal-dispatcher.tsx +124 -6
- 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 +5 -0
- package/src/document-form-dialog.tsx +357 -0
- package/src/dynamic-crud-page.tsx +21 -2
- package/src/dynamic-kanban.tsx +3 -1
- package/src/dynamic-table.tsx +6 -2
- package/src/index.ts +28 -0
- package/src/nav-permissions.ts +58 -0
- package/src/navigation-builder.tsx +27 -1
- 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 +49 -0
- package/src/use-print-document.ts +66 -3
- package/src/use-record-mutation-tick.ts +12 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface VehicleResult {
|
|
2
|
+
id: string;
|
|
3
|
+
/** Placa tal como la capturó el taller (sin formato hardcodeado por país). */
|
|
4
|
+
plate?: string;
|
|
5
|
+
vin?: string;
|
|
6
|
+
make?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
year?: number | string;
|
|
9
|
+
color?: string;
|
|
10
|
+
/** Propietario, cuando el host lo expone. */
|
|
11
|
+
customer_id?: string;
|
|
12
|
+
customer_name?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface VehiclePickerProps {
|
|
15
|
+
value: VehicleResult | null;
|
|
16
|
+
/** Evento único de cambio: vehículo elegido/creado, o `null` al limpiar. */
|
|
17
|
+
onChange: (vehicle: VehicleResult | null) => void;
|
|
18
|
+
/** Búsqueda por placa o VIN. Estable (useCallback). */
|
|
19
|
+
search: (q: string, signal: AbortSignal) => Promise<VehicleResult[]>;
|
|
20
|
+
/** Modelo del kernel para el alta rápida. Default `Vehicle`. */
|
|
21
|
+
model?: string;
|
|
22
|
+
/** Endpoint CRUD del alta. Default `/data/<model>/me`. */
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
/** Convierte el registro guardado a `VehicleResult`. Default: id, placa, VIN, marca, modelo, año. */
|
|
25
|
+
mapRecord?: (rec: Record<string, unknown>) => VehicleResult;
|
|
26
|
+
/** Override de permiso (default `<model>.create` con useCan). */
|
|
27
|
+
canCreate?: boolean;
|
|
28
|
+
/** Valores iniciales del alta (p. ej. `customer_id` del cliente ya elegido). */
|
|
29
|
+
createDefaults?: Record<string, unknown>;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Línea corta de un vehículo: «Nissan Versa 2019 · ABC-123 · VIN…». */
|
|
34
|
+
export declare function describeVehicle(v: VehicleResult): string;
|
|
35
|
+
export declare function VehiclePicker({ value, onChange, search, model, endpoint, mapRecord, canCreate, createDefaults, disabled, placeholder, }: VehiclePickerProps): import("react").JSX.Element;
|
|
36
|
+
//# sourceMappingURL=vehicle-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vehicle-picker.d.ts","sourceRoot":"","sources":["../../src/business/vehicle-picker.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;IAC3B,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI,CAAA;IACjD,uDAAuD;IACvD,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IACpE,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qGAAqG;IACrG,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,aAAa,CAAA;IAC3D,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAQD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAGxD;AAED,wBAAgB,aAAa,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAiB,EACjB,QAAQ,EACR,SAAS,EACT,SAAS,EACT,cAAc,EACd,QAAQ,EACR,WAAW,GACd,EAAE,kBAAkB,+BAmHpB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// VehiclePicker + alta rápida — busca por placa o VIN (y marca/modelo/cliente que
|
|
3
|
+
// el host decida indexar) y reutiliza el <CreateRecordDialog> dinámico del modelo
|
|
4
|
+
// vehículo para el alta rápida. Mismo contrato que <CustomerPicker>: controlado
|
|
5
|
+
// (`value` / `onChange`), `search` inyectado por el host, permisos con useCan
|
|
6
|
+
// (sin <PermissionsProvider> no bloquea nada: el backend autoriza).
|
|
7
|
+
import { useCallback, useState } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Car, Plus, Search, X } from 'lucide-react';
|
|
10
|
+
import { Button, Input } from '@asteby/metacore-ui';
|
|
11
|
+
import { CreateRecordDialog } from '../dialogs/create-record-dialog';
|
|
12
|
+
import { useApi } from '../api-context';
|
|
13
|
+
import { useCan } from '../permissions-context';
|
|
14
|
+
import { useAsyncSearch } from './use-async-search';
|
|
15
|
+
import { EmptyState } from './feedback';
|
|
16
|
+
function snake(model) {
|
|
17
|
+
return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
20
|
+
/** Línea corta de un vehículo: «Nissan Versa 2019 · ABC-123 · VIN…». */
|
|
21
|
+
export function describeVehicle(v) {
|
|
22
|
+
const unit = [v.make, v.model, v.year].filter((x) => x != null && x !== '').join(' ');
|
|
23
|
+
return [unit, v.plate, v.vin].filter(Boolean).join(' · ');
|
|
24
|
+
}
|
|
25
|
+
export function VehiclePicker({ value, onChange, search, model = 'Vehicle', endpoint, mapRecord, canCreate, createDefaults, disabled, placeholder, }) {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const api = useApi();
|
|
28
|
+
const can = useCan();
|
|
29
|
+
const [text, setText] = useState('');
|
|
30
|
+
const [creating, setCreating] = useState(false);
|
|
31
|
+
const { results, loading, error } = useAsyncSearch(text, search, { minChars: 2 });
|
|
32
|
+
const mayCreate = canCreate ?? can(`${snake(model)}.create`);
|
|
33
|
+
const base = endpoint ?? `/data/${model}/me`;
|
|
34
|
+
const pick = (v) => {
|
|
35
|
+
onChange(v);
|
|
36
|
+
setText('');
|
|
37
|
+
};
|
|
38
|
+
const toResult = useCallback((rec) => mapRecord
|
|
39
|
+
? mapRecord(rec)
|
|
40
|
+
: {
|
|
41
|
+
id: String(rec.id ?? ''),
|
|
42
|
+
plate: str(rec.plate),
|
|
43
|
+
vin: str(rec.vin),
|
|
44
|
+
make: str(rec.make),
|
|
45
|
+
model: str(rec.model),
|
|
46
|
+
year: typeof rec.year === 'number' || typeof rec.year === 'string' ? rec.year : undefined,
|
|
47
|
+
color: str(rec.color),
|
|
48
|
+
customer_id: str(rec.customer_id),
|
|
49
|
+
}, [mapRecord]);
|
|
50
|
+
if (value) {
|
|
51
|
+
return (_jsxs("div", { "data-slot": "vehicle-picker", className: "flex items-start justify-between gap-2 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(Car, { className: "mt-0.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: value.plate || value.vin || t('vehiclePicker.noPlate', { defaultValue: 'Sin placa' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: [[value.make, value.model, value.year].filter((x) => x != null && x !== '').join(' '), value.vin && value.plate ? value.vin : '', value.customer_name]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(' · ') })] })] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => onChange(null), "aria-label": t('vehiclePicker.clear', { defaultValue: 'Quitar vehículo' }), children: _jsx(X, { className: "size-4" }) }))] }));
|
|
54
|
+
}
|
|
55
|
+
return (_jsxs("div", { "data-slot": "vehicle-picker", className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", disabled: disabled, value: text, placeholder: placeholder ?? t('vehiclePicker.placeholder', { defaultValue: 'Placa o VIN' }), onChange: (e) => setText(e.target.value) })] }), mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: () => setCreating(true), "aria-label": t('vehiclePicker.create', { defaultValue: 'Nuevo vehículo' }), children: _jsx(Plus, { className: "size-4" }) }))] }), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('vehiclePicker.error', { defaultValue: 'No se pudo buscar vehículos. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('vehiclePicker.empty', { defaultValue: 'No encontramos ese vehículo' }), action: mayCreate ? { label: t('vehiclePicker.createAction', { defaultValue: 'Crear vehículo' }), onClick: () => setCreating(true) } : undefined })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((v) => (_jsx("li", { role: "option", "aria-selected": false, children: _jsxs("button", { type: "button", className: "flex w-full flex-col px-2 py-1.5 text-left hover:bg-accent", onClick: () => pick(v), children: [_jsx("span", { className: "text-sm font-medium", children: v.plate || v.vin || v.id }), _jsx("span", { className: "text-xs text-muted-foreground", children: [describeVehicle({ ...v, plate: undefined }), v.customer_name].filter(Boolean).join(' · ') })] }) }, v.id))) })), creating && (_jsx(CreateRecordDialog, { modelKey: model, open: creating, onOpenChange: setCreating, endpoint: base, defaults: createDefaults ?? (text.trim() ? { plate: text.trim() } : undefined), onCreate: async (data) => {
|
|
56
|
+
const res = await api.post(base, data);
|
|
57
|
+
const rec = (res.data?.data ?? res.data);
|
|
58
|
+
pick(toResult(rec));
|
|
59
|
+
return rec.id != null ? { id: String(rec.id) } : undefined;
|
|
60
|
+
} }))] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CfdiStampResult {
|
|
2
|
+
fiscalUuid?: string;
|
|
3
|
+
pdfUrl?: string;
|
|
4
|
+
xmlUrl?: string;
|
|
5
|
+
/** Optional human folio (`serie-folio`) when the server sends it. */
|
|
6
|
+
folio?: string;
|
|
7
|
+
documentId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads a stamp result out of a successful action response body. Looks at
|
|
11
|
+
* `body.data`, `body.result` and the body itself. Returns `undefined` unless
|
|
12
|
+
* the payload carries `fiscal_uuid` and/or `pdf_url` / `xml_url` — a response
|
|
13
|
+
* without those keys is never turned into a fiscal result.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractStampResult(body: unknown): CfdiStampResult | undefined;
|
|
16
|
+
export interface CfdiStampPanelProps {
|
|
17
|
+
result: CfdiStampResult;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function CfdiStampPanel({ result, className }: CfdiStampPanelProps): import("react").JSX.Element;
|
|
21
|
+
export interface CfdiStampResultDialogProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
result: CfdiStampResult;
|
|
25
|
+
title?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Dialog shell used by the action modals after a successful stamp. */
|
|
28
|
+
export declare function CfdiStampResultDialog({ open, onOpenChange, result, title }: CfdiStampResultDialogProps): import("react").JSX.Element;
|
|
29
|
+
//# sourceMappingURL=cfdi-stamp-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cfdi-stamp-panel.d.ts","sourceRoot":"","sources":["../src/cfdi-stamp-panel.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAOD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAsB7E;AAED,MAAM,WAAW,mBAAmB;IAChC,MAAM,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,mBAAmB,+BAkDxE;AAED,MAAM,WAAW,0BAA0B;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,MAAM,EAAE,eAAe,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,0BAA0B,+BAkBtG"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// CfdiStampPanel — human-readable result of stamping (timbrar) a CFDI.
|
|
3
|
+
//
|
|
4
|
+
// A stamp action answers `{ document_id, fiscal_uuid, pdf_url, xml_url }`. The
|
|
5
|
+
// dialog used to dump that JSON; this panel shows the fiscal UUID as text and
|
|
6
|
+
// the PDF / XML as download links instead (#1024, FAC-3 / PIT-051). The SDK
|
|
7
|
+
// invents nothing: only the keys the server actually returned are rendered.
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, } from '@asteby/metacore-ui/primitives';
|
|
10
|
+
import { CheckCircle2, FileCode2, FileText } from 'lucide-react';
|
|
11
|
+
const str = (v) => typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
12
|
+
const isSafeUrl = (u) => /^(https?:)?\/\//i.test(u) || u.startsWith('/') || u.startsWith('blob:');
|
|
13
|
+
/**
|
|
14
|
+
* Reads a stamp result out of a successful action response body. Looks at
|
|
15
|
+
* `body.data`, `body.result` and the body itself. Returns `undefined` unless
|
|
16
|
+
* the payload carries `fiscal_uuid` and/or `pdf_url` / `xml_url` — a response
|
|
17
|
+
* without those keys is never turned into a fiscal result.
|
|
18
|
+
*/
|
|
19
|
+
export function extractStampResult(body) {
|
|
20
|
+
if (!body || typeof body !== 'object')
|
|
21
|
+
return undefined;
|
|
22
|
+
const b = body;
|
|
23
|
+
const candidates = [b.data, b.result, b];
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
if (!c || typeof c !== 'object' || Array.isArray(c))
|
|
26
|
+
continue;
|
|
27
|
+
const o = c;
|
|
28
|
+
const fiscalUuid = str(o.fiscal_uuid) ?? str(o.fiscalUuid) ?? str(o.uuid);
|
|
29
|
+
const pdf = str(o.pdf_url) ?? str(o.pdfUrl);
|
|
30
|
+
const xml = str(o.xml_url) ?? str(o.xmlUrl);
|
|
31
|
+
// A bare `uuid` is too generic to call a fiscal result on its own.
|
|
32
|
+
const hasFiscal = !!str(o.fiscal_uuid) || !!str(o.fiscalUuid) || !!pdf || !!xml;
|
|
33
|
+
if (!hasFiscal)
|
|
34
|
+
continue;
|
|
35
|
+
return {
|
|
36
|
+
fiscalUuid,
|
|
37
|
+
pdfUrl: pdf && isSafeUrl(pdf) ? pdf : undefined,
|
|
38
|
+
xmlUrl: xml && isSafeUrl(xml) ? xml : undefined,
|
|
39
|
+
folio: str(o.folio) ?? str(o.number),
|
|
40
|
+
documentId: str(o.document_id) ?? str(o.documentId),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
export function CfdiStampPanel({ result, className }) {
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
return (_jsxs("div", { "data-slot": "cfdi-stamp-panel", className: `space-y-4 ${className ?? ''}`, children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-emerald-600 dark:text-emerald-400", children: [_jsx(CheckCircle2, { className: "size-5", "aria-hidden": true }), t('cfdi.stamp.success', { defaultValue: 'CFDI timbrado correctamente' })] }), _jsxs("dl", { className: "space-y-2 rounded-md border bg-muted/40 p-3 text-sm", children: [result.folio && (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("dt", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t('cfdi.stamp.folio', { defaultValue: 'Folio' }) }), _jsx("dd", { children: result.folio })] })), result.fiscalUuid && (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("dt", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t('cfdi.stamp.uuid', { defaultValue: 'UUID fiscal' }) }), _jsx("dd", { className: "break-all font-mono", "data-slot": "cfdi-stamp-uuid", children: result.fiscalUuid })] }))] }), (result.pdfUrl || result.xmlUrl) && (_jsxs("div", { className: "flex flex-wrap gap-2", children: [result.pdfUrl && (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs("a", { href: result.pdfUrl, target: "_blank", rel: "noopener noreferrer", "data-slot": "cfdi-stamp-pdf", children: [_jsx(FileText, { className: "mr-2 size-4", "aria-hidden": true }), t('cfdi.stamp.pdf', { defaultValue: 'Descargar PDF' })] }) })), result.xmlUrl && (_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs("a", { href: result.xmlUrl, target: "_blank", rel: "noopener noreferrer", "data-slot": "cfdi-stamp-xml", children: [_jsx(FileCode2, { className: "mr-2 size-4", "aria-hidden": true }), t('cfdi.stamp.xml', { defaultValue: 'Descargar XML' })] }) }))] }))] }));
|
|
48
|
+
}
|
|
49
|
+
/** Dialog shell used by the action modals after a successful stamp. */
|
|
50
|
+
export function CfdiStampResultDialog({ open, onOpenChange, result, title }) {
|
|
51
|
+
const { t } = useTranslation();
|
|
52
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title ?? t('cfdi.stamp.title', { defaultValue: 'Resultado del timbrado' }) }), _jsx(DialogDescription, { className: "sr-only", children: t('cfdi.stamp.desc', { defaultValue: 'UUID fiscal y archivos del CFDI timbrado' }) })] }), _jsx(CfdiStampPanel, { result: result }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => onOpenChange(false), children: t('common.close', { defaultValue: 'Cerrar' }) }) })] }) }));
|
|
53
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
// flows through <ApiProvider> from runtime-react. Host-specific runtime values —
|
|
11
11
|
// the image-url resolver and the org IANA timezone — are passed as props so the
|
|
12
12
|
// SDK stays transport- and host-agnostic.
|
|
13
|
+
import { emitRecordMutation } from '../record-mutation-events';
|
|
13
14
|
import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
|
|
14
15
|
import { useTranslation } from 'react-i18next';
|
|
15
16
|
/** Model key of the open create/edit dialog — used to hide "+" on self-FK pickers
|
|
@@ -621,6 +622,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
|
|
|
621
622
|
if (isCreate && onCreate) {
|
|
622
623
|
const created = await onCreate(payload);
|
|
623
624
|
toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
|
|
625
|
+
emitRecordMutation(model, 'create');
|
|
624
626
|
onSaved?.(created ?? undefined);
|
|
625
627
|
onOpenChange(false);
|
|
626
628
|
return;
|
|
@@ -628,6 +630,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
|
|
|
628
630
|
if (!isCreate && recordId && onUpdate) {
|
|
629
631
|
const updated = await onUpdate(String(recordId), payload);
|
|
630
632
|
toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }));
|
|
633
|
+
emitRecordMutation(model, 'update');
|
|
631
634
|
onSaved?.(updated ?? undefined);
|
|
632
635
|
onOpenChange(false);
|
|
633
636
|
return;
|
|
@@ -652,6 +655,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
|
|
|
652
655
|
? t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' })
|
|
653
656
|
: t('dynamic.update_success', { defaultValue: 'Guardado correctamente' })));
|
|
654
657
|
// Hand the persisted record back so callers can auto-select it.
|
|
658
|
+
emitRecordMutation(model, isCreate ? 'create' : 'update');
|
|
655
659
|
onSaved?.(res.data?.data ?? res.data ?? undefined);
|
|
656
660
|
onOpenChange(false);
|
|
657
661
|
}
|
|
@@ -675,6 +679,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
|
|
|
675
679
|
setDeleting(true);
|
|
676
680
|
try {
|
|
677
681
|
await onDelete();
|
|
682
|
+
emitRecordMutation(model, 'delete');
|
|
678
683
|
onOpenChange(false);
|
|
679
684
|
}
|
|
680
685
|
catch (err) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type DocumentLinesGridProps } from './business/line-items-editor';
|
|
2
|
+
import type { DocumentFormsManifest, TableMetadata } from './types';
|
|
3
|
+
export interface DocumentFormDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
/** Modelo en el que se da de alta (p. ej. `fiscal_documents`). */
|
|
7
|
+
model: string;
|
|
8
|
+
/** Endpoint de alta. Default `/dynamic/<model>`. */
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
forms: DocumentFormsManifest;
|
|
11
|
+
/** Tipo preseleccionado (se salta el selector). */
|
|
12
|
+
initialType?: string;
|
|
13
|
+
/** Se llama con el registro creado. */
|
|
14
|
+
onSaved?: (record?: unknown) => void;
|
|
15
|
+
/** Buscador de productos para el editor de renglones (mismo contrato que ProductPicker). */
|
|
16
|
+
searchProducts?: DocumentLinesGridProps['search'];
|
|
17
|
+
/** Moneda ISO para el editor de renglones (default: la de la org). */
|
|
18
|
+
currency?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Manifest efectivo: la prop gana sobre el metadata; sin tipos devuelve undefined. */
|
|
21
|
+
export declare function resolveDocumentForms(metadata?: Pick<TableMetadata, 'document_forms'> | null, override?: DocumentFormsManifest | null): DocumentFormsManifest | undefined;
|
|
22
|
+
export declare function DocumentFormDialog({ open, onOpenChange, model, endpoint, forms, initialType, onSaved, searchProducts, currency, }: DocumentFormDialogProps): import("react").JSX.Element;
|
|
23
|
+
//# sourceMappingURL=document-form-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-form-dialog.d.ts","sourceRoot":"","sources":["../src/document-form-dialog.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAEH,KAAK,sBAAsB,EAE9B,MAAM,8BAA8B,CAAA;AAYrC,OAAO,KAAK,EAGR,qBAAqB,EAErB,aAAa,EAChB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,uBAAuB;IACpC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uCAAuC;IACvC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC,4FAA4F;IAC5F,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAChC,QAAQ,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,IAAI,EACvD,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,GACxC,qBAAqB,GAAG,SAAS,CAGnC;AAUD,wBAAgB,kBAAkB,CAAC,EAC/B,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,WAAW,EACX,OAAO,EACP,cAAc,EACd,QAAQ,GACX,EAAE,uBAAuB,+BAmQzB"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DocumentFormDialog — alta guiada por tipo de documento (FAC-12, #1022).
|
|
3
|
+
//
|
|
4
|
+
// "Crear documento fiscal" era el mismo formulario genérico para factura, nota
|
|
5
|
+
// de crédito, REP, global y traslado: los campos los dictaba el modelo, no el
|
|
6
|
+
// tipo. Aquí el metadata (`document_forms`) o una prop declara los tipos; el
|
|
7
|
+
// diálogo muestra tarjetas de tipo y, al elegir uno, SUS campos y (si lo declara)
|
|
8
|
+
// el paso de renglones con el editor de renglones del SDK. Un solo tipo se salta
|
|
9
|
+
// el selector; sin manifest el alta genérica sigue igual (DynamicCRUDPage decide).
|
|
10
|
+
//
|
|
11
|
+
// El SDK no hardcodea catálogos SAT: campos, valores fijos y el valor del tipo
|
|
12
|
+
// vienen del manifest que sirve el kernel o el addon.
|
|
13
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
15
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@asteby/metacore-ui/primitives';
|
|
16
|
+
import { Loader2 } from 'lucide-react';
|
|
17
|
+
import { toast } from 'sonner';
|
|
18
|
+
import { useApi } from './api-context';
|
|
19
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
20
|
+
import { FieldCell, FieldGrid, FieldLabel } from './field-grid';
|
|
21
|
+
import { FormErrorBanner } from './business/feedback';
|
|
22
|
+
import { DocumentLinesGrid, } from './business/line-items-editor';
|
|
23
|
+
import { serializeLineItems, validateLineItems } from './business/line-items';
|
|
24
|
+
import { actionErrorBannerMessage, buildFieldDefaults, renderField, } from './action-modal-dispatcher';
|
|
25
|
+
import { extractFieldErrors, localizeFieldErrorMap } from './server-error';
|
|
26
|
+
import { validateValues, bagHasErrors } from './validator';
|
|
27
|
+
import { clearFieldErrorTree } from './field-validation-ui';
|
|
28
|
+
import { emitRecordMutation } from './record-mutation-events';
|
|
29
|
+
import { isLineItemsField, resolveWidget } from './dynamic-form-schema';
|
|
30
|
+
/** Manifest efectivo: la prop gana sobre el metadata; sin tipos devuelve undefined. */
|
|
31
|
+
export function resolveDocumentForms(metadata, override) {
|
|
32
|
+
const m = override ?? metadata?.document_forms;
|
|
33
|
+
return m && Array.isArray(m.types) && m.types.length > 0 ? m : undefined;
|
|
34
|
+
}
|
|
35
|
+
function linesConfig(type, forms) {
|
|
36
|
+
if (!type.lines)
|
|
37
|
+
return undefined;
|
|
38
|
+
const cfg = type.lines === true ? {} : type.lines;
|
|
39
|
+
return { ...cfg, field: cfg.field ?? forms.lines_field ?? 'lines' };
|
|
40
|
+
}
|
|
41
|
+
export function DocumentFormDialog({ open, onOpenChange, model, endpoint, forms, initialType, onSaved, searchProducts, currency, }) {
|
|
42
|
+
const { t, i18n } = useTranslation();
|
|
43
|
+
const api = useApi();
|
|
44
|
+
const types = forms.types;
|
|
45
|
+
const single = types.length === 1 ? types[0] : undefined;
|
|
46
|
+
const [typeKey, setTypeKey] = useState(initialType ?? single?.key ?? null);
|
|
47
|
+
const [step, setStep] = useState(initialType || single ? 'fields' : 'type');
|
|
48
|
+
const [formData, setFormData] = useState({});
|
|
49
|
+
const [lines, setLines] = useState([]);
|
|
50
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
51
|
+
const [serverLineErrors, setServerLineErrors] = useState({});
|
|
52
|
+
const [formError, setFormError] = useState();
|
|
53
|
+
const [saving, setSaving] = useState(false);
|
|
54
|
+
const type = useMemo(() => types.find((x) => x.key === typeKey) ?? null, [types, typeKey]);
|
|
55
|
+
const lineCfg = type ? linesConfig(type, forms) : undefined;
|
|
56
|
+
// Reset every time the dialog (re)opens.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!open)
|
|
59
|
+
return;
|
|
60
|
+
const start = initialType ?? single?.key ?? null;
|
|
61
|
+
setTypeKey(start);
|
|
62
|
+
setStep(start ? 'fields' : 'type');
|
|
63
|
+
setLines([]);
|
|
64
|
+
setFieldErrors({});
|
|
65
|
+
setServerLineErrors({});
|
|
66
|
+
setFormError(undefined);
|
|
67
|
+
}, [open, initialType, single?.key]);
|
|
68
|
+
// Seed field defaults for the chosen type.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!type)
|
|
71
|
+
return;
|
|
72
|
+
setFormData({ ...buildFieldDefaults(type.fields, undefined), ...(type.defaults ?? {}) });
|
|
73
|
+
setFieldErrors({});
|
|
74
|
+
setFormError(undefined);
|
|
75
|
+
}, [type]);
|
|
76
|
+
const updateField = (key, value) => {
|
|
77
|
+
setFormData((prev) => ({ ...prev, [key]: value }));
|
|
78
|
+
setFieldErrors((prev) => clearFieldErrorTree(prev, key));
|
|
79
|
+
};
|
|
80
|
+
const tl = (s) => t(s, { defaultValue: s });
|
|
81
|
+
const lang = i18n?.language;
|
|
82
|
+
const chooseType = (k) => {
|
|
83
|
+
setTypeKey(k);
|
|
84
|
+
setStep('fields');
|
|
85
|
+
};
|
|
86
|
+
/** Validate the header fields; returns true when they pass. */
|
|
87
|
+
const validateHeader = () => {
|
|
88
|
+
if (!type)
|
|
89
|
+
return false;
|
|
90
|
+
const bag = validateValues(type.fields, formData);
|
|
91
|
+
if (!bagHasErrors(bag)) {
|
|
92
|
+
setFieldErrors({});
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const labels = {};
|
|
96
|
+
for (const f of type.fields)
|
|
97
|
+
labels[f.key] = tl(f.label);
|
|
98
|
+
setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
const goNext = () => {
|
|
102
|
+
if (validateHeader())
|
|
103
|
+
setStep('lines');
|
|
104
|
+
};
|
|
105
|
+
const submit = async () => {
|
|
106
|
+
if (!type)
|
|
107
|
+
return;
|
|
108
|
+
if (!validateHeader()) {
|
|
109
|
+
setStep('fields');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (lineCfg && (lineCfg.required ?? true)) {
|
|
113
|
+
const v = validateLineItems(lines, { requireItems: true });
|
|
114
|
+
if (!v.form && Object.keys(v.errors).length === 0) {
|
|
115
|
+
/* ok */
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
setServerLineErrors(v.errors);
|
|
119
|
+
setFormError(v.form ?? t('documentForm.lines_invalid', { defaultValue: 'Revisa los renglones del documento.' }));
|
|
120
|
+
setStep('lines');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
setSaving(true);
|
|
125
|
+
setFormError(undefined);
|
|
126
|
+
try {
|
|
127
|
+
const payload = { ...(type.defaults ?? {}), ...formData };
|
|
128
|
+
if (forms.type_field)
|
|
129
|
+
payload[forms.type_field] = type.value ?? type.key;
|
|
130
|
+
if (lineCfg)
|
|
131
|
+
payload[lineCfg.field] = serializeLineItems(lines);
|
|
132
|
+
const url = type.endpoint ?? endpoint ?? `/dynamic/${model}`;
|
|
133
|
+
const res = await api.post(url, payload);
|
|
134
|
+
if (res.data?.success === false) {
|
|
135
|
+
handleError({ response: { data: res.data } });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
toast.success(t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
|
|
139
|
+
emitRecordMutation(model, 'create');
|
|
140
|
+
onSaved?.(res.data?.data ?? res.data ?? undefined);
|
|
141
|
+
onOpenChange(false);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
handleError(err);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
setSaving(false);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/** 422 → marks the fields and shows the banner INSIDE the dialog; stays open. */
|
|
151
|
+
const handleError = (err) => {
|
|
152
|
+
setFormError(actionErrorBannerMessage(err, type?.fields, t, lang));
|
|
153
|
+
const map = extractFieldErrors(err);
|
|
154
|
+
if (map) {
|
|
155
|
+
const labels = {};
|
|
156
|
+
for (const f of type?.fields ?? [])
|
|
157
|
+
labels[f.key] = tl(f.label);
|
|
158
|
+
const localized = localizeFieldErrorMap(map, t, { labels, language: lang });
|
|
159
|
+
const header = {};
|
|
160
|
+
const lineErrs = {};
|
|
161
|
+
const prefix = lineCfg ? `${lineCfg.field}.` : null;
|
|
162
|
+
for (const [k, v] of Object.entries(localized)) {
|
|
163
|
+
if (prefix && k.startsWith(prefix))
|
|
164
|
+
lineErrs[k.slice(prefix.length)] = v;
|
|
165
|
+
else
|
|
166
|
+
header[k] = v;
|
|
167
|
+
}
|
|
168
|
+
setFieldErrors(header);
|
|
169
|
+
setServerLineErrors(lineErrs);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const canGoBackToTypes = types.length > 1 && !initialType;
|
|
173
|
+
const stepLabel = (s) => s === 'type'
|
|
174
|
+
? t('documentForm.step_type', { defaultValue: 'Tipo de documento' })
|
|
175
|
+
: s === 'fields'
|
|
176
|
+
? t('documentForm.step_data', { defaultValue: 'Datos' })
|
|
177
|
+
: tl(lineCfg?.title ?? 'Renglones');
|
|
178
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "flex max-h-[90dvh] flex-col overflow-hidden sm:max-w-2xl", style: { maxHeight: '90dvh', ...(step === 'lines' ? { maxWidth: '980px', width: '95vw' } : {}) }, "data-slot": "document-form-dialog", children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsx(DialogTitle, { children: type ? tl(type.label) : t('documentForm.title', { defaultValue: 'Nuevo documento' }) }), _jsx(DialogDescription, { children: stepLabel(step) })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 space-y-3 overflow-y-auto px-1 py-3", children: [_jsx(FormErrorBanner, { message: formError }), step === 'type' && (_jsx("div", { role: "radiogroup", className: "grid gap-3 sm:grid-cols-2", children: types.map((x) => (_jsxs("button", { type: "button", role: "radio", "aria-checked": x.key === typeKey, "data-type-card": x.key, onClick: () => chooseType(x.key), className: "flex items-start gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [x.icon && _jsx(DynamicIcon, { name: x.icon, className: "mt-0.5 size-5 shrink-0 text-primary" }), _jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: tl(x.label) }), x.description && (_jsx("span", { className: "block text-xs text-muted-foreground", children: tl(x.description) }))] })] }, x.key))) })), step === 'fields' && type && (_jsx(FieldGrid, { children: type.fields.map((field) => {
|
|
179
|
+
const fullWidth = isLineItemsField(field) ||
|
|
180
|
+
resolveWidget(field) === 'textarea' ||
|
|
181
|
+
resolveWidget(field) === 'richtext';
|
|
182
|
+
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, undefined, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "mt-1 text-xs text-destructive", children: fieldErrors[field.key] }))] }, field.key));
|
|
183
|
+
}) })), step === 'lines' && type && lineCfg && (_jsx(DocumentLinesGrid, { value: lines, onChange: (next) => {
|
|
184
|
+
setLines(next);
|
|
185
|
+
setServerLineErrors({});
|
|
186
|
+
}, columns: lineCfg.columns ?? ['discount', 'tax'], priceSource: lineCfg.price_source ?? 'sale', serverErrors: serverLineErrors, search: searchProducts, currency: currency }))] }), _jsxs(DialogFooter, { className: "shrink-0", children: [step === 'type' ? (_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t('common.cancel', { defaultValue: 'Cancelar' }) })) : (_jsx(Button, { type: "button", variant: "outline", disabled: saving, onClick: () => {
|
|
187
|
+
if (step === 'lines')
|
|
188
|
+
setStep('fields');
|
|
189
|
+
else if (canGoBackToTypes)
|
|
190
|
+
setStep('type');
|
|
191
|
+
else
|
|
192
|
+
onOpenChange(false);
|
|
193
|
+
}, children: step === 'fields' && !canGoBackToTypes
|
|
194
|
+
? t('common.cancel', { defaultValue: 'Cancelar' })
|
|
195
|
+
: t('common.back', { defaultValue: 'Atrás' }) })), step === 'fields' && type && lineCfg && (_jsx(Button, { type: "button", onClick: goNext, children: t('common.next', { defaultValue: 'Siguiente' }) })), ((step === 'fields' && type && !lineCfg) || step === 'lines') && (_jsxs(Button, { type: "button", onClick: submit, disabled: saving, children: [saving && _jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }), type?.submit_label ? tl(type.submit_label) : t('common.create', { defaultValue: 'Crear' })] }))] })] }) }));
|
|
196
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { DocumentFormsManifest } from './types';
|
|
2
3
|
export interface DynamicCRUDPageStrings {
|
|
3
4
|
refresh?: string;
|
|
4
5
|
export?: string;
|
|
@@ -39,6 +40,11 @@ export interface DynamicCRUDPageProps {
|
|
|
39
40
|
classes?: DynamicCRUDPageClasses;
|
|
40
41
|
/** Fired after a create/import/refresh successfully reloads the table. */
|
|
41
42
|
onChange?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Guided create by document type (FAC-12). Wins over `metadata.document_forms`;
|
|
45
|
+
* with neither, the generic record dialog is used as before.
|
|
46
|
+
*/
|
|
47
|
+
documentForms?: DocumentFormsManifest;
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
50
|
* Page-level CRUD shell wired around <DynamicTable>. Hosts mount a single
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAad,OAAO,KAAK,EAAE,qBAAqB,EAAiB,MAAM,SAAS,CAAA;AAEnE,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,qBAAqB,CAAA;CACxC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,qBA6N1D"}
|
|
@@ -29,6 +29,7 @@ import { useApi } from './api-context';
|
|
|
29
29
|
import { useMetadataCache } from './metadata-cache';
|
|
30
30
|
import { DynamicTable } from './dynamic-table';
|
|
31
31
|
import { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
32
|
+
import { DocumentFormDialog, resolveDocumentForms } from './document-form-dialog';
|
|
32
33
|
import { ExportDialog } from './dialogs/export';
|
|
33
34
|
import { ImportDialog } from './dialogs/import';
|
|
34
35
|
import { getModelExtension } from './model-extension-registry';
|
|
@@ -47,7 +48,7 @@ const defaultStrings = {
|
|
|
47
48
|
* metadata fetch, dialogs and toolbar.
|
|
48
49
|
*/
|
|
49
50
|
export function DynamicCRUDPage(props) {
|
|
50
|
-
const { model, endpoint, title: titleOverride, newLabel, i18n, hideCreate, hideExport, hideImport, hideRefresh, headerExtras, toolbarExtras, classes, onChange, } = props;
|
|
51
|
+
const { model, endpoint, title: titleOverride, newLabel, i18n, hideCreate, hideExport, hideImport, hideRefresh, headerExtras, toolbarExtras, classes, onChange, documentForms, } = props;
|
|
51
52
|
const strings = { ...defaultStrings, ...(i18n ?? {}) };
|
|
52
53
|
const dataEndpoint = endpoint ?? `/dynamic/${model}`;
|
|
53
54
|
const ext = getModelExtension(model);
|
|
@@ -117,6 +118,7 @@ export function DynamicCRUDPage(props) {
|
|
|
117
118
|
const showImport = enableCRUD && !effectiveHideImport && can(modelCapability(model, 'import'));
|
|
118
119
|
const showExport = !effectiveHideExport && can(modelCapability(model, 'export'));
|
|
119
120
|
const showRefresh = !effectiveHideRefresh;
|
|
121
|
+
const guidedForms = resolveDocumentForms(metadata, documentForms);
|
|
120
122
|
const handleRefresh = useCallback(() => {
|
|
121
123
|
setRefreshKey((k) => k + 1);
|
|
122
124
|
onChange?.();
|
|
@@ -127,5 +129,5 @@ export function DynamicCRUDPage(props) {
|
|
|
127
129
|
const titleCls = classes?.title ?? 'text-2xl font-bold tracking-tight';
|
|
128
130
|
const toolbarCls = classes?.toolbar ?? 'flex items-center gap-2';
|
|
129
131
|
const tableWrapperCls = classes?.tableWrapper ?? 'flex-1 min-h-0';
|
|
130
|
-
return (_jsxs("div", { className: rootCls, children: [_jsxs("div", { className: containerCls, children: [ext?.headerExtras && _jsx(ext.headerExtras, { model: model, onRefresh: handleRefresh }), headerExtras, _jsxs("div", { className: headerCls, children: [metadata ? (_jsx("h1", { className: titleCls, children: title })) : (_jsx("div", { className: 'h-8 w-48 bg-muted rounded animate-pulse' })), _jsxs("div", { className: toolbarCls, children: [showRefresh && (_jsx("button", { type: 'button', onClick: handleRefresh, "aria-label": strings.refresh, className: 'inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground', children: _jsx(RefreshCw, { className: 'size-4' }) })), metadata && showExport && (_jsxs("button", { type: 'button', onClick: () => setOpenExport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Download, { className: 'size-4' }), strings.export] })), metadata && showImport && (_jsxs("button", { type: 'button', onClick: () => setOpenImport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Upload, { className: 'size-4' }), strings.import] })), ext?.toolbarExtras && _jsx(ext.toolbarExtras, { model: model, onRefresh: handleRefresh }), toolbarExtras, _jsx(ModelActionToolbar, { model: model, endpoint: dataEndpoint, actions: metadata?.actions, primaryActionKey: ext?.primaryActionKey, onChange: handleRefresh }), showCreate && (_jsxs("button", { type: 'button', "data-primary": 'true', onClick: () => setOpenCreate(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium', children: [_jsx(Plus, { className: 'size-4' }), resolvedNewLabel ?? `${strings.newPrefix} ${singular}`] }))] })] }), _jsx("div", { className: tableWrapperCls, children: _jsx(DynamicTable, { model: model, endpoint: dataEndpoint, refreshTrigger: refreshKey }, model) })] }), showCreate && (_jsx(DynamicRecordDialog, { open: openCreate, onOpenChange: setOpenCreate, mode: 'create', model: model, endpoint: dataEndpoint, onSaved: handleRefresh })), metadata && showExport && (_jsx(ExportDialog, { open: openExport, onOpenChange: setOpenExport, model: model, metadata: metadata })), metadata && showImport && (_jsx(ImportDialog, { open: openImport, onOpenChange: setOpenImport, model: model, metadata: metadata, onImported: handleRefresh }))] }));
|
|
132
|
+
return (_jsxs("div", { className: rootCls, children: [_jsxs("div", { className: containerCls, children: [ext?.headerExtras && _jsx(ext.headerExtras, { model: model, onRefresh: handleRefresh }), headerExtras, _jsxs("div", { className: headerCls, children: [metadata ? (_jsx("h1", { className: titleCls, children: title })) : (_jsx("div", { className: 'h-8 w-48 bg-muted rounded animate-pulse' })), _jsxs("div", { className: toolbarCls, children: [showRefresh && (_jsx("button", { type: 'button', onClick: handleRefresh, "aria-label": strings.refresh, className: 'inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground', children: _jsx(RefreshCw, { className: 'size-4' }) })), metadata && showExport && (_jsxs("button", { type: 'button', onClick: () => setOpenExport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Download, { className: 'size-4' }), strings.export] })), metadata && showImport && (_jsxs("button", { type: 'button', onClick: () => setOpenImport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Upload, { className: 'size-4' }), strings.import] })), ext?.toolbarExtras && _jsx(ext.toolbarExtras, { model: model, onRefresh: handleRefresh }), toolbarExtras, _jsx(ModelActionToolbar, { model: model, endpoint: dataEndpoint, actions: metadata?.actions, primaryActionKey: ext?.primaryActionKey, onChange: handleRefresh }), showCreate && (_jsxs("button", { type: 'button', "data-primary": 'true', onClick: () => setOpenCreate(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium', children: [_jsx(Plus, { className: 'size-4' }), resolvedNewLabel ?? `${strings.newPrefix} ${singular}`] }))] })] }), _jsx("div", { className: tableWrapperCls, children: _jsx(DynamicTable, { model: model, endpoint: dataEndpoint, refreshTrigger: refreshKey }, model) })] }), showCreate && guidedForms && (_jsx(DocumentFormDialog, { open: openCreate, onOpenChange: setOpenCreate, model: model, endpoint: dataEndpoint, forms: guidedForms, onSaved: handleRefresh })), showCreate && !guidedForms && (_jsx(DynamicRecordDialog, { open: openCreate, onOpenChange: setOpenCreate, mode: 'create', model: model, endpoint: dataEndpoint, onSaved: handleRefresh })), metadata && showExport && (_jsx(ExportDialog, { open: openExport, onOpenChange: setOpenExport, model: model, metadata: metadata })), metadata && showImport && (_jsx(ImportDialog, { open: openImport, onOpenChange: setOpenImport, model: model, metadata: metadata, onImported: handleRefresh }))] }));
|
|
131
133
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqH9B,OAAO,EAEH,qBAAqB,EACrB,qBAAqB,EACxB,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
|
package/dist/dynamic-kanban.js
CHANGED
|
@@ -26,6 +26,7 @@ import { useFlipAnimation } from './use-flip-animation';
|
|
|
26
26
|
import { PortalDragOverlay } from './portal-drag-overlay';
|
|
27
27
|
import { objectLabel } from './dynamic-relation-helpers';
|
|
28
28
|
import { useMetadataCache } from './metadata-cache';
|
|
29
|
+
import { useRecordMutationTick } from './use-record-mutation-tick';
|
|
29
30
|
import { useOptionsResolver } from './use-options-resolver';
|
|
30
31
|
import { ActivityValueRenderer } from './activity-value-renderer';
|
|
31
32
|
import { isColumnVisibleInTable } from './column-visibility';
|
|
@@ -582,6 +583,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
|
|
|
582
583
|
// Realtime refetch (opt-in) — debounced counter bumped by DATA_EVENTs for
|
|
583
584
|
// this model; folded into the board refetch effect next to refreshTrigger.
|
|
584
585
|
const realtimeDefault = useRealtimeDefault();
|
|
586
|
+
const mutationTick = useRecordMutationTick(model);
|
|
585
587
|
const realtimeTick = useRealtimeTick({
|
|
586
588
|
models: [model],
|
|
587
589
|
enabled: realtimeProp ?? realtimeDefault,
|
|
@@ -953,7 +955,7 @@ function DynamicKanbanBoard({ model, endpoint, refreshTrigger, realtime: realtim
|
|
|
953
955
|
return () => clearTimeout(handle);
|
|
954
956
|
// realtimeTick: data events for this model (see the `realtime` prop).
|
|
955
957
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
956
|
-
}, [fetchData, metadata, refreshTrigger, realtimeTick]);
|
|
958
|
+
}, [fetchData, metadata, refreshTrigger, realtimeTick, mutationTick]);
|
|
957
959
|
// Filterable fields for the toolbar, in metadata order (explicit filters
|
|
958
960
|
// first, then filterable columns), each labeled from its metadata source.
|
|
959
961
|
const filterFields = useMemo(() => {
|