@asteby/metacore-runtime-react 45.2.0 → 45.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/action-modal-dispatcher.d.ts +29 -0
  3. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  4. package/dist/action-modal-dispatcher.js +115 -6
  5. package/dist/activity-diff.d.ts +2 -0
  6. package/dist/activity-diff.d.ts.map +1 -1
  7. package/dist/activity-diff.js +2 -2
  8. package/dist/business/catalog.d.ts.map +1 -1
  9. package/dist/business/catalog.js +8 -0
  10. package/dist/business/index.d.ts +2 -0
  11. package/dist/business/index.d.ts.map +1 -1
  12. package/dist/business/index.js +2 -0
  13. package/dist/business/refund-destination.js +1 -1
  14. package/dist/business/refund.d.ts +1 -1
  15. package/dist/business/refund.d.ts.map +1 -1
  16. package/dist/business/refund.js +1 -0
  17. package/dist/business/return-wizard.d.ts +23 -0
  18. package/dist/business/return-wizard.d.ts.map +1 -0
  19. package/dist/business/return-wizard.js +59 -0
  20. package/dist/business/return.d.ts +90 -0
  21. package/dist/business/return.d.ts.map +1 -0
  22. package/dist/business/return.js +112 -0
  23. package/dist/dynamic-columns.d.ts.map +1 -1
  24. package/dist/dynamic-columns.js +3 -1
  25. package/dist/dynamic-relation.d.ts +4 -0
  26. package/dist/dynamic-relation.d.ts.map +1 -1
  27. package/dist/dynamic-relation.js +10 -1
  28. package/dist/dynamic-row-actions.d.ts.map +1 -1
  29. package/dist/dynamic-row-actions.js +14 -2
  30. package/dist/dynamic-table.d.ts.map +1 -1
  31. package/dist/dynamic-table.js +28 -3
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -0
  35. package/dist/reason-prompt.d.ts +59 -0
  36. package/dist/reason-prompt.d.ts.map +1 -0
  37. package/dist/reason-prompt.js +111 -0
  38. package/dist/use-addon-settings.d.ts +17 -3
  39. package/dist/use-addon-settings.d.ts.map +1 -1
  40. package/dist/use-addon-settings.js +20 -8
  41. package/package.json +1 -1
  42. package/src/__tests__/prefill-from-record.test.ts +69 -0
  43. package/src/__tests__/reason-prompt.test.tsx +121 -0
  44. package/src/__tests__/use-addon-settings.test.ts +16 -1
  45. package/src/action-modal-dispatcher.tsx +145 -3
  46. package/src/activity-diff.tsx +10 -1
  47. package/src/business/business.test.ts +46 -0
  48. package/src/business/catalog.ts +8 -0
  49. package/src/business/index.ts +2 -0
  50. package/src/business/refund-destination.tsx +2 -2
  51. package/src/business/refund.ts +2 -1
  52. package/src/business/return-wizard.tsx +223 -0
  53. package/src/business/return.ts +181 -0
  54. package/src/dynamic-columns.tsx +5 -1
  55. package/src/dynamic-relation.tsx +14 -1
  56. package/src/dynamic-row-actions.tsx +13 -1
  57. package/src/dynamic-table.tsx +22 -2
  58. package/src/index.ts +10 -0
  59. package/src/reason-prompt.tsx +247 -0
  60. package/src/use-addon-settings.ts +26 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 45.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ce3dd3b: Action modal: `PrefillSpec.fromField` — a line-items grid can be seeded from the document picked in a sibling ref field. A `create`-placed action (no record) such as «Recibir OC» now fills its rows as soon as the user selects the purchase order: `$prefillFromRecord` names a one_to_many relation of the referenced model, `map`/`remaining`/`lock` work as in row actions, and clearing the selection empties the grid. Exports `prefillFromFieldRequests` and `prefillFromFieldRelationRequest`.
8
+ - 6ffdaa9: Motivo obligatorio al eliminar/cancelar (PER-4). Un modelo que declara `reason_required` en su manifest responde 422 `errors.reason` a un DELETE o acción sin motivo; `useReasonPrompt().run({ request })` prueba la petición, y si el servidor lo exige pregunta una vez (`ReasonPromptDialog`) y reintenta con `reason`. Ya cableado en la eliminación de fila, la eliminación masiva (un motivo para el lote) y los modales de acción (confirmación y formulario). `ActivityEvent.reason` se pinta en `ActivityDiff` («Motivo: …»). `useAddonSettings`/`useUpdateAddonSettings` aceptan `branchId` para leer/escribir los ajustes por sucursal (`settings[].scope: branch`, POS-1); `addonSettingsKey`/`addonSettingsUrl` lo respetan.
9
+ - cfafe4e: `ReturnWizard` («Devolver mercancía», benchmark DEV-2): un solo asistente para POS, Pedido, Factura, RMA y OT. Renglones con tope vendido − devuelto, condición vendible/defectuoso → stock/merma/garantía, paso «Recibir» dentro del asistente cuando no hay almacén (PIT-021), reembolso con `RefundDestination` (nuevo destino `transfer`), estados de RMA con motivo obligatorio al rechazar/cancelar (`canTransitionRma`) y NC CFDI con relación 01/03 automática (`creditNoteRelation`). Helpers puros: `computeReturnTotals`, `validateReturnChoices`, `serializeReturn`, `returnSteps`.
10
+
11
+ ### Patch Changes
12
+
13
+ - 0eb49fb: DynamicRelation: una sub-tabla cuya carga falla (403/500) ya no se muestra como «No hay registros relacionados»; muestra «Sin permiso…» o un aviso de error (strings opcionales `forbiddenState` / `errorState`). La columna `tags` con objetos usa su `label`/`name`/`key` en lugar de «[object Object]» (PIT-045, PIT-049).
14
+
3
15
  ## 45.2.0
4
16
 
5
17
  ### Minor Changes
@@ -16,6 +16,16 @@ export interface PrefillSpec {
16
16
  * flow (which carries no prefill spec); only the prefilled action locks them.
17
17
  */
18
18
  lock?: string[];
19
+ /**
20
+ * Key of a SIBLING action field (a dynamic_select with a `ref`) whose selected
21
+ * value is the id of the document the rows are seeded from. Without it the
22
+ * rows come from the record the action was opened on (row actions); with it
23
+ * a `create`-placed action — which has no record — seeds its grid as soon as
24
+ * the user picks the source document (e.g. a goods receipt picking its
25
+ * purchase order). `$prefillFromRecord` then names a one_to_many relation of
26
+ * the REFERENCED model. Clearing the selection empties the grid.
27
+ */
28
+ fromField?: string;
19
29
  }
20
30
  export declare function isPrefillSpec(v: unknown): v is PrefillSpec;
21
31
  export declare function applyPrefillLock(field: ActionFieldDef): ActionFieldDef;
@@ -33,6 +43,25 @@ export declare function prefillRelationRequests(fields: ActionFieldDef[] | undef
33
43
  foreign_key?: string;
34
44
  scope?: Record<string, unknown>;
35
45
  }>): PrefillRelationRequest[];
46
+ export interface PrefillFromFieldRequest {
47
+ fieldKey: string;
48
+ spec: PrefillSpec;
49
+ /** Model key the sibling field references (its `ref`). */
50
+ refModel: string;
51
+ /** Selected document id; '' when nothing is selected (grid must clear). */
52
+ refId: string;
53
+ }
54
+ export declare function prefillFromFieldRequests(fields: ActionFieldDef[] | undefined, formData: Record<string, any>): PrefillFromFieldRequest[];
55
+ export declare function prefillFromFieldRelationRequest(req: PrefillFromFieldRequest, relations: Array<{
56
+ name?: string;
57
+ kind?: string;
58
+ through?: string;
59
+ foreign_key?: string;
60
+ scope?: Record<string, unknown>;
61
+ }>): {
62
+ endpoint: string;
63
+ params: Record<string, string | number>;
64
+ } | null;
36
65
  export declare function unwrapRecordScalar(value: unknown): unknown;
37
66
  export declare function readRecordPath(record: any, path: string): unknown;
38
67
  /** Scalar seed for one action field from the row being acted on. */
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4EA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAGxB,MAAM,sBAAsB,CAAA;AAG7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AAYD,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CAC1C;AAED,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,cAAc,EAAE,GAAG,SAAS,EACpC,MAAM,EAAE,GAAG,EACX,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC5H,sBAAsB,EAAE,CAyB1B;AASD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CASjE;AAUD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAgBnF;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAmFlB;AAED,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,QAAS,CAAA"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // ActionModalDispatcher — renders the right modal for a custom action:
3
3
  // 1) Custom component from the SDK registry → use it
4
4
  // 2) action.modal set but no registered component → loading state while the
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  //
16
16
  // The host injects its axios-like client via <ApiProvider>; we no longer
17
17
  // depend on a bundler alias to `@/lib/api`.
18
- import { useState, useEffect, useMemo, useSyncExternalStore } from 'react';
18
+ import { useState, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
19
19
  import { useTranslation } from 'react-i18next';
20
20
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
21
21
  import { Loader2 } from 'lucide-react';
@@ -27,6 +27,7 @@ import { validateValues, bagHasErrors } from './validator';
27
27
  import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
28
28
  import { validationCatalog } from './validation-catalog';
29
29
  import { useApi } from './api-context';
30
+ import { useReasonPrompt } from './reason-prompt';
30
31
  import { buildRelationFilterParams } from './dynamic-relation-helpers';
31
32
  import { DynamicIcon } from './dynamic-icon';
32
33
  import { DynamicLineItems } from './dynamic-line-items';
@@ -130,6 +131,45 @@ export function prefillRelationRequests(fields, record, relations) {
130
131
  }
131
132
  return out;
132
133
  }
134
+ export function prefillFromFieldRequests(fields, formData) {
135
+ const out = [];
136
+ if (!fields)
137
+ return out;
138
+ for (const field of fields) {
139
+ if (!isLineItemsField(field))
140
+ continue;
141
+ const spec = lineItemsDefault(field);
142
+ if (!isPrefillSpec(spec) || !spec.fromField)
143
+ continue;
144
+ const source = fields.find((f) => f.key === spec.fromField);
145
+ const refModel = source ? getFieldRef(source) : undefined;
146
+ if (!refModel)
147
+ continue;
148
+ const raw = unwrapRecordScalar(formData?.[spec.fromField]);
149
+ const refId = raw === undefined || raw === null ? '' : String(raw);
150
+ out.push({ fieldKey: field.key, spec, refModel, refId });
151
+ }
152
+ return out;
153
+ }
154
+ // prefillFromFieldRelationRequest resolves the child-rows request for a
155
+ // selected document, from the referenced model's declared relations. Null when
156
+ // the relation is missing or not a plain one_to_many.
157
+ export function prefillFromFieldRelationRequest(req, relations) {
158
+ if (!req.refId)
159
+ return null;
160
+ const rel = (relations || []).find((r) => r?.name === req.spec.$prefillFromRecord && (r.kind ?? 'one_to_many') === 'one_to_many');
161
+ if (!rel?.through || !rel.foreign_key)
162
+ return null;
163
+ const scope = {};
164
+ for (const [k, v] of Object.entries(rel.scope ?? {})) {
165
+ if (v !== undefined && v !== null)
166
+ scope[k] = String(v);
167
+ }
168
+ return {
169
+ endpoint: `/data/${rel.through}`,
170
+ params: { ...buildRelationFilterParams(rel.foreign_key, req.refId, scope), per_page: 200 },
171
+ };
172
+ }
133
173
  // ---- scalar prefill from the acted-on record --------------------------------
134
174
  //
135
175
  // Row actions (stamp / refactura / cancel-with-reason) should open with the
@@ -481,6 +521,9 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
481
521
  const { t, i18n } = useTranslation();
482
522
  const api = useApi();
483
523
  const [executing, setExecuting] = useState(false);
524
+ // Actions listed in the model's `reason_required.actions` (cancel, void…)
525
+ // answer 422 errors.reason without a motive; the prompt asks and retries.
526
+ const reasonPrompt = useReasonPrompt();
484
527
  // `action.label` is an addon-contributed i18n key; its locale bundle loads
485
528
  // asynchronously, so translate at render (defaultValue keeps an already
486
529
  // localized label unchanged).
@@ -489,7 +532,14 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
489
532
  setExecuting(true);
490
533
  try {
491
534
  const url = buildActionUrl(endpoint, model, record.id, action.key);
492
- const res = await api.post(url, {});
535
+ const res = await reasonPrompt.run({
536
+ title: label,
537
+ description: t('actions.reason_desc', { defaultValue: 'Esta acción pide el motivo. Queda en la bitácora del documento.' }),
538
+ request: (reason) => api.post(url, reason ? { reason } : {}),
539
+ });
540
+ // Operator cancelled the reason prompt: nothing ran.
541
+ if (!res)
542
+ return;
493
543
  if (res.data.success) {
494
544
  toastServerSuccess(res.data, { t });
495
545
  onOpenChange(false);
@@ -506,7 +556,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
506
556
  setExecuting(false);
507
557
  }
508
558
  };
509
- return (_jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) }));
559
+ return (_jsxs(_Fragment, { children: [reasonPrompt.dialog, _jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) })] }));
510
560
  }
511
561
  function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
512
562
  const { t, i18n } = useTranslation();
@@ -515,6 +565,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
515
565
  // defaultValue keeps an already-localized string unchanged.
516
566
  const tl = (s) => t(s, { defaultValue: s });
517
567
  const api = useApi();
568
+ const reasonPrompt = useReasonPrompt();
518
569
  const branchGate = useBranchCreateGate();
519
570
  const [formData, setFormData] = useState({});
520
571
  const [executing, setExecuting] = useState(false);
@@ -580,6 +631,58 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
580
631
  cancelled = true;
581
632
  };
582
633
  }, [open, action.fields, record, relations, api]);
634
+ // Lines seeded from a document picked in a sibling field (PrefillSpec.fromField).
635
+ // Re-runs only when the picked id changes; each pick loads the referenced
636
+ // model's relations (cached per model for the life of the modal) and then
637
+ // the child rows.
638
+ const fromFieldReqs = useMemo(() => prefillFromFieldRequests(action.fields, formData), [action.fields, formData]);
639
+ const fromFieldSig = fromFieldReqs.map((r) => `${r.fieldKey}=${r.refModel}:${r.refId}`).join('|');
640
+ const refRelationsCache = useRef(new Map());
641
+ useEffect(() => {
642
+ if (!open || fromFieldReqs.length === 0)
643
+ return;
644
+ let cancelled = false;
645
+ for (const req of fromFieldReqs) {
646
+ if (!req.refId) {
647
+ setFormData((prev) => Array.isArray(prev[req.fieldKey]) && prev[req.fieldKey].length === 0
648
+ ? prev
649
+ : { ...prev, [req.fieldKey]: [] });
650
+ continue;
651
+ }
652
+ let relsPromise = refRelationsCache.current.get(req.refModel);
653
+ if (!relsPromise) {
654
+ relsPromise = api
655
+ .get(`/metadata/table/${req.refModel}`)
656
+ .then((res) => {
657
+ const rels = res?.data?.relations ?? res?.data?.data?.relations ?? [];
658
+ return Array.isArray(rels) ? rels : [];
659
+ })
660
+ .catch(() => []);
661
+ refRelationsCache.current.set(req.refModel, relsPromise);
662
+ }
663
+ relsPromise
664
+ .then((rels) => {
665
+ const call = prefillFromFieldRelationRequest(req, rels);
666
+ if (!call)
667
+ return;
668
+ return api.get(call.endpoint, { params: call.params }).then((res) => {
669
+ if (cancelled)
670
+ return;
671
+ const rows = res?.data?.data ?? res?.data ?? [];
672
+ if (!Array.isArray(rows))
673
+ return;
674
+ const built = buildPrefillRows(req.spec, { [req.spec.$prefillFromRecord]: rows });
675
+ setFormData((prev) => ({ ...prev, [req.fieldKey]: built }));
676
+ });
677
+ })
678
+ .catch(() => { });
679
+ }
680
+ return () => {
681
+ cancelled = true;
682
+ };
683
+ // fromFieldSig captures every dependency of fromFieldReqs that matters.
684
+ // eslint-disable-next-line react-hooks/exhaustive-deps
685
+ }, [open, fromFieldSig, api]);
583
686
  const updateField = (key, value) => {
584
687
  setFormData((prev) => ({ ...prev, [key]: value }));
585
688
  setFieldErrors((prev) => clearFieldErrorTree(prev, key));
@@ -643,7 +746,13 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
643
746
  payload = { ...payload, branch_id: branchId };
644
747
  }
645
748
  const url = buildActionUrl(endpoint, model, record?.id, action.key);
646
- const res = await api.post(url, payload);
749
+ const res = await reasonPrompt.run({
750
+ title: tl(action.label),
751
+ description: t('actions.reason_desc', { defaultValue: 'Esta acción pide el motivo. Queda en la bitácora del documento.' }),
752
+ request: (reason) => api.post(url, reason ? { ...payload, reason } : payload),
753
+ });
754
+ if (!res)
755
+ return;
647
756
  if (res.data.success) {
648
757
  toastServerSuccess(res.data, { t });
649
758
  onOpenChange(false);
@@ -680,7 +789,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
680
789
  resolveWidget(field) === 'textarea' ||
681
790
  resolveWidget(field) === 'richtext';
682
791
  return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
683
- }), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] })] }) }));
792
+ }), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] }), reasonPrompt.dialog] }) }));
684
793
  }
685
794
  // buildFieldDefaults seeds formData for a set of action fields, honoring the
686
795
  // same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
@@ -43,6 +43,8 @@ export interface ActivityEvent {
43
43
  to: unknown;
44
44
  }> | null;
45
45
  summary?: string | null;
46
+ /** Motive the operator stated for a delete / reason_required action (PER-4). */
47
+ reason?: string | null;
46
48
  occurred_at: string;
47
49
  }
48
50
  export interface ActivityDiffProps {
@@ -1 +1 @@
1
- {"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AA8GD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA4KpD,CAAA"}
1
+ {"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AA8GD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAmLpD,CAAA"}
@@ -142,10 +142,10 @@ export const ActivityDiff = ({ event, columns, timeZone, currency, locale = 'es'
142
142
  const isCreated = variant === 'created';
143
143
  const isDeleted = variant === 'deleted';
144
144
  const variantBadge = VARIANT_BADGE[variant] ?? VARIANT_BADGE.other;
145
- if (allKeys.length === 0 && !event.summary) {
145
+ if (allKeys.length === 0 && !event.summary && !event.reason) {
146
146
  return (_jsx("div", { className: cn('text-sm text-muted-foreground italic py-1', className), children: "Sin campos registrados." }));
147
147
  }
148
- return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: "outline", className: cn('text-xs font-medium px-2 py-0.5', variantBadge.className), children: variantBadge.label || event.action }), changed.size > 0 && variant === 'updated' && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [changed.size, " campo", changed.size !== 1 ? 's' : '', " modificado", changed.size !== 1 ? 's' : ''] })), allKeys.length > 0 && variant === 'updated' && (_jsx("button", { type: "button", onClick: () => setShowOnlyChanged((v) => !v), className: "ml-auto text-xs text-primary hover:underline", children: showOnlyChanged ? `Ver todos (${allKeys.length})` : 'Solo cambios' }))] }), event.summary && (_jsx("p", { className: "text-sm text-muted-foreground italic", children: event.summary })), displayedKeys.length > 0 && (_jsxs("div", { className: "rounded-lg border border-border/60 overflow-hidden text-sm", children: [isCreated || isDeleted ? (_jsxs("div", { className: "grid grid-cols-[1fr_2fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: isDeleted ? 'Valor anterior' : 'Valor' })] })) : (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: "Antes" }), _jsx("span", { children: "Despu\u00E9s" })] })), displayedKeys.map((key, idx) => {
148
+ return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx(Badge, { variant: "outline", className: cn('text-xs font-medium px-2 py-0.5', variantBadge.className), children: variantBadge.label || event.action }), changed.size > 0 && variant === 'updated' && (_jsxs("span", { className: "text-xs text-muted-foreground", children: [changed.size, " campo", changed.size !== 1 ? 's' : '', " modificado", changed.size !== 1 ? 's' : ''] })), allKeys.length > 0 && variant === 'updated' && (_jsx("button", { type: "button", onClick: () => setShowOnlyChanged((v) => !v), className: "ml-auto text-xs text-primary hover:underline", children: showOnlyChanged ? `Ver todos (${allKeys.length})` : 'Solo cambios' }))] }), event.reason && (_jsxs("p", { className: "text-sm", "data-testid": "activity-reason", children: [_jsx("span", { className: "font-medium", children: "Motivo:" }), " ", event.reason] })), event.summary && (_jsx("p", { className: "text-sm text-muted-foreground italic", children: event.summary })), displayedKeys.length > 0 && (_jsxs("div", { className: "rounded-lg border border-border/60 overflow-hidden text-sm", children: [isCreated || isDeleted ? (_jsxs("div", { className: "grid grid-cols-[1fr_2fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: isDeleted ? 'Valor anterior' : 'Valor' })] })) : (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr] border-b border-border/40 bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground gap-x-2", children: [_jsx("span", { children: "Campo" }), _jsx("span", { children: "Antes" }), _jsx("span", { children: "Despu\u00E9s" })] })), displayedKeys.map((key, idx) => {
149
149
  const col = resolveColumn(key, columns);
150
150
  const label = resolveLabel(key, columns);
151
151
  const isChanged = changed.has(key);
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAyCtD,CAAA"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAiDtD,CAAA"}
@@ -39,4 +39,12 @@ export const BUSINESS_COMPONENTS = [
39
39
  usedBy: ['ReturnWizard', 'Cancelación de apartado', 'Anticipos'],
40
40
  helpers: ['validateRefund', 'singleDestination', 'DEFAULT_REFUND_DESTINATIONS'],
41
41
  },
42
+ {
43
+ name: 'ReturnWizard',
44
+ value: 'lines: ReturnableLine[]',
45
+ events: ['onSubmit', 'onCancel'],
46
+ permissions: [],
47
+ usedBy: ['POS', 'Pedido', 'Factura', 'RMA', 'OT'],
48
+ helpers: ['computeReturnTotals', 'validateReturnChoices', 'creditNoteRelation', 'returnSteps', 'serializeReturn'],
49
+ },
42
50
  ];
@@ -11,4 +11,6 @@ export * from './product-search';
11
11
  export * from './product-picker';
12
12
  export * from './customer-picker';
13
13
  export * from './catalog';
14
+ export * from './return';
15
+ export * from './return-wizard';
14
16
  //# sourceMappingURL=index.d.ts.map
@@ -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,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"}
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,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"}
@@ -11,3 +11,5 @@ export * from './product-search';
11
11
  export * from './product-picker';
12
12
  export * from './customer-picker';
13
13
  export * from './catalog';
14
+ export * from './return';
15
+ export * from './return-wizard';
@@ -27,5 +27,5 @@ export function RefundDestination({ total, value, onChange, options = DEFAULT_RE
27
27
  const off = o.available === false;
28
28
  const on = chosen(o.kind);
29
29
  return (_jsx(Button, { type: "button", variant: on ? 'default' : 'outline', role: allowSplit ? 'checkbox' : 'radio', "aria-checked": on, disabled: disabled || off, title: off ? o.disabledReason : undefined, className: "h-auto justify-start whitespace-normal py-2 text-left", onClick: () => toggle(o), children: _jsxs("span", { children: [_jsx("span", { className: "block", children: o.label }), off && o.disabledReason && _jsx("span", { className: "block text-xs opacity-70", children: o.disabledReason }), !off && o.maxAmount != null && _jsxs("span", { className: "block text-xs opacity-70", children: [t('refund.max', { defaultValue: 'Máx.' }), " ", fmt.money(o.maxAmount)] })] }) }, o.kind));
30
- }) }), value.map((a, i) => (_jsxs("div", { className: "grid grid-cols-[1fr_8rem_10rem] items-start gap-2", children: [_jsx("span", { className: "pt-2 text-sm", children: options.find((o) => o.kind === a.kind)?.label ?? a.kind }), _jsxs("div", { children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(a.amount), disabled: disabled || !allowSplit, "aria-invalid": !!errors[`${i}.amount`], onChange: (e) => emit(value.map((x, j) => (j === i ? { ...x, amount: Number(e.target.value.replace(',', '.')) || 0 } : x))) }), errors[`${i}.amount`] && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: errors[`${i}.amount`] })] }), (a.kind === 'card' || a.kind === 'credit_note') && (_jsx(Input, { placeholder: a.kind === 'card' ? t('refund.cardRef', { defaultValue: 'Referencia' }) : t('refund.ncRef', { defaultValue: 'Folio / UUID relacionado' }), value: a.reference ?? '', disabled: disabled, onChange: (e) => emit(value.map((x, j) => (j === i ? { ...x, reference: e.target.value } : x))) })), errors[`${i}.kind`] && _jsx("p", { className: "col-span-3 text-xs text-destructive", children: errors[`${i}.kind`] })] }, a.kind))), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form })] }));
30
+ }) }), value.map((a, i) => (_jsxs("div", { className: "grid grid-cols-[1fr_8rem_10rem] items-start gap-2", children: [_jsx("span", { className: "pt-2 text-sm", children: options.find((o) => o.kind === a.kind)?.label ?? a.kind }), _jsxs("div", { children: [_jsx(Input, { inputMode: "decimal", className: "text-right", value: String(a.amount), disabled: disabled || !allowSplit, "aria-invalid": !!errors[`${i}.amount`], onChange: (e) => emit(value.map((x, j) => (j === i ? { ...x, amount: Number(e.target.value.replace(',', '.')) || 0 } : x))) }), errors[`${i}.amount`] && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: errors[`${i}.amount`] })] }), (a.kind === 'card' || a.kind === 'transfer' || a.kind === 'credit_note') && (_jsx(Input, { placeholder: a.kind === 'card' || a.kind === 'transfer' ? t('refund.cardRef', { defaultValue: 'Referencia' }) : t('refund.ncRef', { defaultValue: 'Folio / UUID relacionado' }), value: a.reference ?? '', disabled: disabled, onChange: (e) => emit(value.map((x, j) => (j === i ? { ...x, reference: e.target.value } : x))) })), errors[`${i}.kind`] && _jsx("p", { className: "col-span-3 text-xs text-destructive", children: errors[`${i}.kind`] })] }, a.kind))), validation.form && value.length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form })] }));
31
31
  }
@@ -1,4 +1,4 @@
1
- export type RefundDestinationKind = 'cash' | 'card' | 'wallet' | 'store_credit' | 'credit_note';
1
+ export type RefundDestinationKind = 'cash' | 'card' | 'transfer' | 'wallet' | 'store_credit' | 'credit_note';
2
2
  export interface RefundDestinationOption {
3
3
  kind: RefundDestinationKind;
4
4
  label: string;
@@ -1 +1 @@
1
- {"version":3,"file":"refund.d.ts","sourceRoot":"","sources":["../../src/business/refund.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CAAA;AAE/F,MAAM,WAAW,uBAAuB;IACpC,IAAI,EAAE,qBAAqB,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,qBAAqB,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,EAAE,uBAAuB,EAMhE,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,cAAc,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,gBAAgB,EAAE,EAC/B,OAAO,GAAE,uBAAuB,EAAgC,GACjE,gBAAgB,CAqBlB;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAEhG"}
1
+ {"version":3,"file":"refund.d.ts","sourceRoot":"","sources":["../../src/business/refund.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CAAA;AAE5G,MAAM,WAAW,uBAAuB;IACpC,IAAI,EAAE,qBAAqB,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,qBAAqB,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,EAAE,uBAAuB,EAOhE,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,cAAc,CAC1B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,gBAAgB,EAAE,EAC/B,OAAO,GAAE,uBAAuB,EAAgC,GACjE,gBAAgB,CAqBlB;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAEhG"}
@@ -5,6 +5,7 @@ import { roundMoney, toAmount } from './format';
5
5
  export const DEFAULT_REFUND_DESTINATIONS = [
6
6
  { kind: 'cash', label: 'Efectivo' },
7
7
  { kind: 'card', label: 'Tarjeta' },
8
+ { kind: 'transfer', label: 'Transferencia' },
8
9
  { kind: 'wallet', label: 'Monedero' },
9
10
  { kind: 'store_credit', label: 'Saldo a favor' },
10
11
  { kind: 'credit_note', label: 'Nota de crédito (CFDI)' },
@@ -0,0 +1,23 @@
1
+ import { type RefundDestinationOption } from './refund';
2
+ import { type ReturnSubmitPayload, type ReturnableLine } from './return';
3
+ export interface ReturnWizardProps {
4
+ lines: ReturnableLine[];
5
+ /** ¿Hay un addon de almacén que confirma la recepción? Sin él, «Recibir» es un paso. */
6
+ warehouseConnected?: boolean;
7
+ /** La venta está facturada: habilita la NC CFDI con relación 01/03 automática. */
8
+ invoiced?: boolean;
9
+ /** Destinos de reembolso; `credit_note` se deshabilita solo si no está facturada. */
10
+ refundOptions?: RefundDestinationOption[];
11
+ reasons?: {
12
+ value: string;
13
+ label: string;
14
+ }[];
15
+ currency?: string;
16
+ submitting?: boolean;
17
+ /** Errores del servidor (`"<key>.<campo>"`). */
18
+ serverErrors?: Record<string, string>;
19
+ onSubmit: (payload: ReturnSubmitPayload) => void | Promise<void>;
20
+ onCancel?: () => void;
21
+ }
22
+ export declare function ReturnWizard({ lines, warehouseConnected, invoiced, refundOptions, reasons, currency, submitting, serverErrors, onSubmit, onCancel, }: ReturnWizardProps): import("react").JSX.Element;
23
+ //# sourceMappingURL=return-wizard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"return-wizard.d.ts","sourceRoot":"","sources":["../../src/business/return-wizard.tsx"],"names":[],"mappings":"AAQA,OAAO,EAGH,KAAK,uBAAuB,EAC/B,MAAM,UAAU,CAAA;AACjB,OAAO,EAcH,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACtB,MAAM,UAAU,CAAA;AAEjB,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qFAAqF;IACrF,aAAa,CAAC,EAAE,uBAAuB,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAYD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,kBAA0B,EAC1B,QAAgB,EAChB,aAA2C,EAC3C,OAAyB,EACzB,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,QAAQ,GACX,EAAE,iBAAiB,+BAyJnB"}
@@ -0,0 +1,59 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // ReturnWizard («Devolver mercancía») — un solo componente para POS, Pedido,
3
+ // Factura, RMA y OT (benchmark §7, DEV-2). No conoce endpoints: recibe los
4
+ // renglones devolvibles y entrega un payload por `onSubmit`.
5
+ import { useMemo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Button, Input } from '@asteby/metacore-ui';
8
+ import { useFormatter } from './format';
9
+ import { RefundDestination } from './refund-destination';
10
+ import { DEFAULT_REFUND_DESTINATIONS, } from './refund';
11
+ import { RETURN_CONDITION_DESTINATIONS, computeReturnTotals, creditNoteRelation, defaultStockDestination, returnSteps, returnableQty, serializeReturn, validateReturnChoices, validateReturnRefund, } from './return';
12
+ const DEFAULT_REASONS = [
13
+ { value: 'not_wanted', label: 'No lo quiso' },
14
+ { value: 'defective', label: 'Defectuoso' },
15
+ { value: 'wrong_item', label: 'Producto equivocado' },
16
+ { value: 'warranty', label: 'Garantía' },
17
+ ];
18
+ const STOCK_LABEL = { stock: 'Regresa a stock', scrap: 'Merma', warranty: 'Garantía con proveedor' };
19
+ const STEP_LABEL = { lines: 'Renglones', condition: 'Condición', receive: 'Recibir', refund: 'Reembolso' };
20
+ export function ReturnWizard({ lines, warehouseConnected = false, invoiced = false, refundOptions = DEFAULT_REFUND_DESTINATIONS, reasons = DEFAULT_REASONS, currency, submitting, serverErrors, onSubmit, onCancel, }) {
21
+ const { t } = useTranslation();
22
+ const fmt = useFormatter({ currency });
23
+ const steps = useMemo(() => returnSteps({ warehouseConnected }), [warehouseConnected]);
24
+ const [stepIdx, setStepIdx] = useState(0);
25
+ const [choices, setChoices] = useState({});
26
+ const [reason, setReason] = useState('');
27
+ const [notes, setNotes] = useState('');
28
+ const [received, setReceived] = useState(false);
29
+ const [refund, setRefund] = useState([]);
30
+ const step = steps[stepIdx];
31
+ const totals = useMemo(() => computeReturnTotals(lines, choices), [lines, choices]);
32
+ const validation = useMemo(() => validateReturnChoices(lines, choices, reason), [lines, choices, reason]);
33
+ const relation = useMemo(() => creditNoteRelation(lines, choices), [lines, choices]);
34
+ const options = useMemo(() => refundOptions.map((o) => o.kind === 'credit_note' && !invoiced
35
+ ? { ...o, available: false, disabledReason: 'La venta no está facturada' }
36
+ : o.kind === 'credit_note'
37
+ ? { ...o, label: `${o.label} · relación ${relation}` }
38
+ : o), [refundOptions, invoiced, relation]);
39
+ const refundValidation = useMemo(() => validateReturnRefund(totals.total, refund, options), [totals.total, refund, options]);
40
+ const errors = { ...validation.errors, ...serverErrors };
41
+ const setChoice = (key, patch) => setChoices((prev) => {
42
+ const cur = prev[key] ?? { quantity: 0, condition: 'sellable', destination: 'stock' };
43
+ const next = { ...cur, ...patch };
44
+ if (patch.condition && !patch.destination)
45
+ next.destination = defaultStockDestination(patch.condition);
46
+ return { ...prev, [key]: next };
47
+ });
48
+ const canNext = step === 'lines' ? validation.valid
49
+ : step === 'condition' ? validation.valid
50
+ : step === 'receive' ? received
51
+ : refundValidation.valid;
52
+ const submit = () => onSubmit(serializeReturn({ lines, choices, reason, notes, received: warehouseConnected ? false : received, refund }));
53
+ return (_jsxs("div", { "data-slot": "return-wizard", className: "space-y-4", children: [_jsxs("ol", { className: "flex flex-wrap gap-2 text-xs", children: [steps.map((s, i) => (_jsxs("li", { "aria-current": i === stepIdx ? 'step' : undefined, className: i === stepIdx ? 'font-semibold text-foreground' : 'text-muted-foreground', children: [i + 1, ". ", t(`returns.step.${s}`, { defaultValue: STEP_LABEL[s] })] }, s))), warehouseConnected && (_jsx("li", { className: "text-muted-foreground", children: t('returns.step.warehouse', { defaultValue: 'Almacén confirma la recepción' }) }))] }), (step === 'lines' || step === 'condition') && (_jsxs("div", { className: "space-y-2", children: [lines.map((l) => {
54
+ const c = choices[l.key];
55
+ const max = returnableQty(l);
56
+ return (_jsxs("div", { className: "grid gap-2 rounded-md border p-2 sm:grid-cols-[1fr_6rem_auto]", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: l.description }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [t('returns.sold', { defaultValue: 'Vendido' }), ": ", l.sold, " \u00B7 ", t('returns.returnable', { defaultValue: 'Por devolver' }), ": ", max, " \u00B7 ", fmt.money(l.unit_price)] })] }), step === 'lines' ? (_jsxs("div", { children: [_jsx(Input, { inputMode: "decimal", className: "text-right", "aria-label": l.description, disabled: max === 0, value: c ? String(c.quantity) : '', placeholder: "0", onChange: (e) => setChoice(l.key, { quantity: Number(e.target.value.replace(',', '.')) || 0 }) }), errors[`${l.key}.quantity`] && _jsx("p", { className: "mt-0.5 text-xs text-destructive", children: errors[`${l.key}.quantity`] })] })) : ((c?.quantity ?? 0) > 0 && (_jsxs("div", { className: "sm:col-span-2 flex flex-wrap items-center gap-2", children: [['sellable', 'defective'].map((cond) => (_jsx(Button, { type: "button", size: "sm", variant: c.condition === cond ? 'default' : 'outline', onClick: () => setChoice(l.key, { condition: cond }), children: cond === 'sellable' ? t('returns.sellable', { defaultValue: 'Vendible' }) : t('returns.defective', { defaultValue: 'Defectuoso' }) }, cond))), RETURN_CONDITION_DESTINATIONS[c.condition].length > 1 &&
57
+ RETURN_CONDITION_DESTINATIONS[c.condition].map((d) => (_jsx(Button, { type: "button", size: "sm", variant: c.destination === d ? 'secondary' : 'ghost', onClick: () => setChoice(l.key, { destination: d }), children: STOCK_LABEL[d] }, d))), RETURN_CONDITION_DESTINATIONS[c.condition].length === 1 && (_jsx("span", { className: "text-xs text-muted-foreground", children: STOCK_LABEL[c.destination] })), errors[`${l.key}.destination`] && _jsx("p", { className: "w-full text-xs text-destructive", children: errors[`${l.key}.destination`] })] })))] }, l.key));
58
+ }), step === 'lines' && (_jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs("select", { className: "h-9 rounded-md border bg-background px-2 text-sm", value: reason, onChange: (e) => setReason(e.target.value), "aria-label": t('returns.reason', { defaultValue: 'Motivo' }), children: [_jsxs("option", { value: "", children: [t('returns.reason', { defaultValue: 'Motivo' }), "\u2026"] }), reasons.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value)))] }), _jsx(Input, { value: notes, onChange: (e) => setNotes(e.target.value), placeholder: t('returns.notes', { defaultValue: 'Notas (opcional)' }) })] })), validation.form && stepIdx === 0 && Object.keys(choices).length > 0 && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: validation.form })] })), step === 'receive' && (_jsxs("label", { className: "flex items-start gap-2 rounded-md border p-3 text-sm", children: [_jsx("input", { type: "checkbox", className: "mt-1", checked: received, onChange: (e) => setReceived(e.target.checked) }), _jsx("span", { children: t('returns.receiveHere', { defaultValue: 'Confirmo que la mercancía ya está en tienda. No hay almacén que la reciba, así que la recepción se registra aquí.' }) })] })), step === 'refund' && (_jsx(RefundDestination, { total: totals.total, value: refund, onChange: setRefund, options: options, currency: currency, allowSplit: true, serverErrors: serverErrors })), _jsxs("div", { className: "flex items-center justify-between gap-2 border-t pt-3", children: [_jsxs("span", { className: "text-sm", children: [t('returns.total', { defaultValue: 'Total a devolver' }), ": ", _jsx("strong", { children: fmt.money(totals.total) })] }), _jsxs("div", { className: "flex gap-2", children: [onCancel && stepIdx === 0 && _jsx(Button, { type: "button", variant: "outline", onClick: onCancel, children: t('common.cancel', { defaultValue: 'Cancelar' }) }), stepIdx > 0 && _jsx(Button, { type: "button", variant: "outline", onClick: () => setStepIdx(stepIdx - 1), children: t('common.back', { defaultValue: 'Atrás' }) }), stepIdx < steps.length - 1 ? (_jsx(Button, { type: "button", disabled: !canNext, onClick: () => setStepIdx(stepIdx + 1), children: t('common.next', { defaultValue: 'Siguiente' }) })) : (_jsxs(Button, { type: "button", disabled: !canNext || submitting, onClick: submit, children: [t('returns.confirm', { defaultValue: 'Confirmar devolución' }), " ", fmt.money(totals.total)] }))] })] })] }));
59
+ }
@@ -0,0 +1,90 @@
1
+ import { type RefundAllocation, type RefundDestinationOption } from './refund';
2
+ /** Estado físico de lo que regresa. */
3
+ export type ReturnCondition = 'sellable' | 'defective';
4
+ /** A dónde va la mercancía: stock vendible, merma o garantía (reclamo al proveedor). */
5
+ export type ReturnStockDestination = 'stock' | 'scrap' | 'warranty';
6
+ export declare const RETURN_CONDITION_DESTINATIONS: Record<ReturnCondition, ReturnStockDestination[]>;
7
+ /** Destino por defecto de una condición (vendible → stock, defectuoso → merma). */
8
+ export declare function defaultStockDestination(condition: ReturnCondition): ReturnStockDestination;
9
+ /** Renglón devolvible: lo vendido menos lo ya devuelto. */
10
+ export interface ReturnableLine {
11
+ key: string;
12
+ product_id?: string;
13
+ description: string;
14
+ sold: number;
15
+ alreadyReturned?: number;
16
+ /** Precio unitario neto de descuento tal como se vendió (congelado). */
17
+ unit_price: number;
18
+ /** Fracción (0.16). */
19
+ tax_rate?: number;
20
+ }
21
+ export interface ReturnLineChoice {
22
+ quantity: number;
23
+ condition: ReturnCondition;
24
+ destination: ReturnStockDestination;
25
+ }
26
+ export type ReturnChoices = Record<string, ReturnLineChoice>;
27
+ export declare function returnableQty(l: ReturnableLine): number;
28
+ export interface ReturnTotals {
29
+ base: number;
30
+ tax: number;
31
+ total: number;
32
+ goodsLines: number;
33
+ }
34
+ export declare function computeReturnTotals(lines: ReturnableLine[], choices: ReturnChoices): ReturnTotals;
35
+ /**
36
+ * Relación SAT (c_TipoRelacion) de la NC: 03 «devolución de mercancía» si regresan
37
+ * renglones con producto; 01 «nota de crédito de los documentos relacionados» si es
38
+ * solo ajuste de importe. Misma regla que aplica el backend al proyectar la NC.
39
+ */
40
+ export declare function creditNoteRelation(lines: ReturnableLine[], choices: ReturnChoices): '01' | '03';
41
+ export interface ReturnValidation {
42
+ errors: Record<string, string>;
43
+ form?: string;
44
+ valid: boolean;
45
+ }
46
+ export declare function validateReturnChoices(lines: ReturnableLine[], choices: ReturnChoices, reason?: string, requireReason?: boolean): ReturnValidation;
47
+ /** Pasos visibles. Sin almacén, «Recibir» es un paso del mismo asistente (PIT-021). */
48
+ export type ReturnStep = 'lines' | 'condition' | 'receive' | 'refund';
49
+ export declare function returnSteps(opts: {
50
+ warehouseConnected: boolean;
51
+ }): ReturnStep[];
52
+ export interface ReturnSubmitPayload {
53
+ reason: string;
54
+ notes?: string;
55
+ /** true cuando el asistente confirmó la recepción (sin almacén). */
56
+ received: boolean;
57
+ lines: {
58
+ key: string;
59
+ product_id?: string;
60
+ quantity: number;
61
+ condition: ReturnCondition;
62
+ destination: ReturnStockDestination;
63
+ }[];
64
+ refund: RefundAllocation[];
65
+ /** Presente solo si el reembolso incluye una NC CFDI. */
66
+ credit_note?: {
67
+ relation: '01' | '03';
68
+ };
69
+ totals: ReturnTotals;
70
+ }
71
+ /** Única puerta de salida hacia el backend: solo renglones con cantidad > 0. */
72
+ export declare function serializeReturn(args: {
73
+ lines: ReturnableLine[];
74
+ choices: ReturnChoices;
75
+ reason: string;
76
+ notes?: string;
77
+ received: boolean;
78
+ refund: RefundAllocation[];
79
+ }): ReturnSubmitPayload;
80
+ /** Valida el paso de reembolso contra el total de la devolución. */
81
+ export declare function validateReturnRefund(total: number, refund: RefundAllocation[], options?: RefundDestinationOption[]): import("./refund").RefundValidation;
82
+ export type RmaState = 'draft' | 'authorized' | 'received' | 'settled' | 'closed' | 'rejected' | 'cancelled';
83
+ export declare const RMA_TRANSITIONS: Record<RmaState, RmaState[]>;
84
+ /** Estados a los que se llega con motivo obligatorio. */
85
+ export declare const RMA_REASON_REQUIRED: RmaState[];
86
+ export declare function canTransitionRma(from: RmaState, to: RmaState, reason?: string): {
87
+ ok: boolean;
88
+ error?: string;
89
+ };
90
+ //# sourceMappingURL=return.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"return.d.ts","sourceRoot":"","sources":["../../src/business/return.ts"],"names":[],"mappings":"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"}