@asteby/metacore-runtime-react 49.3.1 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/business/document-editor-panels.d.ts +2 -1
  3. package/dist/business/document-editor-panels.d.ts.map +1 -1
  4. package/dist/business/document-editor-panels.js +6 -6
  5. package/dist/business/document-editor.d.ts.map +1 -1
  6. package/dist/business/document-editor.js +23 -9
  7. package/dist/business/index.d.ts +1 -0
  8. package/dist/business/index.d.ts.map +1 -1
  9. package/dist/business/index.js +1 -0
  10. package/dist/business/line-items-editor.d.ts +6 -1
  11. package/dist/business/line-items-editor.d.ts.map +1 -1
  12. package/dist/business/line-items-editor.js +109 -83
  13. package/dist/business/line-product-cell.d.ts +32 -0
  14. package/dist/business/line-product-cell.d.ts.map +1 -0
  15. package/dist/business/line-product-cell.js +100 -0
  16. package/dist/dynamic-select-field.d.ts +2 -1
  17. package/dist/dynamic-select-field.d.ts.map +1 -1
  18. package/dist/dynamic-select-field.js +49 -19
  19. package/dist/entity-select.d.ts.map +1 -1
  20. package/dist/entity-select.js +13 -11
  21. package/dist/field-grid.d.ts +7 -1
  22. package/dist/field-grid.d.ts.map +1 -1
  23. package/dist/field-grid.js +4 -2
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/record-picker-actions.d.ts +27 -0
  28. package/dist/record-picker-actions.d.ts.map +1 -0
  29. package/dist/record-picker-actions.js +32 -0
  30. package/package.json +1 -1
  31. package/src/__tests__/document-editor-inline-selects.test.tsx +202 -0
  32. package/src/business/document-editor-panels.tsx +13 -10
  33. package/src/business/document-editor.tsx +72 -20
  34. package/src/business/index.ts +1 -0
  35. package/src/business/line-items-editor.tsx +229 -174
  36. package/src/business/line-product-cell.tsx +219 -0
  37. package/src/dynamic-select-field.tsx +51 -22
  38. package/src/entity-select.tsx +17 -39
  39. package/src/field-grid.tsx +15 -1
  40. package/src/index.ts +2 -0
  41. package/src/record-picker-actions.tsx +81 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 49.3.1
4
10
 
5
11
  ### Patch 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,+BAW1D;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"}
1
+ {"version":3,"file":"document-editor-panels.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-panels.tsx"],"names":[],"mappings":"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"}
@@ -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-lg 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 px-4 py-3 text-left text-sm", 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 })] }));
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-md 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
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: "ml-auto w-full max-w-xs space-y-1 text-sm tabular-nums", "data-slot": "totals", children: rows.map((r) => (_jsxs("div", { className: `flex justify-between gap-6 ${r.emphasis ? 'border-t pt-2 text-base font-semibold' : ''}`, children: [_jsx("dt", { className: r.emphasis ? '' : 'text-muted-foreground', children: r.label }), _jsx("dd", { children: r.value })] }, r.label))) }));
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,+BAwuBrB"}
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' ? t('documentEditor.loading_source', { defaultValue: 'Cargando…' }) : sourceNote, 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 flex-wrap gap-1 rounded-md border p-1", children: sources.map((s) => (_jsx("button", { type: "button", role: "radio", "aria-checked": s.key === sourceKey, "data-source-kind": s.key, onClick: () => {
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 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${s.key === sourceKey ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-accent'}`, children: tl(s.label) }, s.key))) })), activeSource && (_jsx(SourcePicker, { source: activeSource, value: sourceId, seed: sourceSeed, header: header, label: tl(activeSource.label), onPick: (id) => {
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
- const totalsPanel = _jsx(TotalsPanel, { rows: totalsRows });
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', 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)_340px] lg:gap-8", children: [_jsxs("div", { className: "min-w-0 space-y-8", "data-slot": "editor-main", children: [_jsx(FormErrorBanner, { message: formError }), essentials, loadFrom, linesSection, advanced, contributedSections, notes] }), _jsxs("aside", { className: "mt-8 space-y-6 lg:sticky lg:top-0 lg:mt-0 lg:self-start", "data-slot": "editor-aside", children: [_jsx("div", { className: "rounded-lg border p-4", children: totalsPanel }), checklist, previewSection] })] })) : (_jsxs("div", { className: "-mx-1 min-h-0 flex-1 space-y-8 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: [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' })] })] })] }));
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-[240px] 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) }));
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
  }
@@ -4,6 +4,7 @@ export * from './feedback';
4
4
  export * from './line-items';
5
5
  export * from './document-lines';
6
6
  export * from './line-items-editor';
7
+ export * from './line-product-cell';
7
8
  export * from './payment';
8
9
  export * from './payment-capture';
9
10
  export * from './refund';
@@ -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"}
@@ -4,6 +4,7 @@ export * from './feedback';
4
4
  export * from './line-items';
5
5
  export * from './document-lines';
6
6
  export * from './line-items-editor';
7
+ export * from './line-product-cell';
7
8
  export * from './payment';
8
9
  export * from './payment-capture';
9
10
  export * from './refund';
@@ -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,EAGH,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,EAEH,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;CACpB;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,GACX,EAAE,sBAAsB,+BAwWxB;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,0BAAoB,CAAA"}
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, Search, StickyNote, Trash2 } from 'lucide-react';
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 { useAsyncSearch } from './use-async-search';
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
- emit(addProductLine(value, product, variant, { priceSource, warehouseId, mergeSameProduct }));
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
- return (_jsxs("div", { "data-slot": "line-items-editor", "data-component": "DocumentLinesGrid", "data-mode": mode, className: "space-y-2", children: [_jsx("div", { className: "overflow-x-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-left text-xs text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-2 py-2", children: t('lineItems.description', { defaultValue: 'Descripción' }) }), show('unit') && _jsx("th", { className: "w-20 px-2 py-2", children: t('lineItems.unit', { defaultValue: 'Unidad' }) }), _jsx("th", { className: "w-36 px-2 py-2 text-right", children: t('lineItems.quantity', { defaultValue: 'Cant.' }) }), _jsx("th", { className: "w-32 px-2 py-2 text-right", children: priceSource === 'cost'
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: "w-28 px-2 py-2 text-right", children: discountMode === 'amount'
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-20 px-2 py-2 text-right", children: t('lineItems.tax', { defaultValue: 'IVA %' }) }), show('technician') && _jsx("th", { className: "w-32 px-2 py-2", children: t('lineItems.technician', { defaultValue: 'Técnico' }) }), show('lot') && _jsx("th", { className: "w-28 px-2 py-2", children: t('lineItems.lot', { defaultValue: 'Lote' }) }), show('dot') && _jsx("th", { className: "w-24 px-2 py-2", children: "DOT" }), _jsx("th", { className: "w-32 px-2 py-2 text-right", children: t('lineItems.amount', { defaultValue: 'Importe' }) }), _jsx("th", { className: "w-10" })] }) }), _jsxs("tbody", { children: [value.map((l, i) => {
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: "px-2 py-1.5", 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: l.kind === 'section' ? 'font-semibold' : 'italic', onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => {
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: "px-2 py-1.5", children: [_jsx(Input, { value: l.description, disabled: locked, "aria-label": t('lineItems.description', { defaultValue: 'Descripción' }), "aria-invalid": !!cell(i, 'description'), onChange: (e) => patch(i, { description: e.target.value }), onKeyDown: (e) => {
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: "text-right", 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: "px-2 py-1.5", children: [_jsx(Input, { inputMode: "decimal", className: "text-right", 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 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: "px-2 py-1.5", 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: "text-right", 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: "px-2 py-1.5", children: _jsx(Input, { inputMode: "decimal", className: "text-right", 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: "px-2 py-1.5", children: _jsx(Input, { value: l.technician_id ?? '', disabled: locked, onChange: (e) => patch(i, { technician_id: e.target.value }) }) })), show('lot') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.lot ?? '', disabled: locked, onChange: (e) => patch(i, { lot: e.target.value }) }) })), show('dot') && (_jsx("td", { className: "px-2 py-1.5", children: _jsx(Input, { value: l.dot ?? '', disabled: locked, onChange: (e) => patch(i, { dot: e.target.value }) }) })), _jsx("td", { className: "px-2 py-2 text-right tabular-nums", "data-slot": "line-amount", children: fmt.money(a.net) }), _jsx("td", { className: "px-1 py-1.5", children: 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" }) })) })] }, l.key));
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(SearchRow, { search: search, warehouseId: warehouseId, currency: currency, onPick: pickProduct, onAddFree: () => emit([...value, makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {})]) }))] })] }) }), 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: () => {
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' }) })] })), _jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [canAdd ? (_jsxs("div", { className: "flex flex-wrap gap-2", 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: "outline", onClick: () => emit([...value, makeLine(discountMode === 'amount' ? { discount_kind: 'amount' } : {})]), 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' })] })] }))] })) : (_jsx("span", {})), _jsxs("dl", { className: "min-w-56 space-y-0.5 text-sm tabular-nums", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.subtotal', { defaultValue: 'Subtotal' }) }), _jsx("dd", { children: fmt.money(totals.subtotal) })] }), totals.discount > 0 && _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.discountTotal', { defaultValue: 'Descuento' }) }), _jsxs("dd", { children: ["-", fmt.money(totals.discount)] })] }), totals.tax > 0 && _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("dt", { className: "text-muted-foreground", children: t('lineItems.taxTotal', { defaultValue: 'Impuestos' }) }), _jsx("dd", { "data-slot": "tax-total", children: fmt.money(totals.tax) })] }), _jsxs("div", { className: "flex justify-between gap-6 font-semibold", children: [_jsx("dt", { children: t('lineItems.total', { defaultValue: 'Total' }) }), _jsx("dd", { "data-slot": "grand-total", children: fmt.money(totals.total) })] })] })] })] }));
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
- function SearchRow({ search, warehouseId, currency, onPick, onAddFree, }) {
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
- const [active, setActive] = useState(0);
109
- const parsed = parseProductQuery(text);
110
- const run = useMemo(() => {
111
- return (q, signal) => {
112
- const p = parseProductQuery(q);
113
- return p.kind === 'empty' ? Promise.resolve([]) : search(p, signal);
114
- };
115
- }, [search]);
116
- const { results, loading } = useAsyncSearch(text, run, {
117
- minChars: parsed.kind === 'barcode' ? 8 : 2,
118
- delay: parsed.kind === 'barcode' ? 0 : 250,
119
- });
120
- const flat = results.flatMap((p) => (p.variants?.length ?? 0) > 0 ? p.variants.map((v) => ({ product: p, variant: v })) : [{ product: p, variant: undefined }]);
121
- const open = flat.length > 0;
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
- function priceOf(p, v, _warehouseId) {
160
- return v?.price ?? p.price;
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';
@@ -0,0 +1,32 @@
1
+ import { type KeyboardEvent } from 'react';
2
+ import { type ProductQuery, type ProductResult, type ProductVariant } from './product-search';
3
+ export type LineProductSearch = (query: Exclude<ProductQuery, {
4
+ kind: 'empty';
5
+ }>, signal: AbortSignal) => Promise<ProductResult[]>;
6
+ export interface LineProductHit {
7
+ product: ProductResult;
8
+ variant?: ProductVariant;
9
+ }
10
+ export interface LineProductCellProps {
11
+ search: LineProductSearch;
12
+ /** Texto de la celda (la descripción del renglón, o el borrador del renglón vacío). */
13
+ text: string;
14
+ onTextChange: (text: string) => void;
15
+ onPick: (product: ProductResult, variant?: ProductVariant) => void;
16
+ /**
17
+ * Teclas que el buscador no consume (lista cerrada): el grid aplica sus
18
+ * comandos (Enter agrega, Supr en celda vacía borra…).
19
+ */
20
+ onKeyDownClosed?: (e: KeyboardEvent<HTMLInputElement>) => void;
21
+ warehouseId?: string;
22
+ currency?: string;
23
+ placeholder?: string;
24
+ ariaLabel: string;
25
+ disabled?: boolean;
26
+ invalid?: boolean;
27
+ className?: string;
28
+ /** Atributos de datos para que el grid ubique la celda (foco por teclado). */
29
+ dataCell?: string;
30
+ }
31
+ export declare const LineProductCell: import("react").ForwardRefExoticComponent<LineProductCellProps & import("react").RefAttributes<HTMLInputElement>>;
32
+ //# sourceMappingURL=line-product-cell.d.ts.map