@asteby/metacore-runtime-react 45.0.0 → 45.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/action-modal-dispatcher.d.ts +29 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +115 -6
- package/dist/activity-diff.d.ts +2 -0
- package/dist/activity-diff.d.ts.map +1 -1
- package/dist/activity-diff.js +2 -2
- package/dist/approval-events.d.ts +16 -0
- package/dist/approval-events.d.ts.map +1 -0
- package/dist/approval-events.js +27 -0
- package/dist/approval-gate.d.ts +39 -0
- package/dist/approval-gate.d.ts.map +1 -0
- package/dist/approval-gate.js +110 -0
- package/dist/approval-pin-dialog.d.ts +25 -0
- package/dist/approval-pin-dialog.d.ts.map +1 -0
- package/dist/approval-pin-dialog.js +66 -0
- package/dist/business/catalog.d.ts +16 -0
- package/dist/business/catalog.d.ts.map +1 -0
- package/dist/business/catalog.js +50 -0
- package/dist/business/customer-picker.d.ts +39 -0
- package/dist/business/customer-picker.d.ts.map +1 -0
- package/dist/business/customer-picker.js +51 -0
- package/dist/business/feedback.d.ts +72 -0
- package/dist/business/feedback.d.ts.map +1 -0
- package/dist/business/feedback.js +52 -0
- package/dist/business/field-errors.d.ts +26 -0
- package/dist/business/field-errors.d.ts.map +1 -0
- package/dist/business/field-errors.js +59 -0
- package/dist/business/format.d.ts +49 -0
- package/dist/business/format.d.ts.map +1 -0
- package/dist/business/format.js +128 -0
- package/dist/business/index.d.ts +16 -0
- package/dist/business/index.d.ts.map +1 -0
- package/dist/business/index.js +15 -0
- package/dist/business/line-items-editor.d.ts +25 -0
- package/dist/business/line-items-editor.d.ts.map +1 -0
- package/dist/business/line-items-editor.js +36 -0
- package/dist/business/line-items.d.ts +69 -0
- package/dist/business/line-items.d.ts.map +1 -0
- package/dist/business/line-items.js +133 -0
- package/dist/business/payment-capture.d.ts +21 -0
- package/dist/business/payment-capture.d.ts.map +1 -0
- package/dist/business/payment-capture.js +24 -0
- package/dist/business/payment.d.ts +61 -0
- package/dist/business/payment.d.ts.map +1 -0
- package/dist/business/payment.js +74 -0
- package/dist/business/product-picker.d.ts +24 -0
- package/dist/business/product-picker.d.ts.map +1 -0
- package/dist/business/product-picker.js +48 -0
- package/dist/business/product-search.d.ts +70 -0
- package/dist/business/product-search.d.ts.map +1 -0
- package/dist/business/product-search.js +51 -0
- package/dist/business/refund-destination.d.ts +19 -0
- package/dist/business/refund-destination.d.ts.map +1 -0
- package/dist/business/refund-destination.js +31 -0
- package/dist/business/refund.d.ts +29 -0
- package/dist/business/refund.d.ts.map +1 -0
- package/dist/business/refund.js +44 -0
- package/dist/business/return-wizard.d.ts +23 -0
- package/dist/business/return-wizard.d.ts.map +1 -0
- package/dist/business/return-wizard.js +59 -0
- package/dist/business/return.d.ts +90 -0
- package/dist/business/return.d.ts.map +1 -0
- package/dist/business/return.js +112 -0
- package/dist/business/use-async-search.d.ts +10 -0
- package/dist/business/use-async-search.d.ts.map +1 -0
- package/dist/business/use-async-search.js +38 -0
- package/dist/document/action-bar.d.ts +12 -0
- package/dist/document/action-bar.d.ts.map +1 -0
- package/dist/document/action-bar.js +39 -0
- package/dist/document/cancel-with-reason.d.ts +47 -0
- package/dist/document/cancel-with-reason.d.ts.map +1 -0
- package/dist/document/cancel-with-reason.js +52 -0
- package/dist/document/document-header.d.ts +41 -0
- package/dist/document/document-header.d.ts.map +1 -0
- package/dist/document/document-header.js +30 -0
- package/dist/document/document-page.d.ts +37 -0
- package/dist/document/document-page.d.ts.map +1 -0
- package/dist/document/document-page.js +137 -0
- package/dist/document/document-timeline.d.ts +20 -0
- package/dist/document/document-timeline.d.ts.map +1 -0
- package/dist/document/document-timeline.js +26 -0
- package/dist/document/evaluate.d.ts +31 -0
- package/dist/document/evaluate.d.ts.map +1 -0
- package/dist/document/evaluate.js +170 -0
- package/dist/document/format.d.ts +9 -0
- package/dist/document/format.d.ts.map +1 -0
- package/dist/document/format.js +52 -0
- package/dist/document/index.d.ts +13 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +12 -0
- package/dist/document/registry.d.ts +20 -0
- package/dist/document/registry.d.ts.map +1 -0
- package/dist/document/registry.js +20 -0
- package/dist/document/smart-buttons.d.ts +16 -0
- package/dist/document/smart-buttons.d.ts.map +1 -0
- package/dist/document/smart-buttons.js +16 -0
- package/dist/document/status-badge.d.ts +16 -0
- package/dist/document/status-badge.d.ts.map +1 -0
- package/dist/document/status-badge.js +23 -0
- package/dist/document/types.d.ts +196 -0
- package/dist/document/types.d.ts.map +1 -0
- package/dist/document/types.js +1 -0
- package/dist/document/use-document-sources.d.ts +14 -0
- package/dist/document/use-document-sources.d.ts.map +1 -0
- package/dist/document/use-document-sources.js +72 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +3 -1
- package/dist/dynamic-relation.d.ts +4 -0
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +10 -1
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +14 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +28 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/reason-prompt.d.ts +59 -0
- package/dist/reason-prompt.d.ts.map +1 -0
- package/dist/reason-prompt.js +111 -0
- package/dist/server-error.d.ts.map +1 -1
- package/dist/server-error.js +19 -0
- package/dist/use-addon-settings.d.ts +17 -3
- package/dist/use-addon-settings.d.ts.map +1 -1
- package/dist/use-addon-settings.js +20 -8
- package/package.json +1 -1
- package/src/__tests__/approval-gate.test.tsx +134 -0
- package/src/__tests__/prefill-from-record.test.ts +69 -0
- package/src/__tests__/reason-prompt.test.tsx +121 -0
- package/src/__tests__/use-addon-settings.test.ts +16 -1
- package/src/action-modal-dispatcher.tsx +145 -3
- package/src/activity-diff.tsx +10 -1
- package/src/approval-events.ts +39 -0
- package/src/approval-gate.tsx +180 -0
- package/src/approval-pin-dialog.tsx +162 -0
- package/src/business/business.test.ts +210 -0
- package/src/business/catalog.ts +68 -0
- package/src/business/customer-picker.tsx +201 -0
- package/src/business/feedback.tsx +141 -0
- package/src/business/field-errors.ts +92 -0
- package/src/business/format.ts +151 -0
- package/src/business/index.ts +15 -0
- package/src/business/line-items-editor.tsx +259 -0
- package/src/business/line-items.ts +190 -0
- package/src/business/payment-capture.tsx +158 -0
- package/src/business/payment.ts +125 -0
- package/src/business/product-picker.tsx +171 -0
- package/src/business/product-search.ts +108 -0
- package/src/business/refund-destination.tsx +121 -0
- package/src/business/refund.ts +72 -0
- package/src/business/return-wizard.tsx +223 -0
- package/src/business/return.ts +181 -0
- package/src/business/use-async-search.ts +43 -0
- package/src/document/__tests__/document-page.test.tsx +107 -0
- package/src/document/__tests__/evaluate.test.ts +140 -0
- package/src/document/action-bar.tsx +140 -0
- package/src/document/cancel-with-reason.tsx +214 -0
- package/src/document/document-header.tsx +140 -0
- package/src/document/document-page.tsx +268 -0
- package/src/document/document-timeline.tsx +66 -0
- package/src/document/evaluate.ts +180 -0
- package/src/document/format.ts +59 -0
- package/src/document/index.ts +28 -0
- package/src/document/registry.ts +39 -0
- package/src/document/smart-buttons.tsx +54 -0
- package/src/document/status-badge.tsx +44 -0
- package/src/document/types.ts +203 -0
- package/src/document/use-document-sources.ts +91 -0
- package/src/dynamic-columns.tsx +5 -1
- package/src/dynamic-relation.tsx +14 -1
- package/src/dynamic-row-actions.tsx +13 -1
- package/src/dynamic-table.tsx +22 -2
- package/src/index.ts +33 -0
- package/src/reason-prompt.tsx +247 -0
- package/src/server-error.ts +20 -0
- package/src/use-addon-settings.ts +26 -7
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ApprovalPinDialog — the supervisor PIN prompt shared by every sensitive action
|
|
3
|
+
// (POS sell-without-stock, discount above the cap, price below the floor, refund,
|
|
4
|
+
// CFDI cancel, inventory adjust). Presentational: it collects a reason + a PIN and
|
|
5
|
+
// hands them to `onSubmit`, which does the network call, so federated addons that
|
|
6
|
+
// carry their own HTTP client reuse the same dialog as hosts with an ApiProvider.
|
|
7
|
+
//
|
|
8
|
+
// The reason is always required (audit trail: who asked, who authorized, why).
|
|
9
|
+
// A rejected `onSubmit` keeps the dialog open and shows the error inline (wrong
|
|
10
|
+
// PIN, locked, no permission) instead of a toast that can be missed.
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import { Loader2, ShieldCheck } from 'lucide-react';
|
|
14
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Textarea, } from '@asteby/metacore-ui/primitives';
|
|
15
|
+
/** Best human message out of an axios-style error carrying the kernel envelope. */
|
|
16
|
+
export function approvalErrorMessage(err, t) {
|
|
17
|
+
const data = err?.response?.data;
|
|
18
|
+
switch (data?.error?.code) {
|
|
19
|
+
case 'approval_pin_invalid':
|
|
20
|
+
return t('approvals.pin_invalid', { defaultValue: 'PIN incorrecto o sin permiso para autorizar esto.' });
|
|
21
|
+
case 'approval_pin_locked':
|
|
22
|
+
return t('approvals.pin_locked', { defaultValue: 'Demasiados intentos. Espera unos minutos.' });
|
|
23
|
+
case 'approval_pin_unavailable':
|
|
24
|
+
return t('approvals.pin_unavailable', { defaultValue: 'La autorización con PIN no está disponible.' });
|
|
25
|
+
case 'approval_reason_required':
|
|
26
|
+
return t('approvals.reason_required', { defaultValue: 'Escribe el motivo.' });
|
|
27
|
+
}
|
|
28
|
+
return data?.error?.message || data?.message || err?.message || t('common.error', { defaultValue: 'Algo salió mal' });
|
|
29
|
+
}
|
|
30
|
+
export function ApprovalPinDialog({ open, title, description, onSubmit, onCancel, secondaryAction }) {
|
|
31
|
+
const { t } = useTranslation();
|
|
32
|
+
const [pin, setPin] = useState('');
|
|
33
|
+
const [reason, setReason] = useState('');
|
|
34
|
+
const [busy, setBusy] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open) {
|
|
38
|
+
setPin('');
|
|
39
|
+
setReason('');
|
|
40
|
+
setError(null);
|
|
41
|
+
setBusy(false);
|
|
42
|
+
}
|
|
43
|
+
}, [open]);
|
|
44
|
+
const ready = /^\d{4,8}$/.test(pin) && reason.trim().length > 0;
|
|
45
|
+
const submit = async () => {
|
|
46
|
+
if (!ready || busy)
|
|
47
|
+
return;
|
|
48
|
+
setBusy(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
await onSubmit({ pin, reason: reason.trim() });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
setError(approvalErrorMessage(err, t));
|
|
55
|
+
setPin('');
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => { if (!o && !busy)
|
|
62
|
+
onCancel(); }, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(ShieldCheck, { className: "h-5 w-5" }), title] }), _jsx(DialogDescription, { asChild: true, children: _jsx("div", { children: description ?? t('approvals.pin_prompt', { defaultValue: 'Requiere autorización de un supervisor.' }) }) })] }), _jsxs("form", { className: "space-y-3", onSubmit: (e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
void submit();
|
|
65
|
+
}, children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-sm font-medium", htmlFor: "approval-reason", children: t('approvals.reason', { defaultValue: 'Motivo' }) }), _jsx(Textarea, { id: "approval-reason", value: reason, onChange: (e) => setReason(e.target.value), rows: 2, placeholder: t('approvals.reason_placeholder', { defaultValue: 'Por qué se autoriza' }) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-sm font-medium", htmlFor: "approval-pin", children: t('approvals.pin', { defaultValue: 'PIN del supervisor' }) }), _jsx(Input, { id: "approval-pin", type: "password", inputMode: "numeric", autoComplete: "off", maxLength: 8, value: pin, onChange: (e) => setPin(e.target.value.replace(/\D/g, '')), "aria-invalid": error ? true : undefined })] }), error ? (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })) : null, _jsxs(DialogFooter, { className: "gap-2 sm:gap-2", children: [secondaryAction ? (_jsx(Button, { type: "button", variant: "ghost", onClick: secondaryAction.onClick, disabled: busy, children: secondaryAction.label })) : null, _jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: busy, children: t('common.cancel', { defaultValue: 'Cancelar' }) }), _jsxs(Button, { type: "submit", disabled: !ready || busy, children: [busy ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, t('approvals.authorize', { defaultValue: 'Autorizar' })] })] })] })] }) }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BusinessComponentSpec {
|
|
2
|
+
/** Nombre exportado desde `@asteby/metacore-runtime-react`. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Prop de datos principal y su tipo. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Eventos (props `on*`) que emite. */
|
|
7
|
+
events: string[];
|
|
8
|
+
/** Capabilities que consulta con `useCan` (formato `<modelo>.<acción>`). */
|
|
9
|
+
permissions: string[];
|
|
10
|
+
/** Módulos donde el benchmark dice que se usa. */
|
|
11
|
+
usedBy: string[];
|
|
12
|
+
/** Utilidades puras exportadas junto al componente. */
|
|
13
|
+
helpers: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare const BUSINESS_COMPONENTS: readonly BusinessComponentSpec[];
|
|
16
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAiDtD,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const BUSINESS_COMPONENTS = [
|
|
2
|
+
{
|
|
3
|
+
name: 'CustomerPicker',
|
|
4
|
+
value: 'value: CustomerResult | null',
|
|
5
|
+
events: ['onChange'],
|
|
6
|
+
permissions: ['<model>.create (alta rápida; default Customer → customer.create)'],
|
|
7
|
+
usedBy: ['POS', 'Cotización', 'Pedido', 'Apartado', 'Factura', 'OT', 'RMA', 'Cobranza'],
|
|
8
|
+
helpers: [],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'ProductPicker',
|
|
12
|
+
value: '(sin estado: search + onSelect)',
|
|
13
|
+
events: ['onSelect'],
|
|
14
|
+
permissions: [],
|
|
15
|
+
usedBy: ['POS', 'Cotización', 'Pedido', 'OC', 'Recepción', 'Traspaso', 'Ajuste', 'OT'],
|
|
16
|
+
helpers: ['parseProductQuery', 'parseTireSize', 'availableStock', 'productToLine'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'LineItemsEditor',
|
|
20
|
+
value: 'value: LineItem[]',
|
|
21
|
+
events: ['onChange', 'onValidate', 'onRequestProduct'],
|
|
22
|
+
permissions: ['editPermission (prop, opcional)'],
|
|
23
|
+
usedBy: ['Cotización', 'Pedido', 'Factura', 'OC', 'Recepción', 'OT', 'RMA', 'NC'],
|
|
24
|
+
helpers: ['makeLine', 'computeLine', 'computeTotals', 'validateLineItems', 'serializeLineItems', 'parseLineItems'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'PaymentCapture',
|
|
28
|
+
value: 'value: PaymentTender[]',
|
|
29
|
+
events: ['onChange', 'onSubmit'],
|
|
30
|
+
permissions: ['submitPermission (prop, opcional)'],
|
|
31
|
+
usedBy: ['POS/Caja', 'Apartado', 'Cobranza', 'Factura (Registrar pago)', 'OT'],
|
|
32
|
+
helpers: ['newTender', 'summarizePayment', 'validatePayment', 'serializePayment'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'RefundDestination',
|
|
36
|
+
value: 'value: RefundAllocation[]',
|
|
37
|
+
events: ['onChange', 'onValidate'],
|
|
38
|
+
permissions: [],
|
|
39
|
+
usedBy: ['ReturnWizard', 'Cancelación de apartado', 'Anticipos'],
|
|
40
|
+
helpers: ['validateRefund', 'singleDestination', 'DEFAULT_REFUND_DESTINATIONS'],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'ReturnWizard',
|
|
44
|
+
value: 'lines: ReturnableLine[]',
|
|
45
|
+
events: ['onSubmit', 'onCancel'],
|
|
46
|
+
permissions: [],
|
|
47
|
+
usedBy: ['POS', 'Pedido', 'Factura', 'RMA', 'OT'],
|
|
48
|
+
helpers: ['computeReturnTotals', 'validateReturnChoices', 'creditNoteRelation', 'returnSteps', 'serializeReturn'],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface CustomerResult {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
/** Identificador fiscal (RFC u homólogo): dato de la org, sin formato hardcodeado. */
|
|
5
|
+
tax_id?: string;
|
|
6
|
+
phone?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
plate?: string;
|
|
9
|
+
vin?: string;
|
|
10
|
+
/** Saldo por cobrar (adeudo). */
|
|
11
|
+
balance?: number;
|
|
12
|
+
credit_limit?: number;
|
|
13
|
+
/** Alertas visibles: «Cliente con adeudo vencido», «Lista negra». */
|
|
14
|
+
alerts?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface CustomerPickerProps {
|
|
17
|
+
value: CustomerResult | null;
|
|
18
|
+
/** Evento único de cambio: cliente elegido/creado, o `null` al limpiar. */
|
|
19
|
+
onChange: (customer: CustomerResult | null) => void;
|
|
20
|
+
/** Búsqueda (nombre, ID fiscal, teléfono, placa, VIN). Estable (useCallback). */
|
|
21
|
+
search: (q: string, signal: AbortSignal) => Promise<CustomerResult[]>;
|
|
22
|
+
/** Modelo del kernel para el alta rápida. Default `Customer`. */
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Endpoint CRUD del alta. Default `/data/<model>/me`. */
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
/** Convierte el registro guardado a `CustomerResult`. Default: id + name. */
|
|
27
|
+
mapRecord?: (rec: Record<string, unknown>) => CustomerResult;
|
|
28
|
+
/** Valida el ID fiscal según la config de la org; devuelve el mensaje o undefined. */
|
|
29
|
+
validateTaxId?: (taxId: string) => string | undefined;
|
|
30
|
+
/** Overrides de permiso (default `<model>.create` con useCan). */
|
|
31
|
+
canCreate?: boolean;
|
|
32
|
+
/** Valores iniciales del alta rápida (p. ej. el texto buscado como nombre). */
|
|
33
|
+
createDefaults?: Record<string, unknown>;
|
|
34
|
+
currency?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function CustomerPicker({ value, onChange, search, model, endpoint, mapRecord, validateTaxId, canCreate, createDefaults, currency, disabled, placeholder, }: CustomerPickerProps): import("react").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=customer-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-picker.d.ts","sourceRoot":"","sources":["../../src/business/customer-picker.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,iFAAiF;IACjF,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IACrE,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,cAAc,CAAA;IAC5D,sFAAsF;IACtF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACrD,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAkB,EAClB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,WAAW,GACd,EAAE,mBAAmB,+BAkIrB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// CustomerPicker + alta rápida — busca por nombre, RFC/ID fiscal, teléfono, placa
|
|
3
|
+
// o VIN; muestra saldo, crédito y alertas (benchmark §7). El alta rápida reutiliza
|
|
4
|
+
// el <CreateRecordDialog> dinámico del modelo (formulario desde metadata).
|
|
5
|
+
import { useCallback, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Plus, Search, X } from 'lucide-react';
|
|
8
|
+
import { Badge, Button, Input } from '@asteby/metacore-ui';
|
|
9
|
+
import { CreateRecordDialog } from '../dialogs/create-record-dialog';
|
|
10
|
+
import { useApi } from '../api-context';
|
|
11
|
+
import { useCan } from '../permissions-context';
|
|
12
|
+
import { useFormatter } from './format';
|
|
13
|
+
import { useAsyncSearch } from './use-async-search';
|
|
14
|
+
import { EmptyState } from './feedback';
|
|
15
|
+
function snake(model) {
|
|
16
|
+
return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
export function CustomerPicker({ value, onChange, search, model = 'Customer', endpoint, mapRecord, validateTaxId, canCreate, createDefaults, currency, disabled, placeholder, }) {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const api = useApi();
|
|
21
|
+
const can = useCan();
|
|
22
|
+
const fmt = useFormatter({ currency });
|
|
23
|
+
const [text, setText] = useState('');
|
|
24
|
+
const [creating, setCreating] = useState(false);
|
|
25
|
+
const { results, loading, error } = useAsyncSearch(text, search, { minChars: 2 });
|
|
26
|
+
const mayCreate = canCreate ?? can(`${snake(model)}.create`);
|
|
27
|
+
const base = endpoint ?? `/data/${model}/me`;
|
|
28
|
+
const taxIdError = value?.tax_id && validateTaxId ? validateTaxId(value.tax_id) : undefined;
|
|
29
|
+
const pick = (c) => {
|
|
30
|
+
onChange(c);
|
|
31
|
+
setText('');
|
|
32
|
+
};
|
|
33
|
+
const toResult = useCallback((rec) => mapRecord
|
|
34
|
+
? mapRecord(rec)
|
|
35
|
+
: {
|
|
36
|
+
id: String(rec.id ?? ''),
|
|
37
|
+
name: String(rec.name ?? ''),
|
|
38
|
+
tax_id: typeof rec.tax_id === 'string' ? rec.tax_id : undefined,
|
|
39
|
+
phone: typeof rec.phone === 'string' ? rec.phone : undefined,
|
|
40
|
+
}, [mapRecord]);
|
|
41
|
+
if (value) {
|
|
42
|
+
const overLimit = value.credit_limit != null && (value.balance ?? 0) > value.credit_limit;
|
|
43
|
+
return (_jsxs("div", { "data-slot": "customer-picker", className: "space-y-1 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: value.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: [value.tax_id, value.phone, value.plate, value.vin].filter(Boolean).join(' · ') })] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => onChange(null), "aria-label": t('customerPicker.clear', { defaultValue: 'Quitar cliente' }), children: _jsx(X, { className: "size-4" }) }))] }), _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [value.balance != null && value.balance !== 0 && (_jsxs(Badge, { variant: overLimit ? 'danger' : 'warning', children: [t('customerPicker.balance', { defaultValue: 'Adeudo' }), " ", fmt.money(value.balance)] })), value.credit_limit != null && (_jsxs(Badge, { variant: "muted", children: [t('customerPicker.credit', { defaultValue: 'Crédito' }), " ", fmt.money(value.credit_limit)] })), value.alerts?.map((a) => (_jsx(Badge, { variant: "danger", children: a }, a)))] }), taxIdError && _jsx("p", { role: "alert", className: "text-xs text-destructive", children: taxIdError })] }));
|
|
44
|
+
}
|
|
45
|
+
return (_jsxs("div", { "data-slot": "customer-picker", className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", disabled: disabled, value: text, placeholder: placeholder ?? t('customerPicker.placeholder', { defaultValue: 'Nombre, RFC, teléfono, placa o VIN' }), onChange: (e) => setText(e.target.value) })] }), mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: () => setCreating(true), "aria-label": t('customerPicker.create', { defaultValue: 'Nuevo cliente' }), children: _jsx(Plus, { className: "size-4" }) }))] }), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('customerPicker.error', { defaultValue: 'No se pudo buscar clientes. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('customerPicker.empty', { defaultValue: 'No encontramos a ese cliente' }), action: mayCreate ? { label: t('customerPicker.createAction', { defaultValue: 'Crear cliente' }), onClick: () => setCreating(true) } : undefined })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((c) => (_jsx("li", { role: "option", "aria-selected": false, children: _jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left hover:bg-accent", onClick: () => pick(c), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: c.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: [c.tax_id, c.phone, c.plate, c.vin].filter(Boolean).join(' · ') })] }), c.balance != null && c.balance > 0 && _jsx(Badge, { variant: "warning", children: fmt.money(c.balance) })] }) }, c.id))) })), creating && (_jsx(CreateRecordDialog, { modelKey: model, open: creating, onOpenChange: setCreating, endpoint: base, defaults: createDefaults ?? (text.trim() ? { name: text.trim() } : undefined), onCreate: async (data) => {
|
|
46
|
+
const res = await api.post(base, data);
|
|
47
|
+
const rec = (res.data?.data ?? res.data);
|
|
48
|
+
pick(toResult(rec));
|
|
49
|
+
return rec.id != null ? { id: String(rec.id) } : undefined;
|
|
50
|
+
} }))] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type LucideIcon } from 'lucide-react';
|
|
3
|
+
/**
|
|
4
|
+
* Props recomendadas para `<Toaster>` de metacore-ui: los toasts viven arriba a
|
|
5
|
+
* la derecha (el área de acciones primarias suele estar abajo) y dejan hueco
|
|
6
|
+
* bajo la cabecera. Se propagan tal cual: `<Toaster {...ACTION_SAFE_TOASTER_PROPS} />`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ACTION_SAFE_TOASTER_PROPS: {
|
|
9
|
+
position: "top-right";
|
|
10
|
+
offset: number;
|
|
11
|
+
mobileOffset: number;
|
|
12
|
+
visibleToasts: number;
|
|
13
|
+
};
|
|
14
|
+
/** Duración de un toast de éxito (ms): confirma y se va. */
|
|
15
|
+
export declare const SUCCESS_TOAST_MS = 3000;
|
|
16
|
+
export declare const notify: {
|
|
17
|
+
/** Confirmación breve de un éxito («Pago registrado»). Admite «Deshacer». */
|
|
18
|
+
success(message: string, opts?: {
|
|
19
|
+
undo?: {
|
|
20
|
+
label?: string;
|
|
21
|
+
onUndo: () => void;
|
|
22
|
+
};
|
|
23
|
+
description?: string;
|
|
24
|
+
}): string | number;
|
|
25
|
+
/**
|
|
26
|
+
* Error inesperado (red, 5xx) sin campo al que atribuirlo. Los errores de
|
|
27
|
+
* validación NO van aquí: usa `mapApiError` + `<FormErrorBanner>`/error del input.
|
|
28
|
+
*/
|
|
29
|
+
error(message: string, description?: string): string | number;
|
|
30
|
+
};
|
|
31
|
+
export interface SystemNoticeBannerProps {
|
|
32
|
+
message: ReactNode;
|
|
33
|
+
/** Acción opcional («Actualizar»). */
|
|
34
|
+
action?: {
|
|
35
|
+
label: string;
|
|
36
|
+
onClick: () => void;
|
|
37
|
+
};
|
|
38
|
+
onDismiss?: () => void;
|
|
39
|
+
dismissLabel?: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Aviso del sistema en flujo normal de la página (no flota sobre botones). */
|
|
43
|
+
export declare function SystemNoticeBanner({ message, action, onDismiss, dismissLabel, className }: SystemNoticeBannerProps): import("react").JSX.Element;
|
|
44
|
+
export interface FormErrorBannerProps {
|
|
45
|
+
/** Qué pasó y cómo corregirlo (típicamente `mapApiError(...).form`). */
|
|
46
|
+
message?: string;
|
|
47
|
+
/** Botón a la solución («Ir al campo», «Reintentar»). */
|
|
48
|
+
action?: {
|
|
49
|
+
label: string;
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
};
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
/** Error persistente de formulario; no renderiza nada sin `message`. */
|
|
55
|
+
export declare function FormErrorBanner({ message, action, className }: FormErrorBannerProps): import("react").JSX.Element | null;
|
|
56
|
+
export interface EmptyStateProps {
|
|
57
|
+
icon?: LucideIcon;
|
|
58
|
+
/** Qué falta («Aún no hay movimientos para este producto»). */
|
|
59
|
+
title: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
/** Acción primaria («Registrar saldo inicial»). Se oculta si `hidden`. */
|
|
62
|
+
action?: {
|
|
63
|
+
label: string;
|
|
64
|
+
onClick?: () => void;
|
|
65
|
+
href?: string;
|
|
66
|
+
hidden?: boolean;
|
|
67
|
+
};
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Estado vacío accionable: explicación + una acción primaria. */
|
|
71
|
+
export declare function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps): import("react").JSX.Element;
|
|
72
|
+
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/business/feedback.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAGpE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;;;;CAKrC,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,eAAO,MAAM,MAAM;IACf,6EAA6E;qBAC5D,MAAM,SAAS;QAAE,IAAI,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,IAAI,CAAA;SAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IASvG;;;OAGG;mBACY,MAAM,gBAAgB,MAAM;CAG9C,CAAA;AAED,MAAM,WAAW,uBAAuB;IACpC,OAAO,EAAE,SAAS,CAAA;IAClB,sCAAsC;IACtC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IAC/C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAA0B,EAAE,SAAS,EAAE,EAAE,uBAAuB,+BAqBhI;AAED,MAAM,WAAW,oBAAoB;IACjC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,oBAAoB,sCAiBnF;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,eAAe,+BAsBhG"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { AlertCircle, Info, X } from 'lucide-react';
|
|
4
|
+
import { Button } from '@asteby/metacore-ui';
|
|
5
|
+
/**
|
|
6
|
+
* Props recomendadas para `<Toaster>` de metacore-ui: los toasts viven arriba a
|
|
7
|
+
* la derecha (el área de acciones primarias suele estar abajo) y dejan hueco
|
|
8
|
+
* bajo la cabecera. Se propagan tal cual: `<Toaster {...ACTION_SAFE_TOASTER_PROPS} />`.
|
|
9
|
+
*/
|
|
10
|
+
export const ACTION_SAFE_TOASTER_PROPS = {
|
|
11
|
+
position: 'top-right',
|
|
12
|
+
offset: 72,
|
|
13
|
+
mobileOffset: 72,
|
|
14
|
+
visibleToasts: 3,
|
|
15
|
+
};
|
|
16
|
+
/** Duración de un toast de éxito (ms): confirma y se va. */
|
|
17
|
+
export const SUCCESS_TOAST_MS = 3000;
|
|
18
|
+
export const notify = {
|
|
19
|
+
/** Confirmación breve de un éxito («Pago registrado»). Admite «Deshacer». */
|
|
20
|
+
success(message, opts) {
|
|
21
|
+
return toast.success(message, {
|
|
22
|
+
duration: SUCCESS_TOAST_MS,
|
|
23
|
+
description: opts?.description,
|
|
24
|
+
action: opts?.undo
|
|
25
|
+
? { label: opts.undo.label ?? 'Deshacer', onClick: opts.undo.onUndo }
|
|
26
|
+
: undefined,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Error inesperado (red, 5xx) sin campo al que atribuirlo. Los errores de
|
|
31
|
+
* validación NO van aquí: usa `mapApiError` + `<FormErrorBanner>`/error del input.
|
|
32
|
+
*/
|
|
33
|
+
error(message, description) {
|
|
34
|
+
return toast.error(message, { description, duration: 8000 });
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/** Aviso del sistema en flujo normal de la página (no flota sobre botones). */
|
|
38
|
+
export function SystemNoticeBanner({ message, action, onDismiss, dismissLabel = 'Descartar', className }) {
|
|
39
|
+
return (_jsxs("div", { role: "status", "data-slot": "system-notice", className: `flex items-center gap-3 rounded-md border bg-muted px-3 py-2 text-sm ${className ?? ''}`, children: [_jsx(Info, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "flex-1", children: message }), action && (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: action.onClick, children: action.label })), onDismiss && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: onDismiss, "aria-label": dismissLabel, children: _jsx(X, { className: "size-4" }) }))] }));
|
|
40
|
+
}
|
|
41
|
+
/** Error persistente de formulario; no renderiza nada sin `message`. */
|
|
42
|
+
export function FormErrorBanner({ message, action, className }) {
|
|
43
|
+
if (!message)
|
|
44
|
+
return null;
|
|
45
|
+
return (_jsxs("div", { role: "alert", "data-slot": "form-error", className: `flex items-start gap-3 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive ${className ?? ''}`, children: [_jsx(AlertCircle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }), _jsx("span", { className: "flex-1", children: message }), action && (_jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: action.onClick, children: action.label }))] }));
|
|
46
|
+
}
|
|
47
|
+
/** Estado vacío accionable: explicación + una acción primaria. */
|
|
48
|
+
export function EmptyState({ icon: Icon, title, description, action, className }) {
|
|
49
|
+
const showAction = action && !action.hidden;
|
|
50
|
+
return (_jsxs("div", { "data-slot": "empty-state", className: `flex flex-col items-center gap-2 px-4 py-10 text-center ${className ?? ''}`, children: [Icon && _jsx(Icon, { className: "size-8 text-muted-foreground", "aria-hidden": true }), _jsx("p", { className: "text-sm font-medium", children: title }), description && _jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: description }), showAction &&
|
|
51
|
+
(action.href ? (_jsx(Button, { asChild: true, size: "sm", className: "mt-2", children: _jsx("a", { href: action.href, children: action.label }) })) : (_jsx(Button, { type: "button", size: "sm", className: "mt-2", onClick: action.onClick, children: action.label })))] }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Translate } from '../server-error';
|
|
2
|
+
export interface MappedApiError {
|
|
3
|
+
/** HTTP status si el error lo trae (axios `response.status`). */
|
|
4
|
+
status?: number;
|
|
5
|
+
/** Mensaje ya localizado por campo, listo para el `error` de cada input. */
|
|
6
|
+
fields: Record<string, string>;
|
|
7
|
+
/** Mensaje general (qué pasó + cómo corregirlo) para un banner; sin jerga. */
|
|
8
|
+
form?: string;
|
|
9
|
+
/** true si hay al menos un mensaje atribuible a un campo. */
|
|
10
|
+
hasFieldErrors: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MapApiErrorOptions {
|
|
13
|
+
t?: Translate;
|
|
14
|
+
language?: string;
|
|
15
|
+
/** Etiquetas traducidas por clave de campo (para «El campo {{label}}…»). */
|
|
16
|
+
labels?: Record<string, string>;
|
|
17
|
+
/** Campos conocidos del formulario: un 409 se atribuye al que la causa nombra. */
|
|
18
|
+
knownFields?: string[];
|
|
19
|
+
fallback?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Convierte cualquier error de mutación en mensajes por campo + mensaje general.
|
|
23
|
+
* Pura salvo por `t`; no muestra toasts.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mapApiError(err: unknown, opts?: MapApiErrorOptions): MappedApiError;
|
|
26
|
+
//# sourceMappingURL=field-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-errors.d.ts","sourceRoot":"","sources":["../../src/business/field-errors.ts"],"names":[],"mappings":"AAOA,OAAO,EAIH,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAA;AAExB,MAAM,WAAW,cAAc;IAC3B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,cAAc,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAC/B,CAAC,CAAC,EAAE,SAAS,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAsBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,GAAE,kBAAuB,GAAG,cAAc,CAgCvF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Mapeo de errores de API (409/422/…) a mensajes por campo (PIT-002/003/013/021/029).
|
|
2
|
+
//
|
|
3
|
+
// Un 422 trae `errors: {campo: [{code, params}]}`; un 409 (duplicado, enum,
|
|
4
|
+
// conflicto de estado) suele traer solo `message`/`details`. Ambos se resuelven
|
|
5
|
+
// aquí a `{ fields, form }`: los campos se pintan junto al input y `form` (lo que
|
|
6
|
+
// no se pudo atribuir a un campo) va a un banner persistente — el toast solo
|
|
7
|
+
// confirma éxitos.
|
|
8
|
+
import { extractFieldErrors, extractServerError, localizeFieldErrorMap, } from '../server-error';
|
|
9
|
+
const CONFLICT_HINTS = [
|
|
10
|
+
{ re: /duplicate|already exists|unique|23505|ya existe/i, es: 'Ya existe un registro con ese valor' },
|
|
11
|
+
];
|
|
12
|
+
/** Traductor sin i18next: devuelve el default con `{{var}}` interpolado. */
|
|
13
|
+
const defaultTranslate = (key, o) => (o?.defaultValue ?? key).replace(/\{\{(\w+)\}\}/g, (_, k) => String(o?.[k] ?? ''));
|
|
14
|
+
function statusOf(err) {
|
|
15
|
+
const s = err?.response?.status;
|
|
16
|
+
return typeof s === 'number' ? s : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Encuentra el campo que un texto de causa nombra (`Key (sku)=(A1) already exists`). */
|
|
19
|
+
function fieldNamedBy(text, known) {
|
|
20
|
+
if (!known?.length)
|
|
21
|
+
return undefined;
|
|
22
|
+
const lower = text.toLowerCase();
|
|
23
|
+
return known.find((k) => new RegExp(`(^|[^a-z0-9_])${k.toLowerCase()}([^a-z0-9_]|$)`).test(lower));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Convierte cualquier error de mutación en mensajes por campo + mensaje general.
|
|
27
|
+
* Pura salvo por `t`; no muestra toasts.
|
|
28
|
+
*/
|
|
29
|
+
export function mapApiError(err, opts = {}) {
|
|
30
|
+
const t = opts.t ?? defaultTranslate;
|
|
31
|
+
const status = statusOf(err);
|
|
32
|
+
const raw = extractFieldErrors(err);
|
|
33
|
+
if (raw) {
|
|
34
|
+
const fields = localizeFieldErrorMap(raw, t, { labels: opts.labels, language: opts.language });
|
|
35
|
+
return { status, fields, hasFieldErrors: Object.keys(fields).length > 0 };
|
|
36
|
+
}
|
|
37
|
+
// Sin respuesta HTTP (red caída, CORS, excepción): el texto crudo es jerga.
|
|
38
|
+
const noResponse = !err?.response && err instanceof Error;
|
|
39
|
+
const fallback = opts.fallback ?? t('common.error', { defaultValue: 'No se pudo completar la operación' });
|
|
40
|
+
if (noResponse)
|
|
41
|
+
return { status, fields: {}, form: fallback, hasFieldErrors: false };
|
|
42
|
+
const { title, description } = extractServerError(err, fallback);
|
|
43
|
+
const text = [title, description].filter(Boolean).join(' — ');
|
|
44
|
+
if (status === 409 || status === 422) {
|
|
45
|
+
const hint = CONFLICT_HINTS.find((h) => h.re.test(text));
|
|
46
|
+
const field = fieldNamedBy(text, opts.knownFields);
|
|
47
|
+
if (field) {
|
|
48
|
+
const label = opts.labels?.[field] ?? field;
|
|
49
|
+
const msg = hint
|
|
50
|
+
? t('validation.duplicate', { defaultValue: 'Ya existe un registro con ese {{label}}', label })
|
|
51
|
+
: t(title, { defaultValue: title });
|
|
52
|
+
return { status, fields: { [field]: msg }, hasFieldErrors: true };
|
|
53
|
+
}
|
|
54
|
+
const form = hint ? hint.es : t(title, { defaultValue: title });
|
|
55
|
+
return { status, fields: {}, form: description && !hint ? `${form}. ${description}` : form, hasFieldErrors: false };
|
|
56
|
+
}
|
|
57
|
+
const form = t(title, { defaultValue: title });
|
|
58
|
+
return { status, fields: {}, form: description ? `${form}. ${description}` : form, hasFieldErrors: false };
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Locale por defecto del producto. */
|
|
2
|
+
export declare const DEFAULT_LOCALE = "es-MX";
|
|
3
|
+
/** Moneda cuando ni la org ni el llamador dan una (mismo fallback que las celdas). */
|
|
4
|
+
export declare const FALLBACK_CURRENCY = "USD";
|
|
5
|
+
export interface FormatOptions {
|
|
6
|
+
/** BCP-47. Default `es-MX`. */
|
|
7
|
+
locale?: string;
|
|
8
|
+
/** ISO-4217 de la org. Default: contexto → `USD`. */
|
|
9
|
+
currency?: string;
|
|
10
|
+
/** IANA de la org. Default: contexto → zona del navegador. */
|
|
11
|
+
timeZone?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Redondea a centavos evitando el error binario (1.005 → 1.01). */
|
|
14
|
+
export declare function roundMoney(n: number, decimals?: number): number;
|
|
15
|
+
/** Número (string con símbolo/separadores, o number) → number; NaN → 0. */
|
|
16
|
+
export declare function toAmount(v: unknown): number;
|
|
17
|
+
/**
|
|
18
|
+
* Monto con símbolo de la moneda de la org: `formatMoney(1060, {currency:'MXN'})`
|
|
19
|
+
* → `$1,060.00`. Un valor no numérico devuelve `''` (nunca «NaN»).
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatMoney(amount: unknown, opts?: FormatOptions): string;
|
|
22
|
+
/** Número plano localizado (`1,234.5`). */
|
|
23
|
+
export declare function formatQuantity(n: unknown, opts?: {
|
|
24
|
+
locale?: string;
|
|
25
|
+
maximumFractionDigits?: number;
|
|
26
|
+
}): string;
|
|
27
|
+
/**
|
|
28
|
+
* Fecha (`29 sep 2026`). Un `YYYY-MM-DD` es fecha de calendario: se formatea en
|
|
29
|
+
* UTC para que no retroceda un día en zonas con offset negativo.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDate(input: unknown, opts?: FormatOptions & {
|
|
32
|
+
style?: Intl.DateTimeFormatOptions['dateStyle'];
|
|
33
|
+
}): string;
|
|
34
|
+
/** Fecha y hora en la zona de la org (`29 sep 2026, 14:05`). */
|
|
35
|
+
export declare function formatDateTime(input: unknown, opts?: FormatOptions): string;
|
|
36
|
+
export interface Formatter {
|
|
37
|
+
locale: string;
|
|
38
|
+
currency: string;
|
|
39
|
+
timeZone: string | undefined;
|
|
40
|
+
money: (amount: unknown, currency?: string) => string;
|
|
41
|
+
quantity: (n: unknown) => string;
|
|
42
|
+
date: (input: unknown) => string;
|
|
43
|
+
dateTime: (input: unknown) => string;
|
|
44
|
+
}
|
|
45
|
+
/** Formateador ligado a un locale/moneda/zona fijos (para tickets y PDFs fuera de React). */
|
|
46
|
+
export declare function createFormatter(opts?: FormatOptions): Formatter;
|
|
47
|
+
/** Formateador con la moneda y zona de la org tomadas del runtime provider. */
|
|
48
|
+
export declare function useFormatter(overrides?: FormatOptions): Formatter;
|
|
49
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/business/format.ts"],"names":[],"mappings":"AASA,uCAAuC;AACvC,eAAO,MAAM,cAAc,UAAU,CAAA;AACrC,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,QAAQ,CAAA;AAEtC,MAAM,WAAW,aAAa;IAC1B,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAI1D;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAW3C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAa7E;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAMjH;AAeD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,IAAI,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAA;CAAO,GAAG,MAAM,CAOjI;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAS/E;AAED,MAAM,WAAW,SAAS;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IACrD,QAAQ,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAA;IAChC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;CACvC;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,IAAI,GAAE,aAAkB,GAAG,SAAS,CAanE;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,SAAS,GAAE,aAAkB,GAAG,SAAS,CAarE"}
|