@asteby/metacore-runtime-react 37.0.4 → 37.1.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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  3. package/dist/action-modal-dispatcher.js +62 -25
  4. package/dist/branch-create-gate.d.ts +25 -0
  5. package/dist/branch-create-gate.d.ts.map +1 -0
  6. package/dist/branch-create-gate.js +24 -0
  7. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  8. package/dist/dialogs/dynamic-record.js +20 -6
  9. package/dist/dynamic-form-schema.d.ts +9 -0
  10. package/dist/dynamic-form-schema.d.ts.map +1 -1
  11. package/dist/dynamic-form-schema.js +45 -0
  12. package/dist/dynamic-form.js +12 -2
  13. package/dist/dynamic-line-items.d.ts +6 -1
  14. package/dist/dynamic-line-items.d.ts.map +1 -1
  15. package/dist/dynamic-line-items.js +38 -15
  16. package/dist/dynamic-table.d.ts.map +1 -1
  17. package/dist/dynamic-table.js +35 -0
  18. package/dist/field-validation-ui.d.ts +22 -0
  19. package/dist/field-validation-ui.d.ts.map +1 -0
  20. package/dist/field-validation-ui.js +123 -0
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -1
  24. package/package.json +3 -3
  25. package/src/__tests__/field-validation-ui.test.ts +57 -0
  26. package/src/__tests__/line-item-formulas.test.ts +43 -0
  27. package/src/action-modal-dispatcher.tsx +82 -21
  28. package/src/branch-create-gate.tsx +40 -0
  29. package/src/dialogs/dynamic-record.tsx +21 -6
  30. package/src/dynamic-form-schema.ts +51 -0
  31. package/src/dynamic-form.tsx +12 -4
  32. package/src/dynamic-line-items.tsx +74 -12
  33. package/src/dynamic-table.tsx +34 -0
  34. package/src/field-validation-ui.ts +140 -0
  35. package/src/index.ts +15 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 37.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3edee3a: Branch create gate (confirm+pick when no active branch), live line-item importe formula (qty×price−discount), and drop sticky f_branch_id when leaving a locked branch defaultFilter.
8
+
9
+ ## 37.0.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 5f2a732: Action modal + line-items: preserve numeric zero (valueAsNumber), paint cell errors (`lines.0.*`), and toast a Label: message description for every field failure.
14
+
3
15
  ## 37.0.4
4
16
 
5
17
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,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;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,sCAoElB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAoEA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,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;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,sCAoElB"}
@@ -15,7 +15,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
15
15
  import { Loader2 } from 'lucide-react';
16
16
  import { toast } from 'sonner';
17
17
  import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
18
+ import { useBranchCreateGate } from './branch-create-gate';
18
19
  import { validateValues, bagHasErrors } from './validator';
20
+ import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
19
21
  import { validationCatalog } from './validation-catalog';
20
22
  import { useApi } from './api-context';
21
23
  import { DynamicIcon } from './dynamic-icon';
@@ -392,6 +394,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
392
394
  // defaultValue keeps an already-localized string unchanged.
393
395
  const tl = (s) => t(s, { defaultValue: s });
394
396
  const api = useApi();
397
+ const branchGate = useBranchCreateGate();
395
398
  const [formData, setFormData] = useState({});
396
399
  const [executing, setExecuting] = useState(false);
397
400
  // Per-field validation errors (localized), shown inline under each input.
@@ -432,41 +435,68 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
432
435
  }, [open, action.fields, record]);
433
436
  const updateField = (key, value) => {
434
437
  setFormData((prev) => ({ ...prev, [key]: value }));
435
- setFieldErrors(prev => {
436
- if (!prev[key])
437
- return prev;
438
- const next = { ...prev };
439
- delete next[key];
440
- return next;
441
- });
438
+ setFieldErrors((prev) => clearFieldErrorTree(prev, key));
442
439
  };
443
440
  const lang = i18n.language;
444
441
  const handleActionError = (err) => {
442
+ const labels = labelsForValidationFields(action.fields, t);
445
443
  const localized = localizeActionFieldErrors(err, action.fields, t, lang);
446
444
  if (localized) {
447
- setFieldErrors(localized);
448
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
445
+ // Enrich labels for dotted line-item paths before toasting.
446
+ const withPathLabels = {};
447
+ for (const [path, msg] of Object.entries(localized)) {
448
+ withPathLabels[path] = msg;
449
+ if (!labels[path])
450
+ labels[path] = labelForValidationPath(path, action.fields, t);
451
+ }
452
+ setFieldErrors(withPathLabels);
453
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
454
+ description: formatFieldErrorsDescription(withPathLabels, action.fields, t),
455
+ });
449
456
  return;
450
457
  }
451
- toastServerError(err, { t, language: lang });
458
+ toastServerError(err, { t, language: lang, labels });
452
459
  };
453
460
  const execute = async () => {
454
461
  if (action.fields) {
455
462
  const bag = validateValues(action.fields, formData);
456
463
  if (bagHasErrors(bag)) {
457
- const labels = {};
458
- for (const f of action.fields)
459
- labels[f.key] = tl(f.label);
460
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
461
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
464
+ const labels = labelsForValidationFields(action.fields, (k, o) => t(k, o));
465
+ // Exact dotted keys (`lines.0.qty`) need path-aware labels.
466
+ for (const path of Object.keys(bag)) {
467
+ if (!labels[path])
468
+ labels[path] = labelForValidationPath(path, action.fields, t);
469
+ }
470
+ const next = localizeFieldErrorMap(bag, t, { labels, language: lang });
471
+ setFieldErrors(next);
472
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
473
+ description: formatFieldErrorsDescription(next, action.fields, t),
474
+ });
462
475
  return;
463
476
  }
464
477
  }
465
478
  setFieldErrors({});
466
479
  setExecuting(true);
467
480
  try {
468
- const url = buildActionUrl(endpoint, model, record.id, action.key);
469
- const res = await api.post(url, formData);
481
+ let payload = { ...formData };
482
+ // Create-placement actions (and any create with no record) may need a
483
+ // host branch stamp when the sidebar is on "Todas". Skip when the
484
+ // form already collected branch_id, or the host has no gate.
485
+ const isCreatePlacement = action.placement === 'create' ||
486
+ record?.id == null ||
487
+ record?.id === '' ||
488
+ record?.id === 'undefined';
489
+ if (branchGate &&
490
+ isCreatePlacement &&
491
+ (payload.branch_id == null || payload.branch_id === '')) {
492
+ const branchId = await branchGate.ensureBranchForCreate(model);
493
+ if (branchId === null)
494
+ return;
495
+ if (branchId)
496
+ payload = { ...payload, branch_id: branchId };
497
+ }
498
+ const url = buildActionUrl(endpoint, model, record?.id, action.key);
499
+ const res = await api.post(url, payload);
470
500
  if (res.data.success) {
471
501
  toastServerSuccess(res.data, { t });
472
502
  onOpenChange(false);
@@ -502,7 +532,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
502
532
  const fullWidth = isLineItemsField(field) ||
503
533
  resolveWidget(field) === 'textarea' ||
504
534
  resolveWidget(field) === 'richtext';
505
- 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[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
535
+ 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));
506
536
  }), 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)] })] })] }) }));
507
537
  }
508
538
  // buildFieldDefaults seeds formData for a set of action fields, honoring the
@@ -648,24 +678,28 @@ function renderField(field, value, onChange,
648
678
  // header picker) resolve a `dependsOn` reference against sibling header
649
679
  // fields. Omitted by callers that have no surrounding form (the field is
650
680
  // then treated as having no resolvable dependency).
651
- formValues, record) {
681
+ formValues, record, fieldErrors) {
652
682
  // Repeatable line-items group → row grid (value is an array of row objects).
653
683
  // The header form values flow in so a cell can depend on a header field.
654
684
  if (isLineItemsField(field)) {
655
- return _jsx(DynamicLineItems, { field: applyPrefillLock(field), value: value, onChange: onChange, formValues: formValues });
685
+ return (_jsx(DynamicLineItems, { field: applyPrefillLock(field), value: value, onChange: onChange, formValues: formValues, errors: lineItemErrorsFor(field.key, fieldErrors) }));
656
686
  }
657
687
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
658
688
  // else inferred from type) so action modals and the standalone form stay in
659
689
  // lockstep — previously this switch keyed off `field.type` and silently
660
690
  // dropped `dynamic_select` to a plain text input.
661
691
  const widget = resolveWidget(field);
692
+ const invalid = !!(fieldErrors && fieldErrors[field.key]);
693
+ const invalidCls = invalid
694
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
695
+ : '';
662
696
  if (widget === 'dynamic_select') {
663
697
  // A header-level dynamic_select may itself depend on another header
664
698
  // field; resolve its filter_value from the form context.
665
699
  const dependsValue = getDependsOn(field)
666
700
  ? resolveDependsValue(field, formValues)
667
701
  : undefined;
668
- return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, seedOption: seedOptionFromRecord(field, value, record) }));
702
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, seedOption: seedOptionFromRecord(field, value, record), invalid: invalid }));
669
703
  }
670
704
  // File upload → themed picker that POSTs the file to the host upload
671
705
  // endpoint and stores the returned url/path. Kept in sync with DynamicForm.
@@ -674,18 +708,21 @@ formValues, record) {
674
708
  }
675
709
  switch (widget) {
676
710
  case 'textarea':
677
- return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
711
+ return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: invalidCls || undefined });
678
712
  case 'select':
679
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
713
+ return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: 'w-full' + (invalidCls ? ` ${invalidCls}` : ''), "aria-invalid": invalid || undefined, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
680
714
  case 'switch':
681
715
  return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
682
716
  case 'number':
683
- return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder });
717
+ return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => {
718
+ const n = e.target.valueAsNumber;
719
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
720
+ }, placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: invalidCls || undefined });
684
721
  case 'date':
685
722
  // Modern shadcn Calendar in a Popover (portaled, never clipped by the
686
723
  // modal) instead of the native, dated, easily-cut <input type=date>.
687
724
  return _jsx(DynamicDateField, { field: field, value: value, onChange: onChange });
688
725
  default:
689
- return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
726
+ return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: invalidCls || undefined });
690
727
  }
691
728
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Optional host gate for stamping `branch_id` on creates when the sidebar is
3
+ * on "all branches" (no active scope).
4
+ *
5
+ * Ops (and other hosts) provide a provider that opens a confirm + branch
6
+ * picker. The SDK's DynamicRecordDialog / ActionModalDispatcher call
7
+ * `ensureBranchForCreate` right before POST — custom federated modals can use
8
+ * the same hook so POS-style flows stay consistent.
9
+ *
10
+ * Return contract for `ensureBranchForCreate`:
11
+ * - `undefined` — model has no branch_id / gate not needed / already scoped
12
+ * - `string` — branch id to stamp onto the payload
13
+ * - `null` — user cancelled the picker (abort submit)
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ export type BranchCreateGateApi = {
17
+ ensureBranchForCreate: (model: string) => Promise<string | null | undefined>;
18
+ };
19
+ export declare function BranchCreateGateProvider({ value, children, }: {
20
+ value: BranchCreateGateApi;
21
+ children: ReactNode;
22
+ }): import("react").JSX.Element;
23
+ /** Hook for custom / federated create modals. Returns null when no host gate. */
24
+ export declare function useBranchCreateGate(): BranchCreateGateApi | null;
25
+ //# sourceMappingURL=branch-create-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-create-gate.d.ts","sourceRoot":"","sources":["../src/branch-create-gate.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjE,MAAM,MAAM,mBAAmB,GAAG;IAChC,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;CAC7E,CAAA;AAID,wBAAgB,wBAAwB,CAAC,EACvC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,mBAAmB,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;CACpB,+BAMA;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,IAAI,mBAAmB,GAAG,IAAI,CAEhE"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Optional host gate for stamping `branch_id` on creates when the sidebar is
4
+ * on "all branches" (no active scope).
5
+ *
6
+ * Ops (and other hosts) provide a provider that opens a confirm + branch
7
+ * picker. The SDK's DynamicRecordDialog / ActionModalDispatcher call
8
+ * `ensureBranchForCreate` right before POST — custom federated modals can use
9
+ * the same hook so POS-style flows stay consistent.
10
+ *
11
+ * Return contract for `ensureBranchForCreate`:
12
+ * - `undefined` — model has no branch_id / gate not needed / already scoped
13
+ * - `string` — branch id to stamp onto the payload
14
+ * - `null` — user cancelled the picker (abort submit)
15
+ */
16
+ import { createContext, useContext } from 'react';
17
+ const BranchCreateGateContext = createContext(null);
18
+ export function BranchCreateGateProvider({ value, children, }) {
19
+ return (_jsx(BranchCreateGateContext.Provider, { value: value, children: children }));
20
+ }
21
+ /** Hook for custom / federated create modals. Returns null when no host gate. */
22
+ export function useBranchCreateGate() {
23
+ return useContext(BranchCreateGateContext);
24
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AA8C1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAoB3C,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;IACzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,WAAW,CAAA;IAC1B,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AA0CD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAK1D;AAaD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAgBtE;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,SAAS,CAExE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CASrE;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,cAAc,GAAG,IAAI,CAa5F;AA+GD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAaD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,QAAQ,EAAE,CAOZ;AAUD,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAiBD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAkkB1B;AAuED,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,+BAelF;AA+GD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BA6SA;AAwLD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IACnE,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,+BAiNA"}
@@ -23,6 +23,7 @@ import { es } from 'date-fns/locale';
23
23
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react';
24
24
  import { BarcodeScanner } from '../barcode-scanner';
25
25
  import { useApi } from '../api-context';
26
+ import { useBranchCreateGate } from '../branch-create-gate';
26
27
  import { toastServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap } from '../server-error';
27
28
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
28
29
  import { DynamicRelations } from '../dynamic-relations';
@@ -319,6 +320,7 @@ function applyEnsureFields(meta, ensureFields) {
319
320
  }
320
321
  export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf, ensureFields, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
321
322
  const api = useApi();
323
+ const branchGate = useBranchCreateGate();
322
324
  const { t } = useTranslation();
323
325
  const [modalMeta, setModalMeta] = useState(schema ? schema : null);
324
326
  const [relations, setRelations] = useState([]);
@@ -556,11 +558,14 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
556
558
  labels[f.key] = f.label;
557
559
  const next = localizeFieldErrorMap(bag, t, { labels });
558
560
  setFieldErrors(next);
559
- const visibleKeys = new Set(visible.map(f => f.key));
560
- const orphans = Object.entries(next).filter(([k]) => !visibleKeys.has(k));
561
- const description = orphans.length
562
- ? orphans.map(([k, msg]) => `${labelForKey(k)}: ${msg}`).join(' · ')
563
- : undefined;
561
+ const description = Object.entries(next)
562
+ .map(([k, msg]) => {
563
+ const label = labels[k] || labelForKey(k);
564
+ return msg.toLowerCase().startsWith(String(label).toLowerCase())
565
+ ? msg
566
+ : `${label}: ${msg}`;
567
+ })
568
+ .join(' · ');
564
569
  toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }), description ? { description } : undefined);
565
570
  return;
566
571
  }
@@ -575,7 +580,16 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
575
580
  const submittedValues = stripHiddenFieldValues(formValues, modalMeta.fields, mode);
576
581
  // Empty reference pickers → null (not "" / nil-UUID) so nullable FK
577
582
  // columns accept them instead of raising a 23503 FK violation.
578
- const payload = normalizeRefFieldsForSubmit(submittedValues, modalMeta.fields);
583
+ let payload = normalizeRefFieldsForSubmit(submittedValues, modalMeta.fields);
584
+ if (isCreate &&
585
+ branchGate &&
586
+ (payload.branch_id == null || payload.branch_id === '')) {
587
+ const branchId = await branchGate.ensureBranchForCreate(model);
588
+ if (branchId === null)
589
+ return;
590
+ if (branchId)
591
+ payload = { ...payload, branch_id: branchId };
592
+ }
579
593
  setSaving(true);
580
594
  try {
581
595
  if (isCreate && onCreate) {
@@ -38,6 +38,15 @@ export declare function toNumber(v: unknown): number;
38
38
  * drift (0.1 + 0.2 noise) that would make a genuinely balanced entry look off.
39
39
  */
40
40
  export declare function computeLineItemTotals(field: ActionFieldDef, rows: any[] | undefined): Record<string, number>;
41
+ /**
42
+ * Live line-amount formula for action / form line-items grids:
43
+ * `(qty|quantity) * (unit_price|…) - (discount|…)` → `subtotal|line_total|importe`.
44
+ *
45
+ * Pure + convention-based so create_sales_order (and similar modals) show
46
+ * Importe as the user types without each manifest declaring a client formula.
47
+ * No-op when the row has no amount column or no qty/price pair.
48
+ */
49
+ export declare function applyLineItemRowFormulas(itemFields: ActionFieldDef[], row: Record<string, any>): Record<string, any>;
41
50
  export interface BalanceState {
42
51
  debit: number;
43
52
  credit: number;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"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"}
@@ -111,6 +111,51 @@ export function computeLineItemTotals(field, rows) {
111
111
  totals[k] = Math.round(totals[k] * 100) / 100;
112
112
  return totals;
113
113
  }
114
+ /** Keys that mean "line net amount" across manifests / locales. */
115
+ const LINE_AMOUNT_KEYS = new Set(['subtotal', 'line_total', 'importe', 'amount', 'total']);
116
+ /** Keys that mean quantity on a line row. */
117
+ const LINE_QTY_KEYS = new Set(['qty', 'quantity', 'cantidad']);
118
+ /** Keys that mean unit price. */
119
+ const LINE_PRICE_KEYS = new Set(['unit_price', 'price', 'precio', 'precio_unitario']);
120
+ /** Keys that mean line discount (absolute). */
121
+ const LINE_DISCOUNT_KEYS = new Set(['discount', 'descuento', 'discount_amount']);
122
+ function firstKey(rowOrFields, candidates) {
123
+ if (Array.isArray(rowOrFields)) {
124
+ for (const f of rowOrFields) {
125
+ if (f.key && candidates.has(f.key))
126
+ return f.key;
127
+ }
128
+ return undefined;
129
+ }
130
+ for (const k of Object.keys(rowOrFields)) {
131
+ if (candidates.has(k))
132
+ return k;
133
+ }
134
+ return undefined;
135
+ }
136
+ /**
137
+ * Live line-amount formula for action / form line-items grids:
138
+ * `(qty|quantity) * (unit_price|…) - (discount|…)` → `subtotal|line_total|importe`.
139
+ *
140
+ * Pure + convention-based so create_sales_order (and similar modals) show
141
+ * Importe as the user types without each manifest declaring a client formula.
142
+ * No-op when the row has no amount column or no qty/price pair.
143
+ */
144
+ export function applyLineItemRowFormulas(itemFields, row) {
145
+ const amountKey = firstKey(itemFields, LINE_AMOUNT_KEYS) ?? firstKey(row, LINE_AMOUNT_KEYS);
146
+ const qtyKey = firstKey(itemFields, LINE_QTY_KEYS) ?? firstKey(row, LINE_QTY_KEYS);
147
+ const priceKey = firstKey(itemFields, LINE_PRICE_KEYS) ?? firstKey(row, LINE_PRICE_KEYS);
148
+ if (!amountKey || !qtyKey || !priceKey)
149
+ return row;
150
+ const discountKey = firstKey(itemFields, LINE_DISCOUNT_KEYS) ?? firstKey(row, LINE_DISCOUNT_KEYS);
151
+ const qty = toNumber(row[qtyKey]);
152
+ const price = toNumber(row[priceKey]);
153
+ const discount = discountKey ? toNumber(row[discountKey]) : 0;
154
+ const next = Math.round((qty * price - discount) * 100) / 100;
155
+ if (toNumber(row[amountKey]) === next)
156
+ return row;
157
+ return { ...row, [amountKey]: next };
158
+ }
114
159
  /**
115
160
  * Evaluates a line-items field's balance rule against its rows. Returns
116
161
  * `undefined` when the field declares no balance rule. `balanced` is true when
@@ -238,7 +238,10 @@ function FieldRenderer({ field, value, onChange, initialValues, effectiveOptions
238
238
  case 'switch':
239
239
  return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
240
240
  case 'number':
241
- return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder });
241
+ return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => {
242
+ const n = e.target.valueAsNumber;
243
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
244
+ }, placeholder: field.placeholder });
242
245
  case 'date':
243
246
  return _jsx(DynamicDateField, { field: field, value: value, onChange: onChange });
244
247
  default:
@@ -263,7 +266,14 @@ function ScannableInput({ field, value, onChange, type, }) {
263
266
  // degrada con un mensaje ("usá un lector físico / escribí a mano") cuando no
264
267
  // hay soporte, así que mostrar el botón es seguro y consistente.
265
268
  const scanEnabled = !!(field.scan ?? field.scannable);
266
- const input = (_jsx(Input, { id: field.key, type: type, value: value ?? '', onChange: (e) => onChange(type === 'number' ? e.target.valueAsNumber || '' : e.target.value), placeholder: field.placeholder }));
269
+ const input = (_jsx(Input, { id: field.key, type: type, value: value ?? '', onChange: (e) => {
270
+ if (type !== 'number') {
271
+ onChange(e.target.value);
272
+ return;
273
+ }
274
+ const n = e.target.valueAsNumber;
275
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
276
+ }, placeholder: field.placeholder }));
267
277
  if (!scanEnabled)
268
278
  return input;
269
279
  return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsx("div", { className: "min-w-0 flex-1", children: input }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) }), _jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: (code) => onChange(code),
@@ -10,6 +10,11 @@ export interface DynamicLineItemsProps {
10
10
  * (e.g. `source_warehouse_id`), not just a sibling cell on the same row.
11
11
  */
12
12
  formValues?: Record<string, any>;
13
+ /**
14
+ * Localized validation messages keyed as `rowIndex.columnKey` (e.g.
15
+ * `0.unit_price`). Painted as destructive borders + under-cell text.
16
+ */
17
+ errors?: Record<string, string>;
13
18
  }
14
- export declare function DynamicLineItems({ field, value, onChange, disabled, formValues }: DynamicLineItemsProps): import("react").JSX.Element;
19
+ export declare function DynamicLineItems({ field, value, onChange, disabled, formValues, errors }: DynamicLineItemsProps): import("react").JSX.Element;
15
20
  //# sourceMappingURL=dynamic-line-items.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BA6O/G"}
1
+ {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAiB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,qBAAqB,+BAwQvH"}
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
12
12
  import { useEffect, useRef } from 'react';
13
13
  import { Plus, Trash2, Check } from 'lucide-react';
14
- import { resolveWidget, getItemFields, computeLineItemTotals, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
14
+ import { resolveWidget, getItemFields, computeLineItemTotals, applyLineItemRowFormulas, evaluateBalance, toNumber, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, applyOptionWhen, } from './dynamic-form-schema';
15
15
  import { DynamicSelectField, DEFAULT_DEPENDS_HINT } from './dynamic-select-field';
16
16
  import { useOptionsResolver } from './use-options-resolver';
17
17
  const fmtNumber = (n) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
@@ -26,9 +26,10 @@ function emptyRow(itemFields) {
26
26
  }
27
27
  return row;
28
28
  }
29
- export function DynamicLineItems({ field, value, onChange, disabled = false, formValues }) {
29
+ export function DynamicLineItems({ field, value, onChange, disabled = false, formValues, errors }) {
30
30
  const itemFields = getItemFields(field);
31
31
  const rows = Array.isArray(value) ? value : [];
32
+ const errMap = errors ?? {};
32
33
  // `lock_rows` fixes the row set: no add-row button, no per-row delete. Rows
33
34
  // stay editable cell-by-cell. Snake_case is what the kernel serves; tolerate
34
35
  // the camelCase alias too. Derived once.
@@ -39,9 +40,11 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
39
40
  const totalKeys = itemFields.filter((c) => c.total).map((c) => c.key);
40
41
  const hasTotals = totalKeys.length > 0;
41
42
  const balance = evaluateBalance(field, rows);
42
- const addRow = () => onChange([...rows, emptyRow(itemFields)]);
43
+ const addRow = () => onChange([...rows, applyLineItemRowFormulas(itemFields, emptyRow(itemFields))]);
43
44
  const removeRow = (idx) => onChange(rows.filter((_, i) => i !== idx));
44
- const updateCell = (idx, key, cellValue) => onChange(rows.map((r, i) => (i === idx ? { ...r, [key]: cellValue } : r)));
45
+ const updateCell = (idx, key, cellValue) => onChange(rows.map((r, i) => i === idx
46
+ ? applyLineItemRowFormulas(itemFields, { ...r, [key]: cellValue })
47
+ : r));
45
48
  // When a balance rule reconciles two columns (e.g. debit ↔ credit), typing
46
49
  // into one clears the sibling on the same row — mirrors the federated modal
47
50
  // UX so a line is never both a debit and a credit.
@@ -58,14 +61,26 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
58
61
  if (balancePair && (key === balancePair[0] || key === balancePair[1])) {
59
62
  const sibling = key === balancePair[0] ? balancePair[1] : balancePair[0];
60
63
  const hasValue = toNumber(cellValue) > 0;
61
- onChange(rows.map((r, i) => i === idx ? { ...r, [key]: cellValue, ...(hasValue ? { [sibling]: '' } : {}) } : r));
64
+ onChange(rows.map((r, i) => i === idx
65
+ ? applyLineItemRowFormulas(itemFields, {
66
+ ...r,
67
+ [key]: cellValue,
68
+ ...(hasValue ? { [sibling]: '' } : {}),
69
+ })
70
+ : r));
62
71
  return;
63
72
  }
64
73
  updateCell(idx, key, cellValue);
65
74
  };
66
75
  const totalCols = itemFields.filter((c) => c.total);
67
- return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row })] }, col.key))) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
68
- (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row }) }, col.key))), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
76
+ return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => {
77
+ const cellErr = errMap[`${idx}.${col.key}`];
78
+ return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive text-xs", children: cellErr }))] }, col.key));
79
+ }) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
80
+ (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => {
81
+ const cellErr = errMap[`${idx}.${col.key}`];
82
+ return (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive mt-1 text-xs", children: cellErr }))] }, col.key));
83
+ }), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
69
84
  if (ci === 0) {
70
85
  return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
71
86
  }
@@ -86,8 +101,11 @@ function BalanceBadge({ state, }) {
86
101
  // without the per-field Label (the column header is the label) and sized for a
87
102
  // table cell. Nested line-items inside a row are not supported (a row column is
88
103
  // a scalar widget).
89
- function CellRenderer({ field, value, onChange, disabled, formValues, rowValues }) {
104
+ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues, invalid }) {
90
105
  const widget = resolveWidget(field);
106
+ const invalidCls = invalid
107
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
108
+ : '';
91
109
  // Per-field read-only: a column locked by a PrefillSpec.lock (e.g. the
92
110
  // "ordered" / "already received" progress columns of a receive-goods modal)
93
111
  // renders disabled so it shows context without being editable. Tolerates the
@@ -119,7 +137,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
119
137
  // Async searchable picker per row cell — e.g. the account_id column of a
120
138
  // journal entry's debit/credit lines. Same widget as the flat form.
121
139
  if (widget === 'dynamic_select') {
122
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro });
140
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro, invalid: invalid }));
123
141
  }
124
142
  if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
125
143
  return (_jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled, formValues: formValues, rowValues: rowValues }));
@@ -127,24 +145,29 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
127
145
  switch (widget) {
128
146
  case 'textarea':
129
147
  case 'richtext':
130
- return (_jsx(Textarea, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, rows: 2 }));
148
+ return (_jsx(Textarea, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, rows: 2, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
131
149
  case 'color':
132
- return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: off }));
150
+ return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
133
151
  case 'select': {
134
152
  const opts = effectiveOptions ?? field.options;
135
153
  // No option applies under the current sibling value → hide the cell.
136
154
  if (effectiveOptions && effectiveOptions.length === 0)
137
155
  return null;
138
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: off, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: opts?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
156
+ return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: off, children: [_jsx(SelectTrigger, { className: 'w-full' + (invalidCls ? ` ${invalidCls}` : ''), "aria-invalid": invalid || undefined, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: opts?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
139
157
  }
140
158
  case 'switch':
141
159
  return _jsx(Switch, { checked: !!value, onCheckedChange: onChange, disabled: off });
142
160
  case 'number':
143
- return (_jsx(Input, { type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder, disabled: off }));
161
+ return (_jsx(Input, { type: "number", value: value ?? '', onChange: (e) => {
162
+ const n = e.target.valueAsNumber;
163
+ // `0 || ''` would wipe a legitimate zero (unit_price=0)
164
+ // and then required-validation fires with nothing marked.
165
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
166
+ }, placeholder: field.placeholder, disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
144
167
  case 'date':
145
- return (_jsx(Input, { type: "date", value: value || '', onChange: (e) => onChange(e.target.value), disabled: off }));
168
+ return (_jsx(Input, { type: "date", value: value || '', onChange: (e) => onChange(e.target.value), disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
146
169
  default:
147
- return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off }));
170
+ return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
148
171
  }
149
172
  }
150
173
  function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {