@asteby/metacore-runtime-react 29.2.9 → 29.2.11

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 29.2.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 9e07da0: Gate `requiresState` on row `status` or `state` so actions like purchases receive_goods hide on draft orders.
8
+
9
+ ## 29.2.10
10
+
11
+ ### Patch Changes
12
+
13
+ - fd81213: BarcodeScanner: prop `feedback` para mostrar un chip de confirmación (producto agregado / sin coincidencia) **sobre** la cámara — los toasts de Sonner no se ven encima del overlay opaco.
14
+
3
15
  ## 29.2.9
4
16
 
5
17
  ### Patch Changes
@@ -12,6 +12,11 @@ export declare function isCameraScanSupported(): boolean;
12
12
  * dispositivo no permite audio. Exportado para reutilizarlo con lectores físicos.
13
13
  */
14
14
  export declare function useScanBeep(): () => void;
15
+ /** Chip superpuesto sobre la cámara (éxito / error del padre tras resolver el código). */
16
+ export type BarcodeScannerFeedback = {
17
+ message: string;
18
+ tone?: 'success' | 'error' | 'neutral';
19
+ };
15
20
  export interface BarcodeScannerProps {
16
21
  open: boolean;
17
22
  onClose: () => void;
@@ -35,12 +40,18 @@ export interface BarcodeScannerProps {
35
40
  * modal, sin taparse con el chrome del host. `fixed` cubre todo el viewport.
36
41
  */
37
42
  position?: 'absolute' | 'fixed';
43
+ /**
44
+ * Feedback del padre (ej. "Harina × 2" / "Sin coincidencia") pintado ENCIMA
45
+ * del video. Los toasts de Sonner del host quedan detrás de este overlay
46
+ * opaco en la práctica — este chip es la confirmación visible al escanear.
47
+ */
48
+ feedback?: BarcodeScannerFeedback | null;
38
49
  }
39
50
  /**
40
51
  * Overlay de cámara a pantalla del contenedor con retícula de puntería. Montalo
41
52
  * condicionalmente (`open`) dentro de un ancestro `relative` cuando uses el
42
53
  * posicionamiento `absolute` por defecto.
43
54
  */
44
- export declare function BarcodeScanner({ open, onClose, onDetected, continuous, beepOnScan, title, hint, position, }: BarcodeScannerProps): React.JSX.Element | null;
55
+ export declare function BarcodeScanner({ open, onClose, onDetected, continuous, beepOnScan, title, hint, position, feedback, }: BarcodeScannerProps): React.JSX.Element | null;
45
56
  export default BarcodeScanner;
46
57
  //# sourceMappingURL=barcode-scanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"barcode-scanner.d.ts","sourceRoot":"","sources":["../src/barcode-scanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAOtF;AAoDD,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,UASlC,CAAA;AAQD,uFAAuF;AACvF,wBAAgB,qBAAqB,IAAI,OAAO,CAM/C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,eAyB1B;AAED,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,8EAA8E;IAC9E,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,uDAAuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,UAAiB,EACjB,UAAiB,EACjB,KAAyB,EACzB,IAAmC,EACnC,QAAqB,GACxB,EAAE,mBAAmB,4BAgMrB;AAED,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"barcode-scanner.d.ts","sourceRoot":"","sources":["../src/barcode-scanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAOtF;AAoDD,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,UASlC,CAAA;AAQD,uFAAuF;AACvF,wBAAgB,qBAAqB,IAAI,OAAO,CAM/C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,eAyB1B;AAED,0FAA0F;AAC1F,MAAM,MAAM,sBAAsB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;CACzC,CAAA;AAED,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,8EAA8E;IAC9E,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,uDAAuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,CAAA;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAA;CAC3C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,UAAiB,EACjB,UAAiB,EACjB,KAAyB,EACzB,IAAmC,EACnC,QAAqB,EACrB,QAAe,GAClB,EAAE,mBAAmB,4BAsNrB;AAED,eAAe,cAAc,CAAA"}
@@ -93,7 +93,7 @@ export function useScanBeep() {
93
93
  * condicionalmente (`open`) dentro de un ancestro `relative` cuando uses el
94
94
  * posicionamiento `absolute` por defecto.
95
95
  */
96
- export function BarcodeScanner({ open, onClose, onDetected, continuous = true, beepOnScan = true, title = 'Escanear código', hint = 'Apuntá al código de barras', position = 'absolute', }) {
96
+ export function BarcodeScanner({ open, onClose, onDetected, continuous = true, beepOnScan = true, title = 'Escanear código', hint = 'Apuntá al código de barras', position = 'absolute', feedback = null, }) {
97
97
  const videoRef = React.useRef(null);
98
98
  const streamRef = React.useRef(null);
99
99
  const rafRef = React.useRef(null);
@@ -207,7 +207,11 @@ export function BarcodeScanner({ open, onClose, onDetected, continuous = true, b
207
207
  const posClass = position === 'fixed' ? 'fixed' : 'absolute';
208
208
  return (_jsxs("div", { className: `${posClass} inset-0 z-[100] flex flex-col bg-black`, children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-3 text-white", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx(ScanLine, { className: "size-5" }), title] }), _jsxs("div", { className: "flex items-center gap-1", children: [torchAvailable && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: torchOn
209
209
  ? 'bg-amber-400/20 text-amber-300 hover:bg-amber-400/30 hover:text-amber-200'
210
- : 'text-white hover:bg-white/10 hover:text-white', onClick: () => void toggleTorch(), "aria-label": torchOn ? 'Apagar linterna' : 'Encender linterna', "aria-pressed": torchOn, title: torchOn ? 'Apagar linterna' : 'Encender linterna', children: torchOn ? (_jsx(Flashlight, { className: "size-5" })) : (_jsx(FlashlightOff, { className: "size-5" })) })), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "text-white hover:bg-white/10 hover:text-white", onClick: onClose, "aria-label": "Cerrar esc\u00E1ner", children: _jsx(X, { className: "size-5" }) })] })] }), _jsx("div", { className: "relative flex flex-1 items-center justify-center overflow-hidden", children: error ? (_jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 px-6 text-center text-white", children: [_jsx(Camera, { className: "size-10 opacity-70" }), _jsx("p", { className: "text-sm leading-relaxed", children: error }), _jsx(Button, { variant: "secondary", onClick: onClose, children: "Entendido" })] })) : (_jsxs(_Fragment, { children: [_jsx("video", { ref: videoRef, className: "h-full w-full object-cover", muted: true, playsInline: true }), _jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "relative h-40 w-72 max-w-[80%] rounded-xl border-2 border-white/80 shadow-[0_0_0_9999px_rgba(0,0,0,0.35)]", children: _jsx("span", { className: "bg-primary absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 animate-pulse" }) }) }), _jsx("p", { className: "absolute bottom-8 left-0 right-0 px-4 text-center text-sm text-white/90", children: !ready
210
+ : 'text-white hover:bg-white/10 hover:text-white', onClick: () => void toggleTorch(), "aria-label": torchOn ? 'Apagar linterna' : 'Encender linterna', "aria-pressed": torchOn, title: torchOn ? 'Apagar linterna' : 'Encender linterna', children: torchOn ? (_jsx(Flashlight, { className: "size-5" })) : (_jsx(FlashlightOff, { className: "size-5" })) })), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "text-white hover:bg-white/10 hover:text-white", onClick: onClose, "aria-label": "Cerrar esc\u00E1ner", children: _jsx(X, { className: "size-5" }) })] })] }), _jsx("div", { className: "relative flex flex-1 items-center justify-center overflow-hidden", children: error ? (_jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 px-6 text-center text-white", children: [_jsx(Camera, { className: "size-10 opacity-70" }), _jsx("p", { className: "text-sm leading-relaxed", children: error }), _jsx(Button, { variant: "secondary", onClick: onClose, children: "Entendido" })] })) : (_jsxs(_Fragment, { children: [_jsx("video", { ref: videoRef, className: "h-full w-full object-cover", muted: true, playsInline: true }), _jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "relative h-40 w-72 max-w-[80%] rounded-xl border-2 border-white/80 shadow-[0_0_0_9999px_rgba(0,0,0,0.35)]", children: _jsx("span", { className: "bg-primary absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 animate-pulse" }) }) }), feedback?.message ? (_jsx("div", { className: "pointer-events-none absolute inset-x-0 top-4 z-10 flex justify-center px-4", "data-testid": "barcode-scanner-feedback", role: "status", "aria-live": "polite", children: _jsx("div", { className: feedback.tone === 'error'
211
+ ? 'max-w-[90%] rounded-full bg-red-500 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-lg'
212
+ : feedback.tone === 'neutral'
213
+ ? 'max-w-[90%] rounded-full bg-white/90 px-4 py-2.5 text-center text-sm font-semibold text-black shadow-lg'
214
+ : 'max-w-[90%] rounded-full bg-emerald-500 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-lg', children: feedback.message }) })) : null, _jsx("p", { className: "absolute bottom-8 left-0 right-0 px-4 text-center text-sm text-white/90", children: !ready
211
215
  ? 'Abriendo cámara…'
212
216
  : torchAvailable
213
217
  ? `${hint} · tocá la linterna si está oscuro`
@@ -42,13 +42,14 @@ export declare const formatAggregateTotal: (col: ColumnDefinition, value: unknow
42
42
  * State-machine gate for per-row actions.
43
43
  *
44
44
  * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
45
- * (snake_case, as served by some backends) is only surfaced for rows whose `status`
46
- * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
47
- * action (requiresState: ['reception']) on an order already in `in_progress`.
45
+ * (snake_case, as served by some backends) is only surfaced for rows whose
46
+ * lifecycle field (`status` or `state`) is contained in that array. This hides
47
+ * e.g. "Recibir" (requiresState: ['confirmed','partial']) on a purchase order
48
+ * still in `draft`.
48
49
  *
49
50
  * Null-safe & non-regressive:
50
51
  * - action without requiresState (or empty array) → always shown.
51
- * - row with no `status` field → all actions shown.
52
+ * - row with neither `status` nor `state` → all actions shown.
52
53
  */
53
54
  export declare const isActionAllowedForRowState: (action: any, row: any) => boolean;
54
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAkC5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA+mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAsDD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAkC5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA+mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -106,24 +106,39 @@ const CodeCell = ({ text, maxLength }) => {
106
106
  };
107
107
  return (_jsxs("div", { className: "group flex items-center gap-1.5", children: [_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/80", title: text, children: display }), _jsx("button", { type: "button", onClick: onCopy, className: "opacity-0 transition-opacity group-hover:opacity-100 text-muted-foreground hover:text-foreground", "aria-label": "Copiar", title: "Copiar", children: copied ? (_jsx(icons.Check, { className: "h-3.5 w-3.5 text-green-500" })) : (_jsx(icons.Copy, { className: "h-3.5 w-3.5" })) })] }));
108
108
  };
109
+ /**
110
+ * Lifecycle column used by `requiresState`: prefer `status` (workshop, vehicles,
111
+ * …) and fall back to `state` (purchases, inventory transfers, …). Empty string
112
+ * is treated as missing so a blank `status` does not hide a populated `state`.
113
+ */
114
+ const rowLifecycleState = (row) => {
115
+ const status = row?.status;
116
+ if (status !== undefined && status !== null && status !== '')
117
+ return status;
118
+ const state = row?.state;
119
+ if (state !== undefined && state !== null && state !== '')
120
+ return state;
121
+ return undefined;
122
+ };
109
123
  /**
110
124
  * State-machine gate for per-row actions.
111
125
  *
112
126
  * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
113
- * (snake_case, as served by some backends) is only surfaced for rows whose `status`
114
- * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
115
- * action (requiresState: ['reception']) on an order already in `in_progress`.
127
+ * (snake_case, as served by some backends) is only surfaced for rows whose
128
+ * lifecycle field (`status` or `state`) is contained in that array. This hides
129
+ * e.g. "Recibir" (requiresState: ['confirmed','partial']) on a purchase order
130
+ * still in `draft`.
116
131
  *
117
132
  * Null-safe & non-regressive:
118
133
  * - action without requiresState (or empty array) → always shown.
119
- * - row with no `status` field → all actions shown.
134
+ * - row with neither `status` nor `state` → all actions shown.
120
135
  */
121
136
  export const isActionAllowedForRowState = (action, row) => {
122
137
  const requires = action?.requiresState ?? action?.requires_state;
123
138
  if (!Array.isArray(requires) || requires.length === 0)
124
139
  return true;
125
- const status = row?.status;
126
- if (status === undefined || status === null || status === '')
140
+ const status = rowLifecycleState(row);
141
+ if (status === undefined)
127
142
  return true;
128
143
  return requires.map(String).includes(String(status));
129
144
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "29.2.9",
3
+ "version": "29.2.11",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,8 +37,8 @@
37
37
  "react-i18next": ">=13",
38
38
  "sonner": ">=1.7",
39
39
  "zustand": ">=5",
40
- "@asteby/metacore-ui": "^2.13.0",
41
- "@asteby/metacore-sdk": "^3.4.0"
40
+ "@asteby/metacore-sdk": "^3.4.0",
41
+ "@asteby/metacore-ui": "^2.13.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@tanstack/react-router": {
@@ -67,8 +67,8 @@
67
67
  "typescript": "^6.0.0",
68
68
  "vitest": "^4.0.0",
69
69
  "zustand": "^5.0.0",
70
- "@asteby/metacore-ui": "2.13.0",
71
- "@asteby/metacore-sdk": "3.4.0"
70
+ "@asteby/metacore-sdk": "3.4.0",
71
+ "@asteby/metacore-ui": "2.13.0"
72
72
  },
73
73
  "scripts": {
74
74
  "build": "tsc -p tsconfig.json",
@@ -48,4 +48,24 @@ describe('isActionAllowedForRowState', () => {
48
48
  expect(isActionAllowedForRowState(action, { status: 2 })).toBe(true)
49
49
  expect(isActionAllowedForRowState(action, { status: '3' })).toBe(false)
50
50
  })
51
+
52
+ it('falls back to row.state when status is absent (purchases, transfers)', () => {
53
+ const receive = { key: 'receive_goods', requiresState: ['confirmed', 'partial'] }
54
+ expect(isActionAllowedForRowState(receive, { state: 'draft' })).toBe(false)
55
+ expect(isActionAllowedForRowState(receive, { state: 'confirmed' })).toBe(true)
56
+ expect(isActionAllowedForRowState(receive, { state: 'partial' })).toBe(true)
57
+ expect(isActionAllowedForRowState(receive, { state: 'received' })).toBe(false)
58
+ })
59
+
60
+ it('prefers status over state when both are set', () => {
61
+ const action = { key: 'start', requiresState: ['reception'] }
62
+ expect(isActionAllowedForRowState(action, { status: 'reception', state: 'draft' })).toBe(true)
63
+ expect(isActionAllowedForRowState(action, { status: 'in_progress', state: 'reception' })).toBe(false)
64
+ })
65
+
66
+ it('ignores empty status and uses state instead', () => {
67
+ const action = { key: 'confirm', requiresState: ['draft'] }
68
+ expect(isActionAllowedForRowState(action, { status: '', state: 'draft' })).toBe(true)
69
+ expect(isActionAllowedForRowState(action, { status: '', state: 'confirmed' })).toBe(false)
70
+ })
51
71
  })
@@ -126,6 +126,12 @@ export function useScanBeep() {
126
126
  }, [])
127
127
  }
128
128
 
129
+ /** Chip superpuesto sobre la cámara (éxito / error del padre tras resolver el código). */
130
+ export type BarcodeScannerFeedback = {
131
+ message: string
132
+ tone?: 'success' | 'error' | 'neutral'
133
+ }
134
+
129
135
  export interface BarcodeScannerProps {
130
136
  open: boolean
131
137
  onClose: () => void
@@ -149,6 +155,12 @@ export interface BarcodeScannerProps {
149
155
  * modal, sin taparse con el chrome del host. `fixed` cubre todo el viewport.
150
156
  */
151
157
  position?: 'absolute' | 'fixed'
158
+ /**
159
+ * Feedback del padre (ej. "Harina × 2" / "Sin coincidencia") pintado ENCIMA
160
+ * del video. Los toasts de Sonner del host quedan detrás de este overlay
161
+ * opaco en la práctica — este chip es la confirmación visible al escanear.
162
+ */
163
+ feedback?: BarcodeScannerFeedback | null
152
164
  }
153
165
 
154
166
  /**
@@ -165,6 +177,7 @@ export function BarcodeScanner({
165
177
  title = 'Escanear código',
166
178
  hint = 'Apuntá al código de barras',
167
179
  position = 'absolute',
180
+ feedback = null,
168
181
  }: BarcodeScannerProps) {
169
182
  const videoRef = React.useRef<HTMLVideoElement | null>(null)
170
183
  const streamRef = React.useRef<MediaStream | null>(null)
@@ -345,6 +358,28 @@ export function BarcodeScanner({
345
358
  <span className="bg-primary absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 animate-pulse" />
346
359
  </div>
347
360
  </div>
361
+ {/* Confirmación del padre ENCIMA del video (Sonner queda
362
+ detrás de este overlay opaco al escanear). */}
363
+ {feedback?.message ? (
364
+ <div
365
+ className="pointer-events-none absolute inset-x-0 top-4 z-10 flex justify-center px-4"
366
+ data-testid="barcode-scanner-feedback"
367
+ role="status"
368
+ aria-live="polite"
369
+ >
370
+ <div
371
+ className={
372
+ feedback.tone === 'error'
373
+ ? 'max-w-[90%] rounded-full bg-red-500 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-lg'
374
+ : feedback.tone === 'neutral'
375
+ ? 'max-w-[90%] rounded-full bg-white/90 px-4 py-2.5 text-center text-sm font-semibold text-black shadow-lg'
376
+ : 'max-w-[90%] rounded-full bg-emerald-500 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-lg'
377
+ }
378
+ >
379
+ {feedback.message}
380
+ </div>
381
+ </div>
382
+ ) : null}
348
383
  <p className="absolute bottom-8 left-0 right-0 px-4 text-center text-sm text-white/90">
349
384
  {!ready
350
385
  ? 'Abriendo cámara…'
@@ -204,23 +204,37 @@ const CodeCell: React.FC<{ text: string; maxLength?: number }> = ({ text, maxLen
204
204
  )
205
205
  }
206
206
 
207
+ /**
208
+ * Lifecycle column used by `requiresState`: prefer `status` (workshop, vehicles,
209
+ * …) and fall back to `state` (purchases, inventory transfers, …). Empty string
210
+ * is treated as missing so a blank `status` does not hide a populated `state`.
211
+ */
212
+ const rowLifecycleState = (row: any): unknown => {
213
+ const status = row?.status
214
+ if (status !== undefined && status !== null && status !== '') return status
215
+ const state = row?.state
216
+ if (state !== undefined && state !== null && state !== '') return state
217
+ return undefined
218
+ }
219
+
207
220
  /**
208
221
  * State-machine gate for per-row actions.
209
222
  *
210
223
  * An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
211
- * (snake_case, as served by some backends) is only surfaced for rows whose `status`
212
- * field value is contained in that array. This hides e.g. an "Iniciar trabajo"
213
- * action (requiresState: ['reception']) on an order already in `in_progress`.
224
+ * (snake_case, as served by some backends) is only surfaced for rows whose
225
+ * lifecycle field (`status` or `state`) is contained in that array. This hides
226
+ * e.g. "Recibir" (requiresState: ['confirmed','partial']) on a purchase order
227
+ * still in `draft`.
214
228
  *
215
229
  * Null-safe & non-regressive:
216
230
  * - action without requiresState (or empty array) → always shown.
217
- * - row with no `status` field → all actions shown.
231
+ * - row with neither `status` nor `state` → all actions shown.
218
232
  */
219
233
  export const isActionAllowedForRowState = (action: any, row: any): boolean => {
220
234
  const requires: unknown = action?.requiresState ?? action?.requires_state
221
235
  if (!Array.isArray(requires) || requires.length === 0) return true
222
- const status = row?.status
223
- if (status === undefined || status === null || status === '') return true
236
+ const status = rowLifecycleState(row)
237
+ if (status === undefined) return true
224
238
  return requires.map(String).includes(String(status))
225
239
  }
226
240