@asteby/metacore-runtime-react 49.3.0 → 49.4.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 +12 -0
- package/dist/business/document-editor-panels.d.ts +2 -1
- package/dist/business/document-editor-panels.d.ts.map +1 -1
- package/dist/business/document-editor-panels.js +6 -6
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +23 -9
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-items-editor.d.ts +6 -1
- package/dist/business/line-items-editor.d.ts.map +1 -1
- package/dist/business/line-items-editor.js +109 -83
- package/dist/business/line-product-cell.d.ts +32 -0
- package/dist/business/line-product-cell.d.ts.map +1 -0
- package/dist/business/line-product-cell.js +100 -0
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +20 -12
- package/dist/dynamic-select-field.d.ts +2 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +49 -19
- package/dist/entity-select.d.ts.map +1 -1
- package/dist/entity-select.js +13 -11
- package/dist/field-grid.d.ts +7 -1
- package/dist/field-grid.d.ts.map +1 -1
- package/dist/field-grid.js +4 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/record-picker-actions.d.ts +27 -0
- package/dist/record-picker-actions.d.ts.map +1 -0
- package/dist/record-picker-actions.js +32 -0
- package/package.json +1 -1
- package/src/__tests__/document-editor-inline-selects.test.tsx +202 -0
- package/src/__tests__/document-form-dialog.no-stale-flash.test.tsx +169 -0
- package/src/business/document-editor-panels.tsx +13 -10
- package/src/business/document-editor.tsx +72 -20
- package/src/business/index.ts +1 -0
- package/src/business/line-items-editor.tsx +229 -174
- package/src/business/line-product-cell.tsx +219 -0
- package/src/document-form-dialog.tsx +20 -11
- package/src/dynamic-select-field.tsx +51 -22
- package/src/entity-select.tsx +17 -39
- package/src/field-grid.tsx +15 -1
- package/src/index.ts +2 -0
- package/src/record-picker-actions.tsx +81 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 49.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e95a6a4: DocumentEditor con selects «pro»: el buscador de producto vive en la celda «Descripción» de cada renglón (con un renglón vacío siempre al final, Enter elige y pasa a «Cant.»), los resultados salen en un portal collision-aware que nunca recorta la tabla ni el modal, y los dynamic_select con `ref` traen crear/editar unidos al trigger (`RecordPickerAction`, compartido con `EntitySelect`; el lápiz despacha `metacore:edit-record`). Pulido visual del editor: cabecera agrupada en tres columnas, etiquetas en sentence case, «Cargar desde» segmentado con estado vacío, tabla densa con totales alineados a «Importe», panel de totales con estado vacío y atajo ⌘/Ctrl+Enter para guardar.
|
|
8
|
+
|
|
9
|
+
## 49.3.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 6e6d443: `DocumentFormDialog` reinicia tipo y paso al abrirse durante el render, no en un efecto: el primer frame ya es el DocumentEditor del tipo pedido. Antes, si el host lo dejaba montado mientras cambiaban sus formularios (misma ruta para otra vista) o el `initialType`, al abrir se pintaba un instante el wizard «Nuevo documento» o el editor del tipo anterior (QA Pitsline r6, Nota de crédito).
|
|
14
|
+
|
|
3
15
|
## 49.3.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -44,8 +44,9 @@ export interface TotalsRow {
|
|
|
44
44
|
emphasis?: boolean;
|
|
45
45
|
}
|
|
46
46
|
/** Totales alineados a la derecha; la última fila con `emphasis` es el total. */
|
|
47
|
-
export declare function TotalsPanel({ rows }: {
|
|
47
|
+
export declare function TotalsPanel({ rows, muted }: {
|
|
48
48
|
rows: TotalsRow[];
|
|
49
|
+
muted?: boolean;
|
|
49
50
|
}): import("react").JSX.Element;
|
|
50
51
|
/** Lista de revisión: errores (rojo) y avisos (ámbar). Vacía → no pinta nada. */
|
|
51
52
|
export declare function ValidationChecklist({ issues, title }: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-editor-panels.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-panels.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGvD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEzD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BAYzI;AAED,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,SAAS,CAAA;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,WAAmB,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,uBAAuB,+BA4BtI;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1D,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAC5B,GAAG,EAAE,SAAS,CAAA;IACd,QAAQ,CAAC,EAAE,SAAS,CAAA;CACvB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,cAAc,+BA4B9E;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,EAAE,CAAA;CAAE,+
|
|
1
|
+
{"version":3,"file":"document-editor-panels.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-panels.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGvD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEzD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BAYzI;AAED,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,SAAS,CAAA;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,WAAmB,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,uBAAuB,+BA4BtI;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1D,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAC5B,GAAG,EAAE,SAAS,CAAA;IACd,QAAQ,CAAC,EAAE,SAAS,CAAA;CACvB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,cAAc,+BA4B9E;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,KAAa,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,+BAc1F;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,KAAiC,EAAE,EAAE;IAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,sCAe3H;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;CAAE,sCAO3E;AAED,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC/C,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,GAAG,EAAE,SAAS,CAAA;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,iBAAiB,+BAgC3F"}
|
|
@@ -9,7 +9,7 @@ import { AlertTriangle, ChevronDown, XCircle } from 'lucide-react';
|
|
|
9
9
|
import { computeLine } from './line-items';
|
|
10
10
|
/** Bloque con título discreto y espaciado generoso (sin bordes pesados). */
|
|
11
11
|
export function EditorSection({ title, hint, children, slot }) {
|
|
12
|
-
return (_jsxs("section", { className: "space-y-3", "data-slot": slot, children: [(title || hint) && (_jsxs("header", { className: "flex items-baseline justify-between gap-3", children: [title && _jsx("h3", { className: "text-sm font-semibold", children: title }), hint && _jsx("span", { className: "text-xs text-muted-foreground", children: hint })] })), children] }));
|
|
12
|
+
return (_jsxs("section", { className: "space-y-3", "data-slot": slot, children: [(title || hint) && (_jsxs("header", { className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1", children: [title && _jsx("h3", { className: "whitespace-nowrap text-sm font-semibold", children: title }), hint && _jsx("span", { className: "text-xs text-muted-foreground", children: hint })] })), children] }));
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Sección plegable. El contenido queda MONTADO aunque esté cerrada (`hidden`):
|
|
@@ -24,25 +24,25 @@ export function CollapsibleSection({ title, summary, open, defaultOpen = false,
|
|
|
24
24
|
setInner(!isOpen);
|
|
25
25
|
onOpenChange?.(!isOpen);
|
|
26
26
|
};
|
|
27
|
-
return (_jsxs("section", { className: "rounded-
|
|
27
|
+
return (_jsxs("section", { className: "rounded-xl border border-dashed", "data-slot": slot, "data-state": isOpen ? 'open' : 'closed', children: [_jsxs("button", { type: "button", "aria-expanded": isOpen, "aria-controls": id, onClick: toggle, className: "flex w-full items-center justify-between gap-3 rounded-xl px-4 py-3 text-left text-sm transition-colors hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50", children: [_jsx("span", { className: "font-medium", children: title }), _jsxs("span", { className: "flex min-w-0 items-center gap-2 text-xs text-muted-foreground", children: [!isOpen && summary && _jsx("span", { className: "truncate", children: summary }), _jsx(ChevronDown, { className: `size-4 shrink-0 transition-transform ${isOpen ? 'rotate-180' : ''}`, "aria-hidden": true })] })] }), _jsx("div", { id: id, hidden: !isOpen, className: "space-y-4 px-4 pb-4", children: children })] }));
|
|
28
28
|
}
|
|
29
29
|
/** Tarjeta compacta de la contraparte: nombre, 3-6 datos clave y estado de crédito. */
|
|
30
30
|
export function PartyCard({ name, rows, credit, fmt, children }) {
|
|
31
|
-
return (_jsxs("div", { className: "rounded-
|
|
31
|
+
return (_jsxs("div", { className: "rounded-lg bg-muted/40 px-4 py-3 text-sm", "data-slot": "party-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [name && _jsx("span", { className: "font-medium", children: name }), credit && (_jsx(Badge, { variant: credit.level === 'block' ? 'destructive' : credit.level === 'warn' ? 'secondary' : 'outline', "data-slot": "party-credit", children: credit.holdReason
|
|
32
32
|
? credit.holdReason
|
|
33
33
|
: credit.available != null
|
|
34
34
|
? `Crédito disponible ${fmt.money(credit.available)}`
|
|
35
35
|
: `Saldo ${fmt.money(credit.balance ?? 0)}` }))] }), rows.length > 0 && (_jsx("dl", { className: "mt-2 grid grid-cols-2 gap-x-6 gap-y-1 sm:grid-cols-3", children: rows.map((r) => (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: r.label }), _jsx("dd", { className: "truncate", children: r.value })] }, r.key))) })), children] }));
|
|
36
36
|
}
|
|
37
37
|
/** Totales alineados a la derecha; la última fila con `emphasis` es el total. */
|
|
38
|
-
export function TotalsPanel({ rows }) {
|
|
39
|
-
return (_jsx("dl", { className:
|
|
38
|
+
export function TotalsPanel({ rows, muted = false }) {
|
|
39
|
+
return (_jsx("dl", { className: `ml-auto w-full max-w-xs space-y-1.5 text-sm tabular-nums ${muted ? 'text-muted-foreground' : ''}`, "data-slot": "totals", children: rows.map((r, i) => (_jsxs("div", { className: `flex items-baseline justify-between gap-6 ${r.emphasis ? `${i > 0 ? 'border-t pt-2.5' : ''} text-lg font-semibold` : ''}`, children: [_jsx("dt", { className: r.emphasis ? '' : 'text-muted-foreground', children: r.label }), _jsx("dd", { children: r.value })] }, r.label))) }));
|
|
40
40
|
}
|
|
41
41
|
/** Lista de revisión: errores (rojo) y avisos (ámbar). Vacía → no pinta nada. */
|
|
42
42
|
export function ValidationChecklist({ issues, title = 'Revisa antes de guardar' }) {
|
|
43
43
|
if (issues.length === 0)
|
|
44
44
|
return null;
|
|
45
|
-
return (_jsxs("div", { className: "rounded-md bg-muted/30 px-4 py-3", "data-slot": "validation-checklist", role: "status", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: title }), _jsx("ul", { className: "space-y-1 text-sm", children: issues.map((i, k) => (_jsxs("li", { className: `flex items-start gap-2 ${i.severity === 'error' ? 'text-destructive' : 'text-amber-700'}`, children: [i.severity === 'error' ? _jsx(XCircle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }) : _jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }), i.message] }, `${i.field ?? ''}:${k}`))) })] }));
|
|
45
|
+
return (_jsxs("div", { className: "rounded-md bg-muted/30 px-4 py-3", "data-slot": "validation-checklist", role: "status", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: title }), _jsx("ul", { className: "space-y-1 text-sm", children: issues.map((i, k) => (_jsxs("li", { className: `flex items-start gap-2 ${i.severity === 'error' ? 'text-destructive' : 'text-amber-700 dark:text-amber-400'}`, children: [i.severity === 'error' ? _jsx(XCircle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }) : _jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }), i.message] }, `${i.field ?? ''}:${k}`))) })] }));
|
|
46
46
|
}
|
|
47
47
|
/** Contenido de la vista previa (PDF, HTML o XML, en ese orden de preferencia). */
|
|
48
48
|
export function PreviewPanel({ preview }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-editor.d.ts","sourceRoot":"","sources":["../../src/business/document-editor.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAAqB,KAAK,sBAAsB,EAAwB,MAAM,qBAAqB,CAAA;AAkD1G,OAAO,KAAK,EAAsC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAE3G,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,IAAI,EAAE,gBAAgB,CAAA;IACtB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAID,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,OAAO,EACP,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,KAAK,EACL,UAAkB,GACrB,EAAE,mBAAmB,+
|
|
1
|
+
{"version":3,"file":"document-editor.d.ts","sourceRoot":"","sources":["../../src/business/document-editor.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAAqB,KAAK,sBAAsB,EAAwB,MAAM,qBAAqB,CAAA;AAkD1G,OAAO,KAAK,EAAsC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAE3G,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,IAAI,EAAE,gBAAgB,CAAA;IACtB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAID,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,OAAO,EACP,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,KAAK,EACL,UAAkB,GACrB,EAAE,mBAAmB,+BA4xBrB"}
|
|
@@ -21,7 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
21
21
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
22
22
|
import { useTranslation } from 'react-i18next';
|
|
23
23
|
import { Button } from '@asteby/metacore-ui/primitives';
|
|
24
|
-
import { Loader2 } from 'lucide-react';
|
|
24
|
+
import { Loader2, ReceiptText } from 'lucide-react';
|
|
25
25
|
import { toast } from 'sonner';
|
|
26
26
|
import { useApi } from '../api-context';
|
|
27
27
|
import { useOrgTaxRate, useTimeZone } from '../org-runtime-context';
|
|
@@ -482,7 +482,7 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
482
482
|
setFieldErrors((prev) => clearFieldErrorTree(prev, key));
|
|
483
483
|
};
|
|
484
484
|
const fieldRecord = useMemo(() => seedRecord(seeds, record), [seeds, record]);
|
|
485
|
-
const fieldCell = (field, fullWidth = false) => visible(field) ? (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(withFriendlyOptions(gatedOptions(field, header)), header[field.key], (v) => updateField(field.key, v), header, fieldRecord, fieldErrors), fieldErrors[field.key] && _jsx("p", { className: "mt-1 text-xs text-destructive", children: fieldErrors[field.key] })] }, field.key)) : null;
|
|
485
|
+
const fieldCell = (field, fullWidth = false, className) => visible(field) ? (_jsxs(FieldCell, { fullWidth: fullWidth, className: className, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, tone: "sentence", children: tl(field.label) }), renderField(withFriendlyOptions(gatedOptions(field, header)), header[field.key], (v) => updateField(field.key, v), header, fieldRecord, fieldErrors), fieldErrors[field.key] && _jsx("p", { className: "mt-1 text-xs text-destructive", children: fieldErrors[field.key] })] }, field.key)) : null;
|
|
486
486
|
const summaryRows = partySummaryRows(party, type.party?.summary, {
|
|
487
487
|
...Object.fromEntries(Object.entries(partyLabels).map(([k, v]) => [k, tl(v)])),
|
|
488
488
|
...Object.fromEntries(allFields.map((f) => [f.key, tl(f.label)])),
|
|
@@ -525,21 +525,28 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
525
525
|
// Sin acción de vista previa del servidor, un documento con renglones ofrece
|
|
526
526
|
// la suya local (encabezado, renglones y totales): sin borrador ni red.
|
|
527
527
|
const localPreview = !previewEnabled && !type.preview && !!lineCfg && !isAllocation && kind !== 'credit';
|
|
528
|
-
const essentials = (_jsxs(EditorSection, { slot: "editor-essentials", children: [_jsxs(FieldGrid, { children: [groups.party && fieldCell(groups.party), groups.essential.map((f) => fieldCell(f))] }), party && (_jsx(PartyCard, { name: party.name ?? party.legal_name, rows: summaryRows, credit: credit, fmt: fmt, children: partyContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id))) }))] }));
|
|
529
|
-
const loadFrom = sources.length > 0 && !isAllocation && (_jsx(EditorSection, { slot: "load-from-source", title: t('documentEditor.load_from', { defaultValue: 'Cargar desde…' }), hint: busy === 'source'
|
|
528
|
+
const essentials = (_jsxs(EditorSection, { slot: "editor-essentials", children: [_jsx("div", { className: "@container rounded-xl border bg-card/40 p-4", children: _jsxs(FieldGrid, { className: "gap-x-4 @2xl:grid-cols-3", children: [groups.party && fieldCell(groups.party, false, '@2xl:col-span-2'), groups.essential.map((f) => fieldCell(f))] }) }), party && (_jsx(PartyCard, { name: party.name ?? party.legal_name, rows: summaryRows, credit: credit, fmt: fmt, children: partyContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id))) }))] }));
|
|
529
|
+
const loadFrom = sources.length > 0 && !isAllocation && (_jsx(EditorSection, { slot: "load-from-source", title: t('documentEditor.load_from', { defaultValue: 'Cargar desde…' }), hint: busy === 'source'
|
|
530
|
+
? t('documentEditor.loading_source', { defaultValue: 'Cargando…' })
|
|
531
|
+
: sourceNote ?? (sourceId ? undefined : t('documentEditor.load_from_hint', { defaultValue: 'Opcional · precarga contraparte y renglones' })), children: _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [sources.length > 1 && (_jsx("div", { role: "radiogroup", "aria-label": t('documentEditor.source_kind', { defaultValue: 'Documento de origen' }), className: "inline-flex h-9 shrink-0 items-center gap-0.5 rounded-md border bg-muted/40 p-0.5", children: sources.map((s) => (_jsx("button", { type: "button", role: "radio", "aria-checked": s.key === sourceKey, "data-source-kind": s.key, onClick: () => {
|
|
530
532
|
setSourceKey(s.key);
|
|
531
533
|
setSourceId('');
|
|
532
|
-
}, className: `rounded px-3
|
|
534
|
+
}, className: `h-full rounded-sm px-3 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 ${s.key === sourceKey
|
|
535
|
+
? 'bg-background font-medium text-foreground shadow-xs dark:bg-input/60'
|
|
536
|
+
: 'text-muted-foreground hover:text-foreground'}`, children: tl(s.label) }, s.key))) })), !activeSource && (_jsx("div", { className: "flex h-9 min-w-60 flex-1 items-center rounded-md border border-dashed px-3 text-sm text-muted-foreground", "data-slot": "source-picker-empty", children: t('documentEditor.pick_source_kind', { defaultValue: 'Elige el tipo de documento para buscarlo' }) })), activeSource && (_jsx(SourcePicker, { source: activeSource, value: sourceId, seed: sourceSeed, header: header, label: tl(activeSource.label), onPick: (id) => {
|
|
533
537
|
setSourceKey(activeSource.key);
|
|
534
538
|
setSourceId(id);
|
|
535
539
|
if (id)
|
|
536
540
|
void loadFromSource(activeSource, id);
|
|
537
541
|
} }, activeSource.key))] }) }));
|
|
538
|
-
|
|
542
|
+
// Sin renglones todavía: un estado vacío amable en vez de $0.00 en cada fila.
|
|
543
|
+
const noLines = !isAllocation && !!lineCfg && lines.length === 0;
|
|
544
|
+
const totalsPanel = noLines ? (_jsx(TotalsPanel, { rows: totalsRows.filter((r) => r.emphasis), muted: true })) : (_jsx(TotalsPanel, { rows: totalsRows }));
|
|
545
|
+
const totalsEmpty = noLines && (_jsxs("div", { className: "flex items-start gap-3 text-sm text-muted-foreground", "data-slot": "totals-empty", children: [_jsx(ReceiptText, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }), _jsx("p", { children: t('documentEditor.totals_empty', { defaultValue: 'Aún no hay renglones. Busca un producto en la tabla para ver el desglose.' }) })] }));
|
|
539
546
|
const linesSection = lineCfg && (_jsxs(EditorSection, { slot: isAllocation ? 'editor-allocation' : 'editor-lines', title: linesTitle, hint: isAllocation && openBalance > 0 && amountKey ? (_jsx("button", { type: "button", className: "underline-offset-2 hover:underline", onClick: () => updateField(amountKey, openBalance), children: t('documentEditor.pay_all', { defaultValue: 'Cobrar todo ({{amount}})', amount: fmt.money(openBalance) }) })) : undefined, children: [isAllocation ? (allocPartyId ? (_jsx(PaymentAllocator, { documents: openDocs, result: allocation, value: alloc, onChange: setAlloc, currency: currency, loading: docsLoading, today: today })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: t('documentEditor.pick_party_first', { defaultValue: 'Elige el cliente para ver sus documentos con saldo.' }) }))) : (_jsx(DocumentLinesGrid, { value: lines, onChange: (next) => {
|
|
540
547
|
setLines(next);
|
|
541
548
|
setServerIssues([]);
|
|
542
|
-
}, columns: lineCfg.columns ?? ['discount', 'tax'], priceSource: lineCfg.price_source ?? (kind === 'purchase' ? 'cost' : 'sale'), discountMode: lineCfg.discount_mode, mode: linesFromSourceDoc ? 'from_source' : 'free', policy: kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined, search: search, currency: currency })), !fullscreen && totalsPanel] }));
|
|
549
|
+
}, columns: lineCfg.columns ?? ['discount', 'tax'], priceSource: lineCfg.price_source ?? (kind === 'purchase' ? 'cost' : 'sale'), discountMode: lineCfg.discount_mode, mode: linesFromSourceDoc ? 'from_source' : 'free', policy: kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined, search: search, currency: currency, showTotals: false })), !fullscreen && totalsPanel] }));
|
|
543
550
|
const advanced = (groups.advanced.length > 0 || headerContribs.length > 0) && (_jsxs(CollapsibleSection, { slot: "advanced-options", title: t('documentEditor.advanced', { defaultValue: 'Opciones fiscales' }), summary: advancedSummary || t('documentEditor.advanced_defaults', { defaultValue: 'Valores sugeridos' }), open: advancedOpen, onOpenChange: setAdvancedOpen, children: [groups.advanced.length > 0 && _jsx(FieldGrid, { children: groups.advanced.map((f) => fieldCell(f)) }), headerContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id)))] }));
|
|
544
551
|
const contributedSections = [...bodyContribs, ...sideContribs].map(({ id, component: C }) => (_jsx("section", { "data-contribution": id, children: _jsx(C, { ...contribProps }) }, id)));
|
|
545
552
|
const notes = groups.notes.length > 0 && _jsx(FieldGrid, { children: groups.notes.map((f) => fieldCell(f, true)) });
|
|
@@ -552,8 +559,15 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
552
559
|
return { label: tl(f.label), value: v == null || v === '' ? '' : opt ? friendlyOptionLabel(tl(String(opt.label ?? v))) : seeded ?? String(v) };
|
|
553
560
|
})
|
|
554
561
|
.filter((r) => r.value), lines: lines, totals: totalsRows, fmt: fmt }) })) : null;
|
|
562
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || '');
|
|
555
563
|
const checklist = _jsx(ValidationChecklist, { issues: shownIssues, title: t('documentEditor.review', { defaultValue: 'Revisa antes de guardar' }) });
|
|
556
|
-
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", "data-slot": "document-editor", "data-kind": kind, "data-layout": fullscreen ? 'fullscreen' : 'dialog',
|
|
564
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", "data-slot": "document-editor", "data-kind": kind, "data-layout": fullscreen ? 'fullscreen' : 'dialog', onKeyDown: (e) => {
|
|
565
|
+
// ⌘/Ctrl+Enter guarda desde cualquier campo del editor.
|
|
566
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && busy === null) {
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
void submit();
|
|
569
|
+
}
|
|
570
|
+
}, children: [fullscreen ? (_jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-2 lg:grid lg:grid-cols-[minmax(0,1fr)_320px] lg:gap-6", children: [_jsxs("div", { className: "min-w-0 space-y-6", "data-slot": "editor-main", children: [_jsx(FormErrorBanner, { message: formError }), essentials, loadFrom, linesSection, advanced, contributedSections, notes] }), _jsxs("aside", { className: "mt-6 space-y-4 lg:sticky lg:top-0 lg:mt-0 lg:self-start", "data-slot": "editor-aside", children: [_jsxs("div", { className: "space-y-3 rounded-xl border bg-card/40 p-4", children: [totalsEmpty, totalsPanel] }), checklist, previewSection] })] })) : (_jsxs("div", { className: "-mx-1 min-h-0 flex-1 space-y-6 overflow-y-auto px-1 py-2", children: [_jsx(FormErrorBanner, { message: formError }), essentials, loadFrom, linesSection, advanced, contributedSections, notes, previewSection, checklist] })), _jsxs("footer", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-4", "data-slot": "editor-actions", children: [_jsxs("span", { className: "mr-auto hidden items-center gap-1 text-xs text-muted-foreground sm:inline-flex", "data-slot": "editor-shortcut", children: [_jsx("kbd", { className: "rounded border bg-muted px-1.5 py-0.5 font-mono text-[0.7rem]", children: isMac ? '⌘' : 'Ctrl' }), _jsx("kbd", { className: "rounded border bg-muted px-1.5 py-0.5 font-mono text-[0.7rem]", children: "Enter" }), _jsx("span", { children: t('documentEditor.shortcut_save', { defaultValue: 'para guardar' }) })] }), footerContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id))), _jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, disabled: busy === 'save', children: t('common.cancel', { defaultValue: 'Cancelar' }) }), _jsxs(Button, { type: "button", "data-primary": "true", onClick: () => void submit(), disabled: busy !== null, children: [busy === 'save' && _jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }), type.submit_label ? tl(type.submit_label) : t('common.save', { defaultValue: 'Guardar' })] })] })] }));
|
|
557
571
|
}
|
|
558
572
|
/** Selector del documento origen (mismo widget de relación que el resto de formularios). */
|
|
559
573
|
function SourcePicker({ source, value, seed, header, label, onPick, }) {
|
|
@@ -564,5 +578,5 @@ function SourcePicker({ source, value, seed, header, label, onPick, }) {
|
|
|
564
578
|
ref: source.model,
|
|
565
579
|
option_filter: source.option_filter,
|
|
566
580
|
}), [source, label]);
|
|
567
|
-
return (_jsx("div", { className: "min-w-
|
|
581
|
+
return (_jsx("div", { className: "min-w-60 flex-1", "data-source": source.key, children: renderField(field, value, (v) => onPick(v == null || v === '' ? '' : String(v)), header, seed ? { id: seed.value, label: seed.label } : undefined) }));
|
|
568
582
|
}
|
package/dist/business/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA"}
|
package/dist/business/index.js
CHANGED
|
@@ -40,10 +40,15 @@ export interface DocumentLinesGridProps {
|
|
|
40
40
|
editPermission?: string;
|
|
41
41
|
/** Moneda ISO (default: la de la org). */
|
|
42
42
|
currency?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Pinta el bloque de totales bajo la tabla. Default true; un contenedor que
|
|
45
|
+
* ya muestra sus propios totales (DocumentEditor) lo apaga.
|
|
46
|
+
*/
|
|
47
|
+
showTotals?: boolean;
|
|
43
48
|
}
|
|
44
49
|
/** @deprecated Usa `DocumentLinesGridProps`. Es el mismo contrato. */
|
|
45
50
|
export type LineItemsEditorProps = DocumentLinesGridProps;
|
|
46
|
-
export declare function DocumentLinesGrid({ value, onChange, columns, policy, serverErrors, onValidate, onRequestProduct, search, priceSource, mode, discountMode, quantityStepper, mergeSameProduct, warehouseId, allowSections, readOnly, editPermission, currency, }: DocumentLinesGridProps): import("react").JSX.Element;
|
|
51
|
+
export declare function DocumentLinesGrid({ value, onChange, columns, policy, serverErrors, onValidate, onRequestProduct, search, priceSource, mode, discountMode, quantityStepper, mergeSameProduct, warehouseId, allowSections, readOnly, editPermission, currency, showTotals, }: DocumentLinesGridProps): import("react").JSX.Element;
|
|
47
52
|
/** Mismo componente. El nombre anterior sigue exportado. */
|
|
48
53
|
export declare const LineItemsEditor: typeof DocumentLinesGrid;
|
|
49
54
|
//# sourceMappingURL=line-items-editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"AASA,OAAO,
|
|
1
|
+
{"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"AASA,OAAO,EAIH,KAAK,iBAAiB,EACtB,KAAK,WAAW,EACnB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAKH,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAC3B,MAAM,cAAc,CAAA;AAErB,OAAO,EACH,KAAK,YAAY,EACjB,KAAK,aAAa,EAErB,MAAM,kBAAkB,CAAA;AAEzB,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,KAAK,GAAG,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;AAExF,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,gEAAgE;IAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAA;IACrC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAC7C,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IAC3G,4FAA4F;IAC5F,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,mFAAmF;IACnF,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,oFAAoF;IACpF,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC5C,6DAA6D;IAC7D,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AAIzD,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,OAAsB,EACtB,MAAM,EACN,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,WAAoB,EACpB,IAAa,EACb,YAAwB,EACxB,eAAe,EACf,gBAAuB,EACvB,WAAW,EACX,aAAoB,EACpB,QAAgB,EAChB,cAAc,EACd,QAAQ,EACR,UAAiB,GACpB,EAAE,sBAAsB,+BAwbxB;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,0BAAoB,CAAA"}
|
|
@@ -2,18 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
// DocumentLinesGrid — editor único de renglones (UX-2).
|
|
3
3
|
// `LineItemsEditor` es el mismo componente: las props nuevas son opcionales.
|
|
4
4
|
// Controlado: `value`/`onChange` con `LineItem[]`. Guardar = `serializeLineItems`.
|
|
5
|
-
import { useMemo, useState } from 'react';
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { Heading, Minus, Plus,
|
|
7
|
+
import { Heading, Minus, Plus, StickyNote, Trash2 } from 'lucide-react';
|
|
8
8
|
import { Button, Input } from '@asteby/metacore-ui';
|
|
9
9
|
import { useCan } from '../permissions-context';
|
|
10
10
|
import { useFormatter } from './format';
|
|
11
|
-
import { addProductLine, lineGridKeyCommand, } from './document-lines';
|
|
11
|
+
import { addProductLine, applyProductToLine, lineGridKeyCommand, } from './document-lines';
|
|
12
12
|
import { computeLine, computeTotals, makeLine, validateLineItems, } from './line-items';
|
|
13
|
-
import {
|
|
14
|
-
import { parseProductQuery, } from './product-search';
|
|
13
|
+
import { LineProductCell } from './line-product-cell';
|
|
15
14
|
const num = (s) => (s.trim() === '' ? 0 : Number(s.replace(',', '.')) || 0);
|
|
16
|
-
export function DocumentLinesGrid({ value, onChange, columns = ['discount'], policy, serverErrors, onValidate, onRequestProduct, search, priceSource = 'sale', mode = 'free', discountMode = 'percent', quantityStepper, mergeSameProduct = true, warehouseId, allowSections = true, readOnly = false, editPermission, currency, }) {
|
|
15
|
+
export function DocumentLinesGrid({ value, onChange, columns = ['discount'], policy, serverErrors, onValidate, onRequestProduct, search, priceSource = 'sale', mode = 'free', discountMode = 'percent', quantityStepper, mergeSameProduct = true, warehouseId, allowSections = true, readOnly = false, editPermission, currency, showTotals = true, }) {
|
|
17
16
|
const { t } = useTranslation();
|
|
18
17
|
const can = useCan();
|
|
19
18
|
const fmt = useFormatter({ currency });
|
|
@@ -46,41 +45,91 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
|
|
|
46
45
|
};
|
|
47
46
|
const cell = (i, field) => errors[`${i}.${field}`];
|
|
48
47
|
const warn = (i, field) => validation.warnings[`${i}.${field}`];
|
|
48
|
+
// Foco tras elegir: Enter en el buscador pasa a «Cant.» del renglón que
|
|
49
|
+
// recibió el producto (nuevo o sumado), como un editor de facturas pro.
|
|
50
|
+
const rootRef = useRef(null);
|
|
51
|
+
const [focusQty, setFocusQty] = useState(null);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!focusQty)
|
|
54
|
+
return;
|
|
55
|
+
const el = rootRef.current?.querySelector(`[data-line-key="${focusQty}"] [data-cell="quantity"]`);
|
|
56
|
+
if (el) {
|
|
57
|
+
el.focus();
|
|
58
|
+
el.select?.();
|
|
59
|
+
}
|
|
60
|
+
setFocusQty(null);
|
|
61
|
+
}, [focusQty, value]);
|
|
62
|
+
const blankLine = () => makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {});
|
|
63
|
+
/** Renglón vacío del final: el producto entra como renglón nuevo (o suma al mismo). */
|
|
49
64
|
const pickProduct = (product, variant) => {
|
|
50
|
-
|
|
65
|
+
const next = addProductLine(value, product, variant, { priceSource, warehouseId, mergeSameProduct });
|
|
66
|
+
emit(next);
|
|
67
|
+
const id = variant?.id ?? product.id;
|
|
68
|
+
const target = next.find((l) => l.kind === 'item' && l.product_id === id && !value.includes(l)) ?? next[next.length - 1];
|
|
69
|
+
if (target)
|
|
70
|
+
setFocusQty(target.key);
|
|
71
|
+
};
|
|
72
|
+
/** Renglón libre existente: el producto elegido en su celda llena ESE renglón. */
|
|
73
|
+
const fillLine = (idx, product, variant) => {
|
|
74
|
+
const cur = value[idx];
|
|
75
|
+
if (!cur)
|
|
76
|
+
return;
|
|
77
|
+
const filled = applyProductToLine(cur, product, variant, { priceSource, warehouseId });
|
|
78
|
+
emit(value.map((l, i) => (i === idx ? (discountMode === 'amount' ? { ...filled, discount_kind: 'amount' } : filled) : l)));
|
|
79
|
+
setFocusQty(cur.key);
|
|
80
|
+
};
|
|
81
|
+
/** Texto escrito en el renglón vacío sin elegir producto → renglón libre con esa descripción. */
|
|
82
|
+
const addFreeFromDraft = (description) => {
|
|
83
|
+
const line = { ...blankLine(), description };
|
|
84
|
+
emit([...value, line]);
|
|
85
|
+
setFocusQty(line.key);
|
|
51
86
|
};
|
|
52
|
-
|
|
87
|
+
// Ancho mínimo = columnas fijas + un mínimo legible para «Descripción».
|
|
88
|
+
const minWidth = 260 +
|
|
89
|
+
(stepper ? 144 : 96) +
|
|
90
|
+
112 +
|
|
91
|
+
112 +
|
|
92
|
+
40 +
|
|
93
|
+
(show('discount') ? (discountMode === 'both' ? 128 : 96) : 0) +
|
|
94
|
+
(show('tax') ? 64 : 0) +
|
|
95
|
+
(show('unit') ? 80 : 0) +
|
|
96
|
+
(show('technician') ? 128 : 0) +
|
|
97
|
+
(show('lot') ? 112 : 0) +
|
|
98
|
+
(show('dot') ? 96 : 0);
|
|
99
|
+
const colCount = 1 + ['unit', 'discount', 'tax', 'technician', 'lot', 'dot'].filter(show).length + 4;
|
|
100
|
+
const descKeys = (e, i, l) => {
|
|
101
|
+
const cmd = lineGridKeyCommand(e.key, {
|
|
102
|
+
cellEmpty: !l.description.trim() && !l.product_id,
|
|
103
|
+
mode,
|
|
104
|
+
});
|
|
105
|
+
if (cmd === 'delete') {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
remove(i);
|
|
108
|
+
}
|
|
109
|
+
else if (cmd === 'add') {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
emit([...value, blankLine()]);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return (_jsxs("div", { ref: rootRef, "data-slot": "line-items-editor", "data-component": "DocumentLinesGrid", "data-mode": mode, className: "space-y-2", children: [_jsx("div", { className: "overflow-x-auto rounded-lg border bg-card/40", children: _jsxs("table", { className: "w-full table-fixed border-collapse text-sm", style: { minWidth }, children: [_jsx("thead", { className: "border-b bg-muted/40 text-left text-xs font-medium text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 font-medium", children: t('lineItems.description', { defaultValue: 'Descripción' }) }), show('unit') && _jsx("th", { className: "w-20 px-3 py-2 font-medium", children: t('lineItems.unit', { defaultValue: 'Unidad' }) }), _jsx("th", { className: (stepper ? 'w-36' : 'w-24') + ' px-3 py-2 text-right font-medium', children: t('lineItems.quantity', { defaultValue: 'Cant.' }) }), _jsx("th", { className: "w-28 px-3 py-2 text-right font-medium", children: priceSource === 'cost'
|
|
53
115
|
? t('lineItems.unitCost', { defaultValue: 'Costo unitario' })
|
|
54
|
-
: t('lineItems.unitPrice', { defaultValue: 'Precio' }) }), show('discount') && (_jsx("th", { className:
|
|
116
|
+
: t('lineItems.unitPrice', { defaultValue: 'Precio' }) }), show('discount') && (_jsx("th", { className: (discountMode === 'both' ? 'w-32' : 'w-24') + ' px-3 py-2 text-right font-medium', children: discountMode === 'amount'
|
|
55
117
|
? t('lineItems.discountAmount', { defaultValue: 'Desc.' })
|
|
56
|
-
: t('lineItems.discount', { defaultValue: 'Desc. %' }) })), show('tax') && _jsx("th", { className: "w-
|
|
118
|
+
: t('lineItems.discount', { defaultValue: 'Desc. %' }) })), show('tax') && _jsx("th", { className: "w-16 px-3 py-2 text-right font-medium", children: t('lineItems.tax', { defaultValue: 'IVA %' }) }), show('technician') && _jsx("th", { className: "w-32 px-3 py-2 font-medium", children: t('lineItems.technician', { defaultValue: 'Técnico' }) }), show('lot') && _jsx("th", { className: "w-28 px-3 py-2 font-medium", children: t('lineItems.lot', { defaultValue: 'Lote' }) }), show('dot') && _jsx("th", { className: "w-24 px-3 py-2 font-medium", children: "DOT" }), _jsx("th", { className: "w-28 px-3 py-2 text-right font-medium", children: t('lineItems.amount', { defaultValue: 'Importe' }) }), _jsx("th", { className: "w-10", children: _jsx("span", { className: "sr-only", children: t('common.actions', { defaultValue: 'Acciones' }) }) })] }) }), _jsxs("tbody", { children: [value.map((l, i) => {
|
|
57
119
|
if (l.kind !== 'item') {
|
|
58
|
-
return (_jsx("tr", { className: "border-t bg-muted/30", children: _jsxs("td", { colSpan: 99, className:
|
|
120
|
+
return (_jsx("tr", { className: "group border-t bg-muted/30", "data-line-key": l.key, children: _jsxs("td", { colSpan: 99, className: CELL, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-invalid": !!cell(i, 'description'), placeholder: l.kind === 'section' ? t('lineItems.sectionPlaceholder', { defaultValue: 'Sección' }) : t('lineItems.notePlaceholder', { defaultValue: 'Nota' }), className: 'h-8 ' + (l.kind === 'section' ? 'font-semibold' : 'italic'), onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => {
|
|
59
121
|
const cmd = lineGridKeyCommand(e.key, { cellEmpty: !l.description.trim(), mode });
|
|
60
122
|
if (cmd === 'delete') {
|
|
61
123
|
e.preventDefault();
|
|
62
124
|
remove(i);
|
|
63
125
|
}
|
|
64
|
-
} }), canAdd && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) }))] }), cell(i, 'description') && _jsx("p", { className: "mt-1 text-xs text-destructive", children: cell(i, 'description') })] }) }, l.key));
|
|
126
|
+
} }), canAdd && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", className: DELETE, onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) }))] }), cell(i, 'description') && _jsx("p", { className: "mt-1 text-xs text-destructive", children: cell(i, 'description') })] }) }, l.key));
|
|
65
127
|
}
|
|
66
128
|
const shown = view[i] ?? l;
|
|
67
129
|
const a = computeLine(shown);
|
|
68
130
|
const kind = shown.discount_kind ?? 'percent';
|
|
69
131
|
const qtyMax = l.max_quantity;
|
|
70
|
-
return (_jsxs("tr", { className: "border-t align-top", "data-line-key": l.key, children: [_jsxs("td", { className:
|
|
71
|
-
const cmd = lineGridKeyCommand(e.key, {
|
|
72
|
-
cellEmpty: !l.description.trim() && !l.product_id,
|
|
73
|
-
mode,
|
|
74
|
-
});
|
|
75
|
-
if (cmd === 'delete') {
|
|
76
|
-
e.preventDefault();
|
|
77
|
-
remove(i);
|
|
78
|
-
}
|
|
79
|
-
else if (cmd === 'add') {
|
|
80
|
-
e.preventDefault();
|
|
81
|
-
emit([...value, makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {})]);
|
|
82
|
-
}
|
|
83
|
-
} }), (l.sku || l.unit) && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [l.sku, !show('unit') ? l.unit : ''].filter(Boolean).join(' · ') })), cell(i, 'description') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'description') })] }), show('unit') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.unit ?? '', disabled: locked, onChange: (e) => patch(i, { unit: e.target.value }) }) })), _jsxs("td", { className: "px-2 py-1.5", children: [stepper ? (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { type: "button", size: "icon", variant: "outline", className: "size-7", disabled: locked || toNum(l.quantity) <= (fromSource ? 0 : 1), "aria-label": t('lineItems.decreaseQty', { defaultValue: 'Disminuir cantidad' }), onClick: () => patch(i, { quantity: Math.max(fromSource ? 0 : 1, toNum(l.quantity) - 1) }), children: _jsx(Minus, { className: "size-3" }) }), _jsx(Input, { inputMode: "decimal", className: "h-8 w-14 text-right", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => {
|
|
132
|
+
return (_jsxs("tr", { className: "group border-t align-top transition-colors hover:bg-muted/30 focus-within:bg-muted/20", "data-line-key": l.key, children: [_jsxs("td", { className: CELL, children: [search && canAdd && !l.product_id ? (_jsx(LineProductCell, { search: search, text: l.description, onTextChange: (text) => patch(i, { description: text }), onPick: (p, v) => fillLine(i, p, v), onKeyDownClosed: (e) => descKeys(e, i, l), warehouseId: warehouseId, currency: currency, ariaLabel: t('lineItems.description', { defaultValue: 'Descripción' }), invalid: !!cell(i, 'description'), dataCell: "description" })) : (_jsx(Input, { value: l.description, disabled: locked, className: INPUT, "data-cell": "description", "aria-label": t('lineItems.description', { defaultValue: 'Descripción' }), "aria-invalid": !!cell(i, 'description'), onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => descKeys(e, i, l) })), (l.sku || l.unit) && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [l.sku, !show('unit') ? l.unit : ''].filter(Boolean).join(' · ') })), cell(i, 'description') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'description') })] }), show('unit') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.unit ?? '', disabled: locked, onChange: (e) => patch(i, { unit: e.target.value }) }) })), _jsxs("td", { className: CELL, children: [stepper ? (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { type: "button", size: "icon", variant: "outline", className: "size-7", disabled: locked || toNum(l.quantity) <= (fromSource ? 0 : 1), "aria-label": t('lineItems.decreaseQty', { defaultValue: 'Disminuir cantidad' }), onClick: () => patch(i, { quantity: Math.max(fromSource ? 0 : 1, toNum(l.quantity) - 1) }), children: _jsx(Minus, { className: "size-3" }) }), _jsx(Input, { inputMode: "decimal", className: "h-8 w-14 text-right tabular-nums", "data-cell": "quantity", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => {
|
|
84
133
|
let q = num(e.target.value);
|
|
85
134
|
if (qtyMax != null && q > qtyMax)
|
|
86
135
|
q = qtyMax;
|
|
@@ -90,72 +139,49 @@ export function DocumentLinesGrid({ value, onChange, columns = ['discount'], pol
|
|
|
90
139
|
} }), _jsx(Button, { type: "button", size: "icon", variant: "outline", className: "size-7", disabled: locked || (qtyMax != null && toNum(l.quantity) >= qtyMax), "aria-label": t('lineItems.increaseQty', { defaultValue: 'Aumentar cantidad' }), onClick: () => {
|
|
91
140
|
const next = toNum(l.quantity) + 1;
|
|
92
141
|
patch(i, { quantity: qtyMax != null ? Math.min(next, qtyMax) : next });
|
|
93
|
-
}, children: _jsx(Plus, { className: "size-3" }) })] })) : (_jsx(Input, { inputMode: "decimal", className: "
|
|
94
|
-
}), value.length === 0 && !search && (_jsx("tr", { children: _jsx("td", { colSpan: 99, className: "px-3 py-6 text-center text-muted-foreground", children: t('lineItems.empty', { defaultValue: 'Aún no hay renglones. Agrega un producto o un renglón libre.' }) }) })), canAdd && search && (_jsx(
|
|
142
|
+
}, children: _jsx(Plus, { className: "size-3" }) })] })) : (_jsx(Input, { inputMode: "decimal", className: NUM, "data-cell": "quantity", value: String(l.quantity), disabled: locked, "aria-label": t('lineItems.quantity', { defaultValue: 'Cant.' }), "aria-invalid": !!cell(i, 'quantity'), onChange: (e) => patch(i, { quantity: num(e.target.value) }) })), cell(i, 'quantity') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'quantity') }), !cell(i, 'quantity') && warn(i, 'quantity') && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: warn(i, 'quantity') })] }), _jsxs("td", { className: CELL, children: [_jsx(Input, { inputMode: "decimal", className: NUM, value: String(l.unit_price), disabled: locked, "aria-label": priceSource === 'cost' ? t('lineItems.unitCost', { defaultValue: 'Costo unitario' }) : t('lineItems.unitPrice', { defaultValue: 'Precio' }), "aria-invalid": !!cell(i, 'unit_price'), onChange: (e) => patch(i, { unit_price: num(e.target.value) }) }), l.catalog_price != null && (_jsxs("p", { className: "mt-0.5 truncate whitespace-nowrap text-right text-xs text-muted-foreground", children: [t('lineItems.catalogPrice', { defaultValue: 'Catálogo' }), " ", fmt.money(l.catalog_price)] })), cell(i, 'unit_price') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'unit_price') }), !cell(i, 'unit_price') && warn(i, 'unit_price') && _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: warn(i, 'unit_price') })] }), show('discount') && (_jsxs("td", { className: CELL, children: [_jsxs("div", { className: "flex items-center gap-1", children: [discountMode === 'both' && (_jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "h-8 px-1.5", "aria-label": t('lineItems.discountKind', { defaultValue: 'Tipo de descuento' }), onClick: () => patch(i, { discount_kind: kind === 'amount' ? 'percent' : 'amount', discount: 0 }), children: kind === 'amount' ? '$' : '%' })), _jsx(Input, { inputMode: "decimal", className: NUM, value: String(l.discount), disabled: locked, "aria-invalid": !!cell(i, 'discount'), onChange: (e) => patch(i, { discount: num(e.target.value), discount_kind: kind }) })] }), cell(i, 'discount') && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: cell(i, 'discount') })] })), show('tax') && (_jsx("td", { className: CELL, children: _jsx(Input, { inputMode: "decimal", className: NUM, value: String(Math.round(l.tax_rate * 10000) / 100), disabled: locked, onChange: (e) => patch(i, { tax_rate: num(e.target.value) / 100 }) }) })), show('technician') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.technician_id ?? '', disabled: locked, onChange: (e) => patch(i, { technician_id: e.target.value }) }) })), show('lot') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.lot ?? '', disabled: locked, onChange: (e) => patch(i, { lot: e.target.value }) }) })), show('dot') && (_jsx("td", { className: CELL, children: _jsx(Input, { className: INPUT, value: l.dot ?? '', disabled: locked, onChange: (e) => patch(i, { dot: e.target.value }) }) })), _jsx("td", { className: "px-3 py-1.5 text-right align-top font-medium tabular-nums", "data-slot": "line-amount", children: _jsx("span", { className: "inline-flex h-8 items-center", children: fmt.money(a.net) }) }), _jsx("td", { className: "px-1 py-1.5 align-top", children: canAdd && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", className: DELETE, onClick: () => remove(i), "aria-label": t('common.delete', { defaultValue: 'Eliminar' }), children: _jsx(Trash2, { className: "size-4" }) })) })] }, l.key));
|
|
143
|
+
}), value.length === 0 && !search && (_jsx("tr", { children: _jsx("td", { colSpan: 99, className: "px-3 py-6 text-center text-muted-foreground", children: t('lineItems.empty', { defaultValue: 'Aún no hay renglones. Agrega un producto o un renglón libre.' }) }) })), canAdd && search && (_jsx(DraftRow, { search: search, warehouseId: warehouseId, currency: currency, onPick: pickProduct, onFree: addFreeFromDraft, onAddBlank: () => emit([...value, blankLine()]), trailingCells: colCount - 1 }))] }), showTotals && (_jsxs("tfoot", { className: "border-t bg-muted/20 text-sm tabular-nums", "data-slot": "line-totals", children: [_jsx(TotalRow, { span: colCount - 2, label: t('lineItems.subtotal', { defaultValue: 'Subtotal' }), value: fmt.money(totals.subtotal) }), totals.discount > 0 && _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.discountTotal', { defaultValue: 'Descuento' }), value: `-${fmt.money(totals.discount)}` }), totals.tax > 0 && _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.taxTotal', { defaultValue: 'Impuestos' }), value: fmt.money(totals.tax), slot: "tax-total" }), _jsx(TotalRow, { span: colCount - 2, label: t('lineItems.total', { defaultValue: 'Total' }), value: fmt.money(totals.total), slot: "grand-total", strong: true })] }))] }) }), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form }), undo && canAdd && (_jsxs("p", { className: "text-sm text-muted-foreground", children: [t('lineItems.removed', { defaultValue: 'Renglón eliminado.' }), ' ', _jsx("button", { type: "button", className: "underline", onClick: () => {
|
|
95
144
|
emit(undo);
|
|
96
145
|
setUndo(null);
|
|
97
|
-
}, children: t('lineItems.undo', { defaultValue: 'Deshacer' }) })] })),
|
|
146
|
+
}, children: t('lineItems.undo', { defaultValue: 'Deshacer' }) })] })), _jsx("div", { className: "flex flex-wrap items-center gap-3", children: canAdd ? (_jsxs("div", { className: "flex flex-wrap gap-1", children: [onRequestProduct && !search && (_jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: onRequestProduct, children: [_jsx(Plus, { className: "mr-1 size-4" }), t('lineItems.addProduct', { defaultValue: 'Agregar producto' })] })), _jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, blankLine()]), children: [_jsx(Plus, { className: "mr-1 size-4" }), t('lineItems.addLine', { defaultValue: 'Renglón libre' })] }), allowSections && (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, makeLine({ kind: 'section' })]), children: [_jsx(Heading, { className: "mr-1 size-4" }), t('lineItems.addSection', { defaultValue: 'Sección' })] }), _jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => emit([...value, makeLine({ kind: 'note' })]), children: [_jsx(StickyNote, { className: "mr-1 size-4" }), t('lineItems.addNote', { defaultValue: 'Nota' })] })] }))] })) : null })] }));
|
|
98
147
|
}
|
|
99
148
|
/** Mismo componente. El nombre anterior sigue exportado. */
|
|
100
149
|
export const LineItemsEditor = DocumentLinesGrid;
|
|
101
150
|
function toNum(n) {
|
|
102
151
|
return Number.isFinite(n) ? n : 0;
|
|
103
152
|
}
|
|
104
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Renglón vacío del final, siempre listo para buscar: el buscador vive en la
|
|
155
|
+
* celda «Descripción» (no en una barra a todo lo ancho). Elegir agrega el
|
|
156
|
+
* producto; Enter con texto sin coincidencias lo agrega como renglón libre.
|
|
157
|
+
*/
|
|
158
|
+
function DraftRow({ search, warehouseId, currency, onPick, onFree, onAddBlank, trailingCells, }) {
|
|
105
159
|
const { t } = useTranslation();
|
|
106
|
-
const fmt = useFormatter({ currency });
|
|
107
160
|
const [text, setText] = useState('');
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const choose = (idx) => {
|
|
123
|
-
const hit = flat[idx];
|
|
124
|
-
if (!hit)
|
|
125
|
-
return;
|
|
126
|
-
onPick(hit.product, hit.variant);
|
|
127
|
-
setText('');
|
|
128
|
-
setActive(0);
|
|
129
|
-
};
|
|
130
|
-
return (_jsx("tr", { className: "border-t", children: _jsx("td", { colSpan: 99, className: "px-2 py-1.5", children: _jsxs("div", { className: "relative", 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", value: text, role: "combobox", "aria-expanded": open, "aria-label": t('lineItems.searchProduct', { defaultValue: 'Buscar producto' }), placeholder: t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), onChange: (e) => {
|
|
131
|
-
setText(e.target.value);
|
|
132
|
-
setActive(0);
|
|
133
|
-
}, onKeyDown: (e) => {
|
|
134
|
-
if (e.key === 'ArrowDown' && open) {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
setActive((n) => Math.min(flat.length - 1, n + 1));
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (e.key === 'ArrowUp' && open) {
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
setActive((n) => Math.max(0, n - 1));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const cmd = lineGridKeyCommand(e.key, { suggestionsOpen: open, cellEmpty: !text.trim(), mode: 'free' });
|
|
145
|
-
if (e.key === 'Enter' && open) {
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
choose(active);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (cmd === 'add' && !text.trim()) {
|
|
151
|
-
e.preventDefault();
|
|
152
|
-
onAddFree();
|
|
153
|
-
}
|
|
154
|
-
} }), open && (_jsx("ul", { role: "listbox", className: "absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover shadow-md", children: flat.map((hit, idx) => {
|
|
155
|
-
const price = priceOf(hit.product, hit.variant, warehouseId);
|
|
156
|
-
return (_jsx("li", { role: "option", "aria-selected": idx === active, children: _jsxs("button", { type: "button", className: 'flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left text-sm hover:bg-accent ' + (idx === active ? 'bg-accent' : ''), onMouseDown: (e) => e.preventDefault(), onClick: () => choose(idx), children: [_jsxs("span", { children: [_jsx("span", { className: "block font-medium", children: hit.variant ? `${hit.product.name} · ${hit.variant.label}` : hit.product.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: hit.variant?.sku ?? hit.product.sku })] }), price != null && _jsx("span", { className: "tabular-nums", children: fmt.money(price) })] }) }, `${hit.product.id}:${hit.variant?.id ?? ''}`));
|
|
157
|
-
}) })), loading && _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) })] }) }) }));
|
|
161
|
+
return (_jsxs("tr", { className: "border-t border-dashed", "data-slot": "line-draft-row", children: [_jsx("td", { className: CELL, children: _jsx(LineProductCell, { search: search, text: text, onTextChange: setText, onPick: (p, v) => {
|
|
162
|
+
setText('');
|
|
163
|
+
onPick(p, v);
|
|
164
|
+
}, onKeyDownClosed: (e) => {
|
|
165
|
+
if (e.key !== 'Enter')
|
|
166
|
+
return;
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const typed = text.trim();
|
|
169
|
+
setText('');
|
|
170
|
+
if (typed)
|
|
171
|
+
onFree(typed);
|
|
172
|
+
else
|
|
173
|
+
onAddBlank();
|
|
174
|
+
}, warehouseId: warehouseId, currency: currency, ariaLabel: t('lineItems.searchProduct', { defaultValue: 'Buscar producto' }), placeholder: t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), className: "border-dashed bg-transparent shadow-none", dataCell: "search" }) }), Array.from({ length: trailingCells }, (_, i) => (_jsx("td", { "aria-hidden": true }, i)))] }));
|
|
158
175
|
}
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
/** Fila de totales alineada a la columna «Importe». */
|
|
177
|
+
function TotalRow({ span, label, value, slot, strong }) {
|
|
178
|
+
return (_jsxs("tr", { className: strong ? 'font-semibold' : 'text-muted-foreground', children: [_jsx("td", { colSpan: span, className: "px-3 py-1 text-right", children: label }), _jsx("td", { className: 'px-3 py-1 text-right ' + (strong ? 'text-base text-foreground' : 'text-foreground'), "data-slot": slot, children: value }), _jsx("td", {})] }));
|
|
161
179
|
}
|
|
180
|
+
/** Borrar renglón: aparece al pasar o enfocar la fila (siempre visible en móvil). */
|
|
181
|
+
const DELETE = 'size-8 text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 max-sm:opacity-100';
|
|
182
|
+
/** Celda densa: misma altura en todos los renglones. */
|
|
183
|
+
const CELL = 'px-2 py-1.5 align-top';
|
|
184
|
+
/** Inputs de celda: un poco más bajos que los del formulario (densidad de editor). */
|
|
185
|
+
const INPUT = 'h-8';
|
|
186
|
+
/** Numéricos: alineados a la derecha y con cifras tabulares. */
|
|
187
|
+
const NUM = 'h-8 text-right tabular-nums';
|