@asteby/metacore-runtime-react 45.0.0 → 45.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/approval-events.d.ts +16 -0
- package/dist/approval-events.d.ts.map +1 -0
- package/dist/approval-events.js +27 -0
- package/dist/approval-gate.d.ts +39 -0
- package/dist/approval-gate.d.ts.map +1 -0
- package/dist/approval-gate.js +110 -0
- package/dist/approval-pin-dialog.d.ts +25 -0
- package/dist/approval-pin-dialog.d.ts.map +1 -0
- package/dist/approval-pin-dialog.js +66 -0
- package/dist/business/catalog.d.ts +16 -0
- package/dist/business/catalog.d.ts.map +1 -0
- package/dist/business/catalog.js +42 -0
- package/dist/business/customer-picker.d.ts +39 -0
- package/dist/business/customer-picker.d.ts.map +1 -0
- package/dist/business/customer-picker.js +51 -0
- package/dist/business/feedback.d.ts +72 -0
- package/dist/business/feedback.d.ts.map +1 -0
- package/dist/business/feedback.js +52 -0
- package/dist/business/field-errors.d.ts +26 -0
- package/dist/business/field-errors.d.ts.map +1 -0
- package/dist/business/field-errors.js +59 -0
- package/dist/business/format.d.ts +49 -0
- package/dist/business/format.d.ts.map +1 -0
- package/dist/business/format.js +128 -0
- package/dist/business/index.d.ts +14 -0
- package/dist/business/index.d.ts.map +1 -0
- package/dist/business/index.js +13 -0
- package/dist/business/line-items-editor.d.ts +25 -0
- package/dist/business/line-items-editor.d.ts.map +1 -0
- package/dist/business/line-items-editor.js +36 -0
- package/dist/business/line-items.d.ts +69 -0
- package/dist/business/line-items.d.ts.map +1 -0
- package/dist/business/line-items.js +133 -0
- package/dist/business/payment-capture.d.ts +21 -0
- package/dist/business/payment-capture.d.ts.map +1 -0
- package/dist/business/payment-capture.js +24 -0
- package/dist/business/payment.d.ts +61 -0
- package/dist/business/payment.d.ts.map +1 -0
- package/dist/business/payment.js +74 -0
- package/dist/business/product-picker.d.ts +24 -0
- package/dist/business/product-picker.d.ts.map +1 -0
- package/dist/business/product-picker.js +48 -0
- package/dist/business/product-search.d.ts +70 -0
- package/dist/business/product-search.d.ts.map +1 -0
- package/dist/business/product-search.js +51 -0
- package/dist/business/refund-destination.d.ts +19 -0
- package/dist/business/refund-destination.d.ts.map +1 -0
- package/dist/business/refund-destination.js +31 -0
- package/dist/business/refund.d.ts +29 -0
- package/dist/business/refund.d.ts.map +1 -0
- package/dist/business/refund.js +43 -0
- package/dist/business/use-async-search.d.ts +10 -0
- package/dist/business/use-async-search.d.ts.map +1 -0
- package/dist/business/use-async-search.js +38 -0
- package/dist/document/action-bar.d.ts +12 -0
- package/dist/document/action-bar.d.ts.map +1 -0
- package/dist/document/action-bar.js +39 -0
- package/dist/document/cancel-with-reason.d.ts +47 -0
- package/dist/document/cancel-with-reason.d.ts.map +1 -0
- package/dist/document/cancel-with-reason.js +52 -0
- package/dist/document/document-header.d.ts +41 -0
- package/dist/document/document-header.d.ts.map +1 -0
- package/dist/document/document-header.js +30 -0
- package/dist/document/document-page.d.ts +37 -0
- package/dist/document/document-page.d.ts.map +1 -0
- package/dist/document/document-page.js +137 -0
- package/dist/document/document-timeline.d.ts +20 -0
- package/dist/document/document-timeline.d.ts.map +1 -0
- package/dist/document/document-timeline.js +26 -0
- package/dist/document/evaluate.d.ts +31 -0
- package/dist/document/evaluate.d.ts.map +1 -0
- package/dist/document/evaluate.js +170 -0
- package/dist/document/format.d.ts +9 -0
- package/dist/document/format.d.ts.map +1 -0
- package/dist/document/format.js +52 -0
- package/dist/document/index.d.ts +13 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +12 -0
- package/dist/document/registry.d.ts +20 -0
- package/dist/document/registry.d.ts.map +1 -0
- package/dist/document/registry.js +20 -0
- package/dist/document/smart-buttons.d.ts +16 -0
- package/dist/document/smart-buttons.d.ts.map +1 -0
- package/dist/document/smart-buttons.js +16 -0
- package/dist/document/status-badge.d.ts +16 -0
- package/dist/document/status-badge.d.ts.map +1 -0
- package/dist/document/status-badge.js +23 -0
- package/dist/document/types.d.ts +196 -0
- package/dist/document/types.d.ts.map +1 -0
- package/dist/document/types.js +1 -0
- package/dist/document/use-document-sources.d.ts +14 -0
- package/dist/document/use-document-sources.d.ts.map +1 -0
- package/dist/document/use-document-sources.js +72 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/server-error.d.ts.map +1 -1
- package/dist/server-error.js +19 -0
- package/package.json +1 -1
- package/src/__tests__/approval-gate.test.tsx +134 -0
- package/src/approval-events.ts +39 -0
- package/src/approval-gate.tsx +180 -0
- package/src/approval-pin-dialog.tsx +162 -0
- package/src/business/business.test.ts +164 -0
- package/src/business/catalog.ts +60 -0
- package/src/business/customer-picker.tsx +201 -0
- package/src/business/feedback.tsx +141 -0
- package/src/business/field-errors.ts +92 -0
- package/src/business/format.ts +151 -0
- package/src/business/index.ts +13 -0
- package/src/business/line-items-editor.tsx +259 -0
- package/src/business/line-items.ts +190 -0
- package/src/business/payment-capture.tsx +158 -0
- package/src/business/payment.ts +125 -0
- package/src/business/product-picker.tsx +171 -0
- package/src/business/product-search.ts +108 -0
- package/src/business/refund-destination.tsx +121 -0
- package/src/business/refund.ts +71 -0
- package/src/business/use-async-search.ts +43 -0
- package/src/document/__tests__/document-page.test.tsx +107 -0
- package/src/document/__tests__/evaluate.test.ts +140 -0
- package/src/document/action-bar.tsx +140 -0
- package/src/document/cancel-with-reason.tsx +214 -0
- package/src/document/document-header.tsx +140 -0
- package/src/document/document-page.tsx +268 -0
- package/src/document/document-timeline.tsx +66 -0
- package/src/document/evaluate.ts +180 -0
- package/src/document/format.ts +59 -0
- package/src/document/index.ts +28 -0
- package/src/document/registry.ts +39 -0
- package/src/document/smart-buttons.tsx +54 -0
- package/src/document/status-badge.tsx +44 -0
- package/src/document/types.ts +203 -0
- package/src/document/use-document-sources.ts +91 -0
- package/src/index.ts +23 -0
- package/src/server-error.ts +20 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// ActionBar — barra de acciones de un documento (benchmark §5.4 / §6.2):
|
|
3
|
+
// una primaria (verbo + sustantivo), 2–3 secundarias visibles, el resto en
|
|
4
|
+
// «Más…» y las destructivas separadas (divisor) y en rojo. Una acción bloqueada se ve
|
|
5
|
+
// deshabilitada con su motivo (no desaparece: el operador entiende por qué).
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Loader2, MoreHorizontal } from 'lucide-react';
|
|
9
|
+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@asteby/metacore-ui/primitives';
|
|
10
|
+
import { cn } from '@asteby/metacore-ui/lib';
|
|
11
|
+
import { DynamicIcon } from '../dynamic-icon';
|
|
12
|
+
export function ActionBar({ layout, onAction, className }) {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [busy, setBusy] = useState(null);
|
|
15
|
+
const run = async (a) => {
|
|
16
|
+
if (a.blockedReason || busy)
|
|
17
|
+
return;
|
|
18
|
+
setBusy(a.def.key);
|
|
19
|
+
try {
|
|
20
|
+
await onAction(a);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
setBusy(null);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const label = (a) => t(a.def.label, { defaultValue: a.def.label });
|
|
27
|
+
const { primary, secondary, more, destructive } = layout;
|
|
28
|
+
const hasMenu = more.length > 0;
|
|
29
|
+
const button = (a, variant) => {
|
|
30
|
+
const loading = busy === a.def.key;
|
|
31
|
+
const btn = (_jsxs(Button, { type: 'button', size: 'sm', variant: variant === 'destructive' ? 'outline' : variant, disabled: !!a.blockedReason || (!!busy && !loading), "aria-disabled": !!a.blockedReason, "data-action": a.def.key, onClick: () => run(a), className: cn(variant === 'destructive' &&
|
|
32
|
+
'border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive'), children: [loading ? (_jsx(Loader2, { className: 'mr-1.5 h-4 w-4 animate-spin' })) : a.def.icon ? (_jsx("span", { className: 'mr-1.5 inline-flex h-4 w-4 items-center', children: _jsx(DynamicIcon, { name: a.def.icon, className: 'h-4 w-4' }) })) : null, label(a)] }, a.def.key));
|
|
33
|
+
if (!a.blockedReason)
|
|
34
|
+
return btn;
|
|
35
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { tabIndex: 0, children: btn }) }), _jsx(TooltipContent, { className: 'max-w-xs', children: t(a.blockedReason, { defaultValue: a.blockedReason }) })] }, a.def.key));
|
|
36
|
+
};
|
|
37
|
+
const item = (a) => (_jsxs(DropdownMenuItem, { disabled: !!a.blockedReason, "data-action": a.def.key, onSelect: () => run(a), children: [a.def.icon ? (_jsx("span", { className: 'mr-2 inline-flex h-4 w-4 items-center', children: _jsx(DynamicIcon, { name: a.def.icon, className: 'h-4 w-4' }) })) : null, _jsxs("span", { className: 'flex flex-col', children: [label(a), a.blockedReason ? (_jsx("span", { className: 'text-xs font-normal text-muted-foreground', children: t(a.blockedReason, { defaultValue: a.blockedReason }) })) : null] })] }, a.def.key));
|
|
38
|
+
return (_jsx(TooltipProvider, { delayDuration: 150, children: _jsxs("div", { className: cn('flex flex-wrap items-center gap-2', className), "data-document-actions": '', children: [primary ? button(primary, primary.def.destructive ? 'destructive' : 'default') : null, secondary.map((a) => button(a, 'outline')), hasMenu ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: 'button', size: 'sm', variant: 'outline', "aria-label": t('document.more', { defaultValue: 'Más…' }), children: [_jsx(MoreHorizontal, { className: 'mr-1.5 h-4 w-4' }), t('document.more', { defaultValue: 'Más…' })] }) }), _jsx(DropdownMenuContent, { align: 'end', className: 'min-w-56', children: more.map((a) => item(a)) })] })) : null, destructive.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: 'mx-1 h-6 w-px bg-border' }), destructive.map((a) => button(a, 'destructive'))] })) : null] }) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface CancelReason {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Ayuda corta bajo el motivo. */
|
|
6
|
+
hint?: string;
|
|
7
|
+
/** Exige apuntar a un documento sustituto (motivo 01 del SAT). */
|
|
8
|
+
requiresSubstitute?: boolean;
|
|
9
|
+
/** El motivo no aplica a este documento (se ve deshabilitado con `disabledReason`). */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
disabledReason?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CancelPayload {
|
|
14
|
+
reason: string;
|
|
15
|
+
substitute?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CancelWithReasonProps {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
/** Identificación del documento («Factura A-1042»). */
|
|
21
|
+
documentLabel: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
reasons: CancelReason[];
|
|
24
|
+
/** Consecuencias que se muestran en el paso de confirmación (§6.2). */
|
|
25
|
+
consequences: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Aviso según el motivo elegido, p. ej. «requiere aceptación del receptor».
|
|
28
|
+
* Devuelve `undefined` si no hay.
|
|
29
|
+
*/
|
|
30
|
+
notice?: (reason: CancelReason) => string | undefined;
|
|
31
|
+
/** Formato del identificador del sustituto; si se define, valida en línea. */
|
|
32
|
+
substitutePattern?: RegExp;
|
|
33
|
+
substituteLabel?: string;
|
|
34
|
+
substituteHelp?: string;
|
|
35
|
+
/** Zona para crear el sustituto desde el mismo asistente (el host la implementa). */
|
|
36
|
+
substituteSlot?: (props: {
|
|
37
|
+
value: string;
|
|
38
|
+
onChange: (v: string) => void;
|
|
39
|
+
}) => ReactNode;
|
|
40
|
+
/** Ejecuta la cancelación; debe lanzar con `message` si falla. */
|
|
41
|
+
onConfirm: (payload: CancelPayload) => Promise<unknown>;
|
|
42
|
+
/** Se llama tras un éxito, antes de cerrar. */
|
|
43
|
+
onDone?: () => void;
|
|
44
|
+
confirmLabel?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function CancelWithReason(props: CancelWithReasonProps): import("react").JSX.Element;
|
|
47
|
+
//# sourceMappingURL=cancel-with-reason.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancel-with-reason.d.ts","sourceRoot":"","sources":["../../src/document/cancel-with-reason.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAiB3D,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,uEAAuE;IACvE,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAA;IACrD,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qFAAqF;IACrF,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,KAAK,SAAS,CAAA;IACvF,kEAAkE;IAClE,SAAS,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACvD,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,+BAgJ5D"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// CancelWithReason — cancelación guiada de 2 pasos (benchmark §5.5 / §7): motivo
|
|
3
|
+
// del catálogo del tipo de documento → (documento sustituto si el motivo lo
|
|
4
|
+
// exige) → confirmación con consecuencias. Genérico: el catálogo de motivos, las
|
|
5
|
+
// consecuencias y la política de aceptación las declara el llamador; el
|
|
6
|
+
// componente no sabe de CFDI. Solo una acción primaria por paso (verbo +
|
|
7
|
+
// sustantivo) y, si la acción falla, el modal sigue abierto con el error.
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { AlertTriangle, Loader2 } from 'lucide-react';
|
|
11
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, RadioGroup, RadioGroupItem, } from '@asteby/metacore-ui/primitives';
|
|
12
|
+
export function CancelWithReason(props) {
|
|
13
|
+
const { open, onOpenChange, reasons, consequences, onConfirm, onDone } = props;
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const [step, setStep] = useState('reason');
|
|
16
|
+
const [reasonValue, setReasonValue] = useState('');
|
|
17
|
+
const [substitute, setSubstitute] = useState('');
|
|
18
|
+
const [busy, setBusy] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (open) {
|
|
22
|
+
setStep('reason');
|
|
23
|
+
setReasonValue('');
|
|
24
|
+
setSubstitute('');
|
|
25
|
+
setError(null);
|
|
26
|
+
setBusy(false);
|
|
27
|
+
}
|
|
28
|
+
}, [open]);
|
|
29
|
+
const reason = reasons.find((r) => r.value === reasonValue);
|
|
30
|
+
const substituteValid = !reason?.requiresSubstitute ||
|
|
31
|
+
(substitute.trim().length > 0 && (!props.substitutePattern || props.substitutePattern.test(substitute.trim())));
|
|
32
|
+
const notice = reason ? props.notice?.(reason) : undefined;
|
|
33
|
+
const tr = (s) => t(s, { defaultValue: s });
|
|
34
|
+
const confirm = async () => {
|
|
35
|
+
if (!reason)
|
|
36
|
+
return;
|
|
37
|
+
setBusy(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
try {
|
|
40
|
+
await onConfirm({ reason: reason.value, substitute: reason.requiresSubstitute ? substitute.trim() : undefined });
|
|
41
|
+
onDone?.();
|
|
42
|
+
onOpenChange(false);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
setError(e?.message || t('document.cancel.failed', { defaultValue: 'No se pudo cancelar. Revisa el detalle e intenta de nuevo.' }));
|
|
46
|
+
setBusy(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => (!busy ? onOpenChange(o) : undefined), children: _jsxs(DialogContent, { className: 'sm:max-w-lg', children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: props.title ?? t('document.cancel.title', { defaultValue: 'Solicitar cancelación' }) }), _jsxs(DialogDescription, { children: [props.documentLabel, " \u00B7", ' ', step === 'reason'
|
|
50
|
+
? t('document.cancel.step_reason', { defaultValue: 'Paso 1 de 2: elige el motivo' })
|
|
51
|
+
: t('document.cancel.step_confirm', { defaultValue: 'Paso 2 de 2: confirma' })] })] }), step === 'reason' ? (_jsxs("div", { className: 'flex flex-col gap-4', children: [_jsx(RadioGroup, { value: reasonValue, onValueChange: setReasonValue, className: 'gap-2', children: reasons.map((r) => (_jsxs("label", { className: 'flex cursor-pointer items-start gap-3 rounded-md border p-3 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 has-[[data-state=checked]]:border-primary', children: [_jsx(RadioGroupItem, { value: r.value, disabled: r.disabled, className: 'mt-0.5' }), _jsxs("span", { className: 'flex flex-col', children: [_jsx("span", { className: 'text-sm font-medium', children: tr(r.label) }), r.disabled && r.disabledReason ? (_jsx("span", { className: 'text-xs text-muted-foreground', children: tr(r.disabledReason) })) : r.hint ? (_jsx("span", { className: 'text-xs text-muted-foreground', children: tr(r.hint) })) : null] })] }, r.value))) }), reason?.requiresSubstitute ? (props.substituteSlot ? (props.substituteSlot({ value: substitute, onChange: setSubstitute })) : (_jsxs("div", { className: 'grid gap-2', children: [_jsx(Label, { children: props.substituteLabel ?? t('document.cancel.substitute', { defaultValue: 'Documento sustituto' }) }), _jsx(Input, { value: substitute, onChange: (e) => setSubstitute(e.target.value), className: 'font-mono text-sm' }), props.substituteHelp ? _jsx("p", { className: 'text-xs text-muted-foreground', children: tr(props.substituteHelp) }) : null, substitute && !substituteValid ? (_jsx("p", { className: 'text-xs text-destructive', children: t('document.cancel.substitute_invalid', { defaultValue: 'El formato del identificador no es válido.' }) })) : null] }))) : null, notice ? _jsx("p", { className: 'rounded-md bg-muted p-3 text-sm', children: tr(notice) }) : null] })) : (_jsxs("div", { className: 'flex flex-col gap-3', children: [_jsxs("div", { className: 'flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive', children: [_jsx(AlertTriangle, { className: 'mt-0.5 h-4 w-4 shrink-0' }), _jsx("ul", { className: 'list-disc pl-4', children: consequences.map((c) => (_jsx("li", { children: tr(c) }, c))) })] }), _jsxs("dl", { className: 'grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm', children: [_jsx("dt", { className: 'text-muted-foreground', children: t('document.cancel.reason', { defaultValue: 'Motivo' }) }), _jsx("dd", { className: 'font-medium', children: reason ? tr(reason.label) : '' }), reason?.requiresSubstitute ? (_jsxs(_Fragment, { children: [_jsx("dt", { className: 'text-muted-foreground', children: t('document.cancel.substitute_short', { defaultValue: 'Sustituto' }) }), _jsx("dd", { className: 'break-all font-mono text-xs', children: substitute })] })) : null] }), notice ? _jsx("p", { className: 'text-sm text-muted-foreground', children: tr(notice) }) : null, error ? (_jsx("p", { role: 'alert', className: 'rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive', children: error })) : null] })), _jsx(DialogFooter, { children: step === 'reason' ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: 'button', variant: 'outline', onClick: () => onOpenChange(false), children: t('document.cancel.keep', { defaultValue: 'Conservar documento' }) }), _jsx(Button, { type: 'button', disabled: !reason || !substituteValid, onClick: () => setStep('confirm'), children: t('document.cancel.continue', { defaultValue: 'Continuar' }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Button, { type: 'button', variant: 'outline', disabled: busy, onClick: () => setStep('reason'), children: t('document.cancel.back', { defaultValue: 'Volver al motivo' }) }), _jsxs(Button, { type: 'button', variant: 'destructive', disabled: busy, onClick: confirm, children: [busy ? _jsx(Loader2, { className: 'mr-1.5 h-4 w-4 animate-spin' }) : null, props.confirmLabel ?? t('document.cancel.confirm', { defaultValue: 'Solicitar cancelación' })] })] })) })] }) }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { formatFieldValue, type FieldFormatOptions } from './format';
|
|
3
|
+
import type { StatusDef } from './types';
|
|
4
|
+
export interface HeaderBadge {
|
|
5
|
+
id: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
status: StatusDef;
|
|
8
|
+
}
|
|
9
|
+
export interface HeaderMetric {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
tone?: 'default' | 'critical';
|
|
13
|
+
mono?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface DocumentHeaderProps {
|
|
16
|
+
title: string;
|
|
17
|
+
/** Texto bajo el título; si `subtitleHref` viene, es un enlace (ficha del cliente). */
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
subtitleHref?: string;
|
|
20
|
+
badges: HeaderBadge[];
|
|
21
|
+
metrics?: HeaderMetric[];
|
|
22
|
+
copyable?: {
|
|
23
|
+
label: string;
|
|
24
|
+
value: string;
|
|
25
|
+
};
|
|
26
|
+
/** Zona de acciones (normalmente `<ActionBar/>`). */
|
|
27
|
+
actions?: ReactNode;
|
|
28
|
+
/** Slot antes del título (botón «Volver»). */
|
|
29
|
+
leading?: ReactNode;
|
|
30
|
+
/** Renderiza el subtítulo como enlace del router del host. */
|
|
31
|
+
renderLink?: (props: {
|
|
32
|
+
href: string;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
}) => ReactNode;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function DocumentHeader({ title, subtitle, subtitleHref, badges, metrics, copyable, actions, leading, renderLink, className, }: DocumentHeaderProps): import("react").JSX.Element;
|
|
39
|
+
export { formatFieldValue };
|
|
40
|
+
export type { FieldFormatOptions };
|
|
41
|
+
//# sourceMappingURL=document-header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-header.d.ts","sourceRoot":"","sources":["../../src/document/document-header.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAMhD,OAAO,EAAE,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,CAAA;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,qDAAqD;IACrD,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAA;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,UAAU,EACV,SAAS,GACZ,EAAE,mBAAmB,+BAmFrB;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAA;AAC3B,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DocumentHeader — cabecera estándar (benchmark §5.2): serie/folio, badges de
|
|
3
|
+
// cada máquina de estado, cliente enlazable, métricas (total, saldo, fechas) y
|
|
4
|
+
// un identificador copiable (UUID).
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Check, Copy } from 'lucide-react';
|
|
8
|
+
import { Button } from '@asteby/metacore-ui/primitives';
|
|
9
|
+
import { cn } from '@asteby/metacore-ui/lib';
|
|
10
|
+
import { StatusBadge } from './status-badge';
|
|
11
|
+
import { formatFieldValue } from './format';
|
|
12
|
+
export function DocumentHeader({ title, subtitle, subtitleHref, badges, metrics, copyable, actions, leading, renderLink, className, }) {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [copied, setCopied] = useState(false);
|
|
15
|
+
const copy = async () => {
|
|
16
|
+
if (!copyable?.value)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
await navigator.clipboard.writeText(copyable.value);
|
|
20
|
+
setCopied(true);
|
|
21
|
+
setTimeout(() => setCopied(false), 1500);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* sin portapapeles (contexto inseguro): el valor sigue seleccionable */
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const subtitleNode = subtitle ? (subtitleHref ? (renderLink ? (renderLink({ href: subtitleHref, children: subtitle, className: 'text-sm text-primary hover:underline' })) : (_jsx("a", { href: subtitleHref, className: 'text-sm text-primary hover:underline', children: subtitle }))) : (_jsx("span", { className: 'text-sm text-muted-foreground', children: subtitle }))) : null;
|
|
28
|
+
return (_jsxs("header", { className: cn('flex flex-col gap-4 rounded-lg border bg-card p-5 text-card-foreground', className), "data-document-header": '', children: [leading, _jsxs("div", { className: 'flex flex-wrap items-start justify-between gap-4', children: [_jsxs("div", { className: 'flex min-w-0 flex-col gap-2', children: [_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [_jsx("h1", { className: 'text-2xl font-bold tracking-tight', children: title }), badges.map((b) => (_jsx(StatusBadge, { status: b.status, srLabel: b.label ? t(b.label, { defaultValue: b.label }) : undefined }, b.id)))] }), subtitleNode] }), actions] }), metrics && metrics.length > 0 ? (_jsx("dl", { className: 'grid grid-cols-2 gap-x-6 gap-y-3 sm:grid-cols-3 lg:grid-cols-5', children: metrics.map((m) => (_jsxs("div", { children: [_jsx("dt", { className: 'text-xs uppercase tracking-wide text-muted-foreground', children: m.label }), _jsx("dd", { className: cn('font-medium tabular-nums', m.mono && 'font-mono text-xs', m.tone === 'critical' && 'text-destructive'), children: m.value })] }, m.label))) })) : null, copyable?.value ? (_jsxs("div", { className: 'flex flex-wrap items-center gap-2 text-xs text-muted-foreground', children: [_jsx("span", { className: 'uppercase tracking-wide', children: copyable.label }), _jsx("code", { className: 'break-all font-mono text-foreground', children: copyable.value }), _jsxs(Button, { type: 'button', size: 'sm', variant: 'ghost', className: 'h-6 px-1.5', onClick: copy, "aria-label": t('document.copy', { defaultValue: 'Copiar' }), children: [copied ? _jsx(Check, { className: 'h-3.5 w-3.5' }) : _jsx(Copy, { className: 'h-3.5 w-3.5' }), _jsx("span", { className: 'ml-1', children: copied ? t('document.copied', { defaultValue: 'Copiado' }) : t('document.copy', { defaultValue: 'Copiar' }) })] })] })) : null] }));
|
|
29
|
+
}
|
|
30
|
+
export { formatFieldValue };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type FieldFormatOptions } from './format';
|
|
3
|
+
import { type DocumentPageRegistration } from './registry';
|
|
4
|
+
import type { DocumentContext, DocumentSlotRenderer, ResolvedAction } from './types';
|
|
5
|
+
export interface DocumentPageProps extends FieldFormatOptions {
|
|
6
|
+
registration: DocumentPageRegistration;
|
|
7
|
+
record: Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Ejecuta una acción. El host decide: abrir el modal de la acción del
|
|
10
|
+
* modelo (`def.modelAction ?? def.key`), navegar (`def.href`), etc. La
|
|
11
|
+
* promesa mantiene el botón en `loading`.
|
|
12
|
+
*/
|
|
13
|
+
onAction: (action: ResolvedAction, ctx: DocumentContext) => void | Promise<unknown>;
|
|
14
|
+
/** UI del host para pestañas `kind: 'slot'` (historial, archivos, vista previa PDF…). */
|
|
15
|
+
slots?: Record<string, DocumentSlotRenderer>;
|
|
16
|
+
/** Navegación del host (botones inteligentes, cliente). */
|
|
17
|
+
onNavigate?: (href: string) => void;
|
|
18
|
+
renderLink?: (props: {
|
|
19
|
+
href: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
}) => ReactNode;
|
|
23
|
+
/** Botón «Volver» u otro contenido antes del título. */
|
|
24
|
+
leading?: ReactNode;
|
|
25
|
+
/** Pestaña inicial (`?tab=`). */
|
|
26
|
+
defaultTab?: string;
|
|
27
|
+
/** Pestaña controlada por el host (p. ej. «Ver detalle del error» abre Datos fiscales). */
|
|
28
|
+
tab?: string;
|
|
29
|
+
onTabChange?: (tab: string) => void;
|
|
30
|
+
/** Ruta base para el enlace del subtítulo (`/m/customers/{{customer_id}}`). */
|
|
31
|
+
subtitleHrefTemplate?: string;
|
|
32
|
+
/** Cambia para forzar recarga de las fuentes tras una acción. */
|
|
33
|
+
reloadKey?: number;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function DocumentPage(props: DocumentPageProps): import("react").JSX.Element;
|
|
37
|
+
//# sourceMappingURL=document-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-page.d.ts","sourceRoot":"","sources":["../../src/document/document-page.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AASzD,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACpE,OAAO,EAAqB,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE7E,OAAO,KAAK,EAER,eAAe,EAEf,oBAAoB,EAGpB,cAAc,EACjB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IACzD,YAAY,EAAE,wBAAwB,CAAA;IACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACnF,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;IAC5C,2DAA2D;IAC3D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAA;IAC5F,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2FAA2F;IAC3F,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,+EAA+E;IAC/E,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BA4HpD"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DocumentPage — «página de documento» declarativa (benchmark §5.2 / §6.1):
|
|
3
|
+
// cabecera + badges + barra de acciones por estado + botones inteligentes +
|
|
4
|
+
// pestañas. Se alimenta de un `DocumentSpec` (datos puros) y de un registro;
|
|
5
|
+
// las pestañas de campos y de registros relacionados se resuelven solas, y lo
|
|
6
|
+
// que necesita UI del host (historial, archivos, PDF) entra por `slots`.
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@asteby/metacore-ui/primitives';
|
|
10
|
+
import { cn } from '@asteby/metacore-ui/lib';
|
|
11
|
+
import { ActionBar } from './action-bar';
|
|
12
|
+
import { DocumentHeader } from './document-header';
|
|
13
|
+
import { SmartButtons } from './smart-buttons';
|
|
14
|
+
import { StatusBadge } from './status-badge';
|
|
15
|
+
import { interpolate, readPath, resolveActions, resolveStatus, resolveTitle } from './evaluate';
|
|
16
|
+
import { formatFieldValue } from './format';
|
|
17
|
+
import { findStatusMachine } from './registry';
|
|
18
|
+
import { useDocumentSources } from './use-document-sources';
|
|
19
|
+
export function DocumentPage(props) {
|
|
20
|
+
const { registration, record, onAction, slots, onNavigate, renderLink, leading, defaultTab, className } = props;
|
|
21
|
+
const { spec } = registration;
|
|
22
|
+
const { t } = useTranslation();
|
|
23
|
+
const fmt = { locale: props.locale, currency: props.currency, timeZone: props.timeZone };
|
|
24
|
+
const recordId = record.id;
|
|
25
|
+
const { sources, reload } = useDocumentSources(spec.sources, recordId);
|
|
26
|
+
const ctx = useMemo(() => {
|
|
27
|
+
const base = { record, sources };
|
|
28
|
+
return { ...base, derived: registration.derive?.(base) ?? {} };
|
|
29
|
+
}, [record, sources, registration]);
|
|
30
|
+
// Recarga las fuentes cuando el host avisa de un cambio (acción ejecutada).
|
|
31
|
+
const [lastReload, setLastReload] = useState(props.reloadKey ?? 0);
|
|
32
|
+
if ((props.reloadKey ?? 0) !== lastReload) {
|
|
33
|
+
setLastReload(props.reloadKey ?? 0);
|
|
34
|
+
reload();
|
|
35
|
+
}
|
|
36
|
+
const handleAction = (a) => {
|
|
37
|
+
const { tab: targetTab, openUrl, href } = a.def;
|
|
38
|
+
if (targetTab)
|
|
39
|
+
return setTab(targetTab);
|
|
40
|
+
if (openUrl) {
|
|
41
|
+
const url = interpolate(openUrl, ctx);
|
|
42
|
+
if (url)
|
|
43
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (href)
|
|
47
|
+
return onNavigate?.(interpolate(href, ctx));
|
|
48
|
+
return onAction(a, ctx);
|
|
49
|
+
};
|
|
50
|
+
const layout = useMemo(() => resolveActions(spec, ctx), [spec, ctx]);
|
|
51
|
+
const badges = spec.statuses
|
|
52
|
+
.map((m) => {
|
|
53
|
+
const status = resolveStatus(m, ctx);
|
|
54
|
+
return status ? { id: m.id, label: m.label, status } : null;
|
|
55
|
+
})
|
|
56
|
+
.filter((b) => !!b);
|
|
57
|
+
const metrics = (spec.metrics ?? [])
|
|
58
|
+
.filter((m) => !m.hideEmpty || readPath(ctx, m.path) != null)
|
|
59
|
+
.map((m) => ({
|
|
60
|
+
label: t(m.label, { defaultValue: m.label }),
|
|
61
|
+
value: formatFieldValue(readPath(ctx, m.path), m.format, fmt),
|
|
62
|
+
tone: m.tone,
|
|
63
|
+
mono: m.format === 'mono',
|
|
64
|
+
}));
|
|
65
|
+
const subtitleRaw = spec.subtitle ? readPath(ctx, spec.subtitle.path) : undefined;
|
|
66
|
+
const subtitleText = subtitleRaw == null || subtitleRaw === '' ? '—' : formatFieldValue(subtitleRaw, 'text', fmt);
|
|
67
|
+
const subtitle = subtitleText === '—' ? undefined : subtitleText;
|
|
68
|
+
const subtitleHref = subtitle && props.subtitleHrefTemplate ? interpolate(props.subtitleHrefTemplate, ctx) : undefined;
|
|
69
|
+
const copyValue = spec.copyable ? readPath(ctx, spec.copyable.path) : undefined;
|
|
70
|
+
const [innerTab, setInnerTab] = useState(defaultTab ?? spec.tabs[0]?.key);
|
|
71
|
+
const tab = props.tab ?? innerTab;
|
|
72
|
+
const setTab = (key) => {
|
|
73
|
+
setInnerTab(key);
|
|
74
|
+
props.onTabChange?.(key);
|
|
75
|
+
};
|
|
76
|
+
const smart = (spec.smartButtons ?? [])
|
|
77
|
+
.map((b) => {
|
|
78
|
+
const count = b.source ? sources[b.source]?.length ?? 0 : undefined;
|
|
79
|
+
const active = b.source ? (count ?? 0) > 0 : b.field ? !!readPath(ctx, b.field) : true;
|
|
80
|
+
if (b.hideWhenEmpty && !active)
|
|
81
|
+
return null;
|
|
82
|
+
const href = b.href ? interpolate(b.href, ctx) : undefined;
|
|
83
|
+
return {
|
|
84
|
+
key: b.key,
|
|
85
|
+
label: b.label,
|
|
86
|
+
icon: b.icon,
|
|
87
|
+
count,
|
|
88
|
+
disabled: !active && !b.tab,
|
|
89
|
+
onClick: () => {
|
|
90
|
+
if (b.tab)
|
|
91
|
+
setTab(b.tab);
|
|
92
|
+
else if (href)
|
|
93
|
+
onNavigate?.(href);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
})
|
|
97
|
+
.filter((b) => !!b);
|
|
98
|
+
return (_jsxs("div", { className: cn('flex flex-col gap-4', className), "data-document-page": spec.model, children: [_jsx(DocumentHeader, { title: resolveTitle(spec.title, ctx) || t('document.untitled', { defaultValue: 'Documento' }), subtitle: subtitle, subtitleHref: subtitleHref, badges: badges, metrics: metrics, copyable: spec.copyable && copyValue
|
|
99
|
+
? { label: t(spec.copyable.label, { defaultValue: spec.copyable.label }), value: String(copyValue) }
|
|
100
|
+
: undefined, leading: leading, renderLink: renderLink, actions: _jsx(ActionBar, { layout: layout, onAction: (a) => handleAction(a) }) }), _jsx(SmartButtons, { items: smart }), _jsxs(Tabs, { value: tab, onValueChange: setTab, className: 'flex flex-col gap-4', children: [_jsx(TabsList, { className: 'w-fit', children: spec.tabs.map((tb) => (_jsxs(TabsTrigger, { value: tb.key, children: [t(tb.label, { defaultValue: tb.label }), tabCount(tb, ctx) !== undefined ? (_jsx("span", { className: 'ml-1.5 rounded-full bg-muted px-1.5 text-xs tabular-nums', children: tabCount(tb, ctx) })) : null] }, tb.key))) }), spec.tabs.map((tb) => (_jsx(TabsContent, { value: tb.key, className: 'mt-0', children: renderTab(tb, spec, ctx, fmt, slots) }, tb.key)))] })] }));
|
|
101
|
+
}
|
|
102
|
+
function tabCount(tb, ctx) {
|
|
103
|
+
if (tb.kind === 'records')
|
|
104
|
+
return ctx.sources[tb.source]?.length;
|
|
105
|
+
if (tb.kind === 'slot' && tb.count) {
|
|
106
|
+
const v = readPath(ctx, tb.count);
|
|
107
|
+
return typeof v === 'number' ? v : undefined;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function renderTab(tb, spec, ctx, fmt, slots) {
|
|
112
|
+
if (tb.kind === 'slot')
|
|
113
|
+
return slots?.[tb.slot]?.(ctx) ?? null;
|
|
114
|
+
if (tb.kind === 'fields') {
|
|
115
|
+
return (_jsx("div", { className: 'flex flex-col gap-4', children: tb.groups.map((g, i) => {
|
|
116
|
+
const fields = g.fields.filter((f) => !f.hideEmpty || readPath(ctx, f.path) != null);
|
|
117
|
+
if (fields.length === 0)
|
|
118
|
+
return null;
|
|
119
|
+
return (_jsxs("section", { className: 'rounded-lg border bg-card p-5', children: [g.title ? _jsx("h2", { className: 'mb-3 text-sm font-semibold', children: g.title }) : null, _jsx("dl", { className: 'grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-2 lg:grid-cols-3', children: fields.map((f) => (_jsxs("div", { className: cn(f.wide && 'sm:col-span-2 lg:col-span-3'), children: [_jsx("dt", { className: 'text-xs uppercase tracking-wide text-muted-foreground', children: f.label }), _jsx("dd", { className: cn('break-words font-medium', f.format === 'mono' && 'font-mono text-xs'), children: formatFieldValue(readPath(ctx, f.path), f.format, fmt) })] }, f.path))) })] }, g.title ?? i));
|
|
120
|
+
}) }));
|
|
121
|
+
}
|
|
122
|
+
const rows = ctx.sources[tb.source] ?? [];
|
|
123
|
+
if (rows.length === 0) {
|
|
124
|
+
return (_jsx("p", { className: 'rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground', children: tb.empty ?? 'Sin registros.' }));
|
|
125
|
+
}
|
|
126
|
+
return (_jsx("div", { className: 'overflow-x-auto rounded-lg border bg-card', children: _jsxs("table", { className: 'w-full text-sm', children: [_jsx("thead", { className: 'bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground', children: _jsx("tr", { children: tb.columns.map((c) => (_jsx("th", { className: 'px-3 py-2 font-medium', children: c.label }, c.path))) }) }), _jsx("tbody", { children: rows.map((row, i) => (_jsx("tr", { className: 'border-t', children: tb.columns.map((c) => (_jsx("td", { className: 'px-3 py-2', children: renderCell(c, row, spec, fmt) }, c.path))) }, String(row.id ?? i)))) })] }) }));
|
|
127
|
+
}
|
|
128
|
+
function renderCell(col, row, spec, fmt) {
|
|
129
|
+
const v = row[col.path];
|
|
130
|
+
if (col.status) {
|
|
131
|
+
const machine = findStatusMachine(spec, col.status);
|
|
132
|
+
const st = machine?.states.find((s) => s.value === String(v));
|
|
133
|
+
if (st)
|
|
134
|
+
return _jsx(StatusBadge, { status: st });
|
|
135
|
+
}
|
|
136
|
+
return (_jsx("span", { className: cn(col.format === 'mono' && 'font-mono text-xs', 'tabular-nums'), children: formatFieldValue(v, col.format, fmt) }));
|
|
137
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type FieldFormatOptions } from './format';
|
|
2
|
+
import type { StatusTone } from './types';
|
|
3
|
+
export interface TimelineEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
at: string;
|
|
6
|
+
/** Texto ya redactado («Timbró la factura»). */
|
|
7
|
+
label: string;
|
|
8
|
+
actor?: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
tone?: StatusTone;
|
|
12
|
+
}
|
|
13
|
+
export interface DocumentTimelineProps extends FieldFormatOptions {
|
|
14
|
+
entries: TimelineEntry[];
|
|
15
|
+
/** Texto del estado vacío; lleva `emptyAction` si el host da una acción. */
|
|
16
|
+
emptyText?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function DocumentTimeline({ entries, emptyText, className, ...fmt }: DocumentTimelineProps): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=document-timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-timeline.d.ts","sourceRoot":"","sources":["../../src/document/document-timeline.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC7D,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAWD,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,GAAG,EAAE,EAAE,qBAAqB,+BA2BhG"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DocumentTimeline — línea de tiempo de eventos de negocio del documento
|
|
3
|
+
// («creó», «timbró», «envió a…», «abrió», «pagó», «solicitó cancelación»,
|
|
4
|
+
// «respuesta del SAT»). Complementa a <RecordHistory> (que muestra diffs de
|
|
5
|
+
// campos): aquí cada entrada es un hecho legible, con actor y hora.
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Circle } from 'lucide-react';
|
|
8
|
+
import { cn } from '@asteby/metacore-ui/lib';
|
|
9
|
+
import { DynamicIcon } from '../dynamic-icon';
|
|
10
|
+
import { formatFieldValue } from './format';
|
|
11
|
+
const TONE_DOT = {
|
|
12
|
+
neutral: 'text-muted-foreground',
|
|
13
|
+
info: 'text-[var(--info,oklch(0.55_0.13_235))]',
|
|
14
|
+
success: 'text-[var(--success,oklch(0.6_0.15_155))]',
|
|
15
|
+
caution: 'text-[var(--warning,oklch(0.6_0.15_65))]',
|
|
16
|
+
warning: 'text-[var(--warning,oklch(0.6_0.15_65))]',
|
|
17
|
+
critical: 'text-destructive',
|
|
18
|
+
};
|
|
19
|
+
export function DocumentTimeline({ entries, emptyText, className, ...fmt }) {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
if (entries.length === 0) {
|
|
22
|
+
return (_jsx("p", { className: 'rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground', children: emptyText ?? t('document.timeline_empty', { defaultValue: 'Aún no hay actividad en este documento.' }) }));
|
|
23
|
+
}
|
|
24
|
+
const sorted = [...entries].sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());
|
|
25
|
+
return (_jsx("ol", { className: cn('relative flex flex-col gap-4 border-l pl-5', className), "data-document-timeline": '', children: sorted.map((e) => (_jsxs("li", { className: 'relative', children: [_jsx("span", { className: cn('absolute -left-[1.85rem] flex h-5 w-5 items-center justify-center rounded-full bg-card', TONE_DOT[e.tone ?? 'neutral']), children: e.icon ? _jsx(DynamicIcon, { name: e.icon, className: 'h-4 w-4' }) : _jsx(Circle, { className: 'h-3 w-3 fill-current' }) }), _jsx("p", { className: 'text-sm font-medium', children: t(e.label, { defaultValue: e.label }) }), _jsxs("p", { className: 'text-xs text-muted-foreground', children: [formatFieldValue(e.at, 'datetime', fmt), e.actor ? ` · ${e.actor}` : ''] }), e.detail ? _jsx("p", { className: 'mt-1 whitespace-pre-wrap text-sm text-muted-foreground', children: e.detail }) : null] }, e.id))) }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DocumentContext, DocumentSpec, Predicate, ResolvedActionLayout, StatusDef, StatusMachine, When } from './types';
|
|
2
|
+
/** Máximo de secundarias visibles; el resto pasa a «Más…» (§5.2 / §6.2). */
|
|
3
|
+
export declare const MAX_VISIBLE_SECONDARY = 3;
|
|
4
|
+
/**
|
|
5
|
+
* Lee una ruta del contexto. `$.x` son los derivados; `sources.k.length` es el
|
|
6
|
+
* total de una fuente; el resto cuelga del registro (`record.x` o `x`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function readPath(ctx: DocumentContext, path: string): unknown;
|
|
9
|
+
export declare function evaluatePredicate(p: Predicate, ctx: DocumentContext): boolean;
|
|
10
|
+
/** Un arreglo es un AND; sin condición siempre se cumple. */
|
|
11
|
+
export declare function evaluateWhen(when: When | undefined, ctx: DocumentContext): boolean;
|
|
12
|
+
/** Estado actual de una máquina para el registro (o su fallback / un estado desconocido explícito). */
|
|
13
|
+
export declare function resolveStatus(machine: StatusMachine, ctx: DocumentContext): StatusDef | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Distribución de acciones para el estado actual. Una acción `hiddenWhen` se
|
|
16
|
+
* omite; una `blockedWhen` queda deshabilitada con motivo. Más de
|
|
17
|
+
* `MAX_VISIBLE_SECONDARY` secundarias se derraman a «Más…»; una acción marcada
|
|
18
|
+
* `destructive` siempre va a la zona destructiva aunque el layout la ponga en otro lado.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveActions(spec: DocumentSpec, ctx: DocumentContext): ResolvedActionLayout;
|
|
21
|
+
/** Interpola `{{campo}}` con valores del registro (sin evaluar código). */
|
|
22
|
+
export declare function interpolate(template: string, ctx: DocumentContext): string;
|
|
23
|
+
/** Título: la interpolación puede dejar vacío el resultado; se prueban alternativas separadas por `||`. */
|
|
24
|
+
export declare function resolveTitle(template: string, ctx: DocumentContext): string;
|
|
25
|
+
/**
|
|
26
|
+
* Quita del spec las acciones que dependen de una acción del modelo que el
|
|
27
|
+
* backend (o el addon que la aporta) no declara: mejor un botón menos que uno
|
|
28
|
+
* que abre un modal vacío. Las que resuelve la propia página (`href`/`openUrl`/`tab`) siempre se conservan.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pruneUnavailableActions(spec: DocumentSpec, availableModelActions: Iterable<string>): DocumentSpec;
|
|
31
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../src/document/evaluate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGR,eAAe,EACf,YAAY,EACZ,SAAS,EAET,oBAAoB,EACpB,SAAS,EACT,aAAa,EACb,IAAI,EACP,MAAM,SAAS,CAAA;AAEhB,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAEtC;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAuBpE;AAkBD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CA0B7E;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAIlF;AAED,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,eAAe,GAAG,SAAS,GAAG,SAAS,CAQjG;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,eAAe,GAAG,oBAAoB,CAoC7F;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,MAAM,CAK1E;AAED,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,MAAM,CAM3E;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,EAAE,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,YAAY,CAMjH"}
|