@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.
- package/CHANGELOG.md +12 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +62 -25
- package/dist/branch-create-gate.d.ts +25 -0
- package/dist/branch-create-gate.d.ts.map +1 -0
- package/dist/branch-create-gate.js +24 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +20 -6
- package/dist/dynamic-form-schema.d.ts +9 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +45 -0
- package/dist/dynamic-form.js +12 -2
- package/dist/dynamic-line-items.d.ts +6 -1
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +38 -15
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +35 -0
- package/dist/field-validation-ui.d.ts +22 -0
- package/dist/field-validation-ui.d.ts.map +1 -0
- package/dist/field-validation-ui.js +123 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +3 -3
- package/src/__tests__/field-validation-ui.test.ts +57 -0
- package/src/__tests__/line-item-formulas.test.ts +43 -0
- package/src/action-modal-dispatcher.tsx +82 -21
- package/src/branch-create-gate.tsx +40 -0
- package/src/dialogs/dynamic-record.tsx +21 -6
- package/src/dynamic-form-schema.ts +51 -0
- package/src/dynamic-form.tsx +12 -4
- package/src/dynamic-line-items.tsx +74 -12
- package/src/dynamic-table.tsx +34 -0
- package/src/field-validation-ui.ts +140 -0
- 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":"
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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:
|
|
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) =>
|
|
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":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -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
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
package/dist/dynamic-form.js
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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;
|
|
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) =>
|
|
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
|
|
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) =>
|
|
68
|
-
|
|
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:
|
|
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) =>
|
|
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 }) {
|