@asteby/metacore-runtime-react 47.0.0 → 48.0.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 (178) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  3. package/dist/action-modal-dispatcher.js +24 -4
  4. package/dist/audit-info.d.ts +54 -0
  5. package/dist/audit-info.d.ts.map +1 -0
  6. package/dist/audit-info.js +142 -0
  7. package/dist/business/catalog-product-search.d.ts +25 -0
  8. package/dist/business/catalog-product-search.d.ts.map +1 -0
  9. package/dist/business/catalog-product-search.js +57 -0
  10. package/dist/business/catalog.d.ts.map +1 -1
  11. package/dist/business/catalog.js +2 -0
  12. package/dist/business/document-editor-model.d.ts +107 -0
  13. package/dist/business/document-editor-model.d.ts.map +1 -0
  14. package/dist/business/document-editor-model.js +279 -0
  15. package/dist/business/document-editor-panels.d.ts +64 -0
  16. package/dist/business/document-editor-panels.d.ts.map +1 -0
  17. package/dist/business/document-editor-panels.js +58 -0
  18. package/dist/business/document-editor.d.ts +27 -0
  19. package/dist/business/document-editor.d.ts.map +1 -0
  20. package/dist/business/document-editor.js +445 -0
  21. package/dist/business/document-lines.d.ts.map +1 -1
  22. package/dist/business/document-lines.js +5 -1
  23. package/dist/business/index.d.ts +5 -0
  24. package/dist/business/index.d.ts.map +1 -1
  25. package/dist/business/index.js +5 -0
  26. package/dist/business/line-items.d.ts +7 -0
  27. package/dist/business/line-items.d.ts.map +1 -1
  28. package/dist/business/line-items.js +6 -1
  29. package/dist/business/payment-allocator.d.ts +19 -0
  30. package/dist/business/payment-allocator.d.ts.map +1 -0
  31. package/dist/business/payment-allocator.js +31 -0
  32. package/dist/business/product-search.d.ts +4 -0
  33. package/dist/business/product-search.d.ts.map +1 -1
  34. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  35. package/dist/dialogs/dynamic-record.js +19 -8
  36. package/dist/document-form-dialog.d.ts +31 -2
  37. package/dist/document-form-dialog.d.ts.map +1 -1
  38. package/dist/document-form-dialog.js +43 -11
  39. package/dist/dynamic-columns-helpers.d.ts +9 -0
  40. package/dist/dynamic-columns-helpers.d.ts.map +1 -1
  41. package/dist/dynamic-columns-helpers.js +19 -0
  42. package/dist/dynamic-columns.d.ts +8 -2
  43. package/dist/dynamic-columns.d.ts.map +1 -1
  44. package/dist/dynamic-columns.js +21 -5
  45. package/dist/dynamic-crud-page.d.ts +6 -0
  46. package/dist/dynamic-crud-page.d.ts.map +1 -1
  47. package/dist/dynamic-crud-page.js +14 -5
  48. package/dist/dynamic-form-schema.d.ts.map +1 -1
  49. package/dist/dynamic-form-schema.js +15 -3
  50. package/dist/dynamic-form.d.ts +16 -1
  51. package/dist/dynamic-form.d.ts.map +1 -1
  52. package/dist/dynamic-form.js +81 -20
  53. package/dist/dynamic-kanban.d.ts +9 -3
  54. package/dist/dynamic-kanban.d.ts.map +1 -1
  55. package/dist/dynamic-kanban.js +39 -13
  56. package/dist/dynamic-line-items.d.ts.map +1 -1
  57. package/dist/dynamic-line-items.js +3 -0
  58. package/dist/dynamic-multi-select-field.d.ts.map +1 -1
  59. package/dist/dynamic-multi-select-field.js +2 -0
  60. package/dist/dynamic-relation-helpers.d.ts.map +1 -1
  61. package/dist/dynamic-relation-helpers.js +8 -0
  62. package/dist/dynamic-relation.js +1 -1
  63. package/dist/dynamic-select-field.d.ts.map +1 -1
  64. package/dist/dynamic-select-field.js +4 -0
  65. package/dist/index.d.ts +8 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +17 -2
  68. package/dist/installed-addons-context.d.ts +15 -0
  69. package/dist/installed-addons-context.d.ts.map +1 -0
  70. package/dist/installed-addons-context.js +39 -0
  71. package/dist/option-filter.d.ts +31 -0
  72. package/dist/option-filter.d.ts.map +1 -0
  73. package/dist/option-filter.js +49 -0
  74. package/dist/org-runtime-context.d.ts +8 -0
  75. package/dist/org-runtime-context.d.ts.map +1 -1
  76. package/dist/org-runtime-context.js +8 -0
  77. package/dist/org-runtime-provider.d.ts +3 -1
  78. package/dist/org-runtime-provider.d.ts.map +1 -1
  79. package/dist/org-runtime-provider.js +3 -3
  80. package/dist/primitives/aging.d.ts +34 -0
  81. package/dist/primitives/aging.d.ts.map +1 -0
  82. package/dist/primitives/aging.js +48 -0
  83. package/dist/primitives/allocation.d.ts +42 -0
  84. package/dist/primitives/allocation.d.ts.map +1 -0
  85. package/dist/primitives/allocation.js +59 -0
  86. package/dist/primitives/approvals.d.ts +71 -0
  87. package/dist/primitives/approvals.d.ts.map +1 -0
  88. package/dist/primitives/approvals.js +48 -0
  89. package/dist/primitives/contributions.d.ts +82 -0
  90. package/dist/primitives/contributions.d.ts.map +1 -0
  91. package/dist/primitives/contributions.js +126 -0
  92. package/dist/primitives/document-kinds.d.ts +25 -0
  93. package/dist/primitives/document-kinds.d.ts.map +1 -0
  94. package/dist/primitives/document-kinds.js +26 -0
  95. package/dist/primitives/index.d.ts +8 -0
  96. package/dist/primitives/index.d.ts.map +1 -0
  97. package/dist/primitives/index.js +23 -0
  98. package/dist/primitives/panels.d.ts +68 -0
  99. package/dist/primitives/panels.d.ts.map +1 -0
  100. package/dist/primitives/panels.js +3 -0
  101. package/dist/primitives/use-contributions.d.ts +15 -0
  102. package/dist/primitives/use-contributions.d.ts.map +1 -0
  103. package/dist/primitives/use-contributions.js +30 -0
  104. package/dist/record-prefill-bar.d.ts +11 -0
  105. package/dist/record-prefill-bar.d.ts.map +1 -0
  106. package/dist/record-prefill-bar.js +33 -0
  107. package/dist/types.d.ts +142 -0
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/use-options-resolver.d.ts +8 -0
  110. package/dist/use-options-resolver.d.ts.map +1 -1
  111. package/dist/use-options-resolver.js +8 -2
  112. package/package.json +5 -5
  113. package/src/__tests__/action-modal-grid-layout.test.tsx +1 -1
  114. package/src/__tests__/apply-sidebar-layout.test.ts +1 -1
  115. package/src/__tests__/audit-info.test.tsx +103 -0
  116. package/src/__tests__/catalog-product-search.test.ts +61 -0
  117. package/src/__tests__/dependent-options.test.tsx +12 -5
  118. package/src/__tests__/document-editor-model.test.ts +167 -0
  119. package/src/__tests__/document-editor.test.tsx +261 -0
  120. package/src/__tests__/document-form-dialog.test.tsx +118 -3
  121. package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
  122. package/src/__tests__/dynamic-kanban.test.tsx +24 -3
  123. package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
  124. package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +4 -0
  125. package/src/__tests__/installed-addons-context.test.tsx +50 -0
  126. package/src/__tests__/line-item-totals.test.ts +28 -0
  127. package/src/__tests__/line-items-lock-rows.test.tsx +2 -1
  128. package/src/__tests__/option-filter.test.tsx +115 -0
  129. package/src/__tests__/primitives-federated.test.tsx +156 -0
  130. package/src/__tests__/primitives.test.ts +224 -0
  131. package/src/__tests__/record-prefill-bar.test.tsx +50 -0
  132. package/src/__tests__/structured-view-value.test.tsx +5 -1
  133. package/src/__tests__/wizard-action-modal.test.tsx +1 -1
  134. package/src/action-modal-dispatcher.tsx +47 -4
  135. package/src/audit-info.tsx +219 -0
  136. package/src/business/catalog-product-search.ts +87 -0
  137. package/src/business/catalog.ts +2 -0
  138. package/src/business/document-editor-model.ts +325 -0
  139. package/src/business/document-editor-panels.tsx +164 -0
  140. package/src/business/document-editor.tsx +727 -0
  141. package/src/business/document-lines.ts +5 -1
  142. package/src/business/index.ts +5 -0
  143. package/src/business/line-items.ts +15 -1
  144. package/src/business/payment-allocator.tsx +110 -0
  145. package/src/business/product-search.ts +4 -0
  146. package/src/dialogs/dynamic-record.tsx +42 -6
  147. package/src/document-form-dialog.tsx +92 -12
  148. package/src/dropdown-menu-entry.test.ts +23 -0
  149. package/src/dynamic-columns-helpers.ts +22 -0
  150. package/src/dynamic-columns.tsx +23 -6
  151. package/src/dynamic-crud-page.tsx +22 -3
  152. package/src/dynamic-form-schema.ts +15 -2
  153. package/src/dynamic-form.tsx +120 -39
  154. package/src/dynamic-kanban.tsx +41 -10
  155. package/src/dynamic-line-items.tsx +3 -0
  156. package/src/dynamic-multi-select-field.tsx +2 -0
  157. package/src/dynamic-relation-helpers.ts +9 -0
  158. package/src/dynamic-relation.tsx +7 -2
  159. package/src/dynamic-select-field.tsx +4 -0
  160. package/src/index.ts +34 -1
  161. package/src/installed-addons-context.tsx +58 -0
  162. package/src/option-filter.ts +88 -0
  163. package/src/org-runtime-context.ts +10 -0
  164. package/src/org-runtime-provider.tsx +7 -2
  165. package/src/primitives/aging.ts +76 -0
  166. package/src/primitives/allocation.ts +104 -0
  167. package/src/primitives/approvals.ts +103 -0
  168. package/src/primitives/contributions.ts +208 -0
  169. package/src/primitives/document-kinds.ts +62 -0
  170. package/src/primitives/index.ts +23 -0
  171. package/src/primitives/panels.ts +59 -0
  172. package/src/primitives/use-contributions.ts +42 -0
  173. package/src/record-prefill-bar.tsx +62 -0
  174. package/src/row-action-stage-gate.test.ts +33 -0
  175. package/src/types.ts +142 -0
  176. package/src/use-options-resolver.ts +19 -1
  177. package/vitest.config.ts +1 -0
  178. package/vitest.setup.ts +7 -0
@@ -0,0 +1,445 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // DocumentEditor — alta de un documento comercial en UNA pantalla por
3
+ // secciones: factura, NC, cobro/REP, cotización, OC… Sustituye al wizard
4
+ // «Datos → Siguiente → Renglones» de DocumentFormDialog cuando el tipo declara
5
+ // `layout: "editor"` en `document_forms` (lo sirve el kernel ≥ v0.190.0).
6
+ //
7
+ // Contraparte + lo esencial (PartyCard: datos clave y crédito)
8
+ // Cargar desde… (sources: el renglón/encabezado se precarga)
9
+ // Renglones | Reparto del pago (DocumentLinesGrid con catálogo | PaymentAllocator)
10
+ // Totales
11
+ // ▸ Opciones fiscales (plegada: extensiones fiscal_data.*, catálogos
12
+ // con default y contribuciones header.fields)
13
+ // Secciones de otros addons (body.sections / side.panels)
14
+ // ▸ Vista previa (plegada; acción `preview` con analyze: true)
15
+ // [Cancelar] [Acción primaria única]
16
+ //
17
+ // Pensado para quien hace varios roles: a la vista solo lo que hay que decidir;
18
+ // lo fiscal trae defaults (de la contraparte y del manifest) y se revisa solo si
19
+ // hace falta. Genérico y sin país: fiscal_mexico aporta sus campos como
20
+ // extensiones o contribuciones; otra localización hace lo mismo.
21
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
+ import { useTranslation } from 'react-i18next';
23
+ import { Button } from '@asteby/metacore-ui/primitives';
24
+ import { Loader2 } from 'lucide-react';
25
+ import { toast } from 'sonner';
26
+ import { useApi } from '../api-context';
27
+ import { useOrgTaxRate } from '../org-runtime-context';
28
+ import { FieldCell, FieldGrid, FieldLabel } from '../field-grid';
29
+ import { FormErrorBanner } from './feedback';
30
+ import { DocumentLinesGrid } from './line-items-editor';
31
+ import { computeTotals, serializeLineItems, taxBreakdown } from './line-items';
32
+ import { roundMoney, toAmount, useFormatter } from './format';
33
+ import { createCatalogProductSearch } from './catalog-product-search';
34
+ import { allocationAmountField, allocationIssueMessages, allocationPayload, creditStatus, editorLinesConfig, linesFromSource, localIssues, partyDefaults, partySummaryRows, splitEditorFields, toOpenDocuments, withFriendlyOptions, } from './document-editor-model';
35
+ import { CollapsibleSection, EditorSection, PartyCard, PreviewPanel, TotalsPanel, ValidationChecklist, } from './document-editor-panels';
36
+ import { PaymentAllocator } from './payment-allocator';
37
+ import { actionErrorBannerMessage, buildFieldDefaults, prefillFromFieldRelationRequest, renderField } from '../action-modal-dispatcher';
38
+ import { extractFieldErrors, localizeFieldErrorMap } from '../server-error';
39
+ import { validateValues, bagHasErrors } from '../validator';
40
+ import { clearFieldErrorTree } from '../field-validation-ui';
41
+ import { emitRecordMutation } from '../record-mutation-events';
42
+ import { evaluateVisibleWhen, getVisibleWhen } from '../dynamic-form-schema';
43
+ import { useInstalledAddons } from '../installed-addons-context';
44
+ import { allocatePayment, validateAllocation } from '../primitives/allocation';
45
+ import { useDocumentContributions } from '../primitives/use-contributions';
46
+ export function DocumentEditor({ model, endpoint, forms, type, record, initialSource, onSaved, onCancel, searchProducts, productModel, defaultTaxRate, currency, today, }) {
47
+ const { t, i18n } = useTranslation();
48
+ const lang = i18n?.language;
49
+ const api = useApi();
50
+ const fmt = useFormatter({ currency });
51
+ const installed = useInstalledAddons();
52
+ const orgTaxRate = useOrgTaxRate();
53
+ const tl = useCallback((s) => t(s, { defaultValue: s }), [t]);
54
+ const lineCfg = useMemo(() => editorLinesConfig(type, forms), [type, forms]);
55
+ const kind = lineCfg?.kind ?? 'sale';
56
+ const isAllocation = kind === 'allocation' && !!lineCfg?.open_documents;
57
+ const amountKey = isAllocation ? allocationAmountField(type.fields) : undefined;
58
+ const [header, setHeader] = useState(() => ({
59
+ ...buildFieldDefaults(type.fields, record),
60
+ ...(type.defaults ?? {}),
61
+ }));
62
+ const [lines, setLines] = useState([]);
63
+ const [linesFromSourceDoc, setLinesFromSourceDoc] = useState(false);
64
+ const [extFields, setExtFields] = useState([]);
65
+ const [party, setParty] = useState(null);
66
+ const [openDocs, setOpenDocs] = useState([]);
67
+ const [docsLoading, setDocsLoading] = useState(false);
68
+ const [alloc, setAlloc] = useState({ strategy: 'oldest_due_first', manual: {} });
69
+ const [draftId, setDraftId] = useState(null);
70
+ const [serverIssues, setServerIssues] = useState([]);
71
+ const [contributed, setContributed] = useState({});
72
+ const [preview, setPreview] = useState(null);
73
+ const [previewOpen, setPreviewOpen] = useState(false);
74
+ const [advancedOpen, setAdvancedOpen] = useState(false);
75
+ const [busy, setBusy] = useState(null);
76
+ const [formError, setFormError] = useState();
77
+ const [fieldErrors, setFieldErrors] = useState({});
78
+ const [attempted, setAttempted] = useState(false);
79
+ const [sourceKey, setSourceKey] = useState(initialSource?.key ?? '');
80
+ const [sourceId, setSourceId] = useState(initialSource?.id ?? '');
81
+ const relCache = useRef(new Map());
82
+ const taxRate = defaultTaxRate ?? orgTaxRate;
83
+ const search = useMemo(() => searchProducts ?? createCatalogProductSearch(api, { model: productModel, defaultTaxRate: taxRate }), [searchProducts, api, productModel, taxRate]);
84
+ const isInstalled = useCallback((k) => !k || !installed || installed.addons.has(k), [installed]);
85
+ const sources = useMemo(() => (type.sources ?? []).filter((s) => isInstalled(s.requires_addon)), [type.sources, isInstalled]);
86
+ const previewEnabled = !!type.preview && !type.submit_action && isInstalled(type.preview.requires_addon);
87
+ // Campos de extensión del modelo (fiscal_data.* que declaran otros addons): van plegados.
88
+ useEffect(() => {
89
+ let cancelled = false;
90
+ api.get(`/metadata/modal/${model}`)
91
+ .then((res) => {
92
+ const fields = res?.data?.data?.fields ?? res?.data?.fields ?? [];
93
+ const declared = new Set(type.fields.map((f) => f.key));
94
+ const ext = fields.filter((f) => typeof f?.key === 'string' && f.key.startsWith('fiscal_data.') && !declared.has(f.key) && !f.readonly && !f.readOnly);
95
+ if (!cancelled)
96
+ setExtFields(ext);
97
+ })
98
+ .catch(() => { });
99
+ return () => {
100
+ cancelled = true;
101
+ };
102
+ }, [api, model, type.fields]);
103
+ const groups = useMemo(() => splitEditorFields(type.fields, {
104
+ partyField: type.party?.field,
105
+ essentialKeys: [amountKey, ...sources.map((s) => s.link_field)].filter((k) => !!k),
106
+ extensionFields: extFields,
107
+ }), [type.fields, type.party?.field, amountKey, sources, extFields]);
108
+ const visible = useCallback((f) => evaluateVisibleWhen(getVisibleWhen(f), header), [header]);
109
+ const allFields = useMemo(() => [...(groups.party ? [groups.party] : []), ...groups.essential, ...groups.advanced, ...groups.notes], [groups]);
110
+ const fieldKeys = useRef([]);
111
+ fieldKeys.current = allFields.map((f) => f.key);
112
+ // Contraparte: tarjeta + defaults de las extensiones con la misma clave.
113
+ const partyId = type.party ? header[type.party.field] : undefined;
114
+ useEffect(() => {
115
+ if (!type.party || !partyId) {
116
+ setParty(null);
117
+ return;
118
+ }
119
+ let cancelled = false;
120
+ const ep = type.party.endpoint ?? `/data/${type.party.model}`;
121
+ api.get(ep, { params: { f_id: `eq:${partyId}`, per_page: 1 } })
122
+ .then((res) => {
123
+ const rows = res?.data?.data ?? [];
124
+ const rec = Array.isArray(rows) ? rows[0] ?? null : rows ?? null;
125
+ if (cancelled)
126
+ return;
127
+ setParty(rec);
128
+ setHeader((h) => ({ ...h, ...partyDefaults(rec, fieldKeys.current, h) }));
129
+ })
130
+ .catch(() => !cancelled && setParty(null));
131
+ return () => {
132
+ cancelled = true;
133
+ };
134
+ }, [api, type.party, partyId]);
135
+ // Cobro: documentos abiertos de la contraparte.
136
+ const openCfg = lineCfg?.open_documents;
137
+ const allocPartyId = openCfg ? header[openCfg.party_field] ?? partyId : undefined;
138
+ useEffect(() => {
139
+ if (!isAllocation || !openCfg || !allocPartyId) {
140
+ setOpenDocs([]);
141
+ return;
142
+ }
143
+ let cancelled = false;
144
+ setDocsLoading(true);
145
+ api.get(`/data/${openCfg.model}`, { params: { [`f_${openCfg.party_field}`]: `eq:${allocPartyId}`, per_page: 200 } })
146
+ .then((res) => {
147
+ if (!cancelled)
148
+ setOpenDocs(toOpenDocuments(res?.data?.data, openCfg, fmt.currency));
149
+ })
150
+ .catch(() => !cancelled && setOpenDocs([]))
151
+ .finally(() => !cancelled && setDocsLoading(false));
152
+ return () => {
153
+ cancelled = true;
154
+ };
155
+ }, [api, isAllocation, openCfg, allocPartyId, fmt.currency]);
156
+ const amount = amountKey ? toAmount(header[amountKey]) : 0;
157
+ const allocation = useMemo(() => allocatePayment(amount, openDocs, alloc.strategy, alloc.manual), [amount, openDocs, alloc]);
158
+ const openBalance = useMemo(() => roundMoney(openDocs.reduce((s, d) => s + d.balance, 0)), [openDocs]);
159
+ const totals = useMemo(() => computeTotals(lines), [lines]);
160
+ const taxes = useMemo(() => taxBreakdown(lines), [lines]);
161
+ const documentTotal = isAllocation ? amount : totals.total;
162
+ const credit = useMemo(() => (isAllocation ? null : creditStatus(party, type.party?.credit, documentTotal)), [isAllocation, party, type.party?.credit, documentTotal]);
163
+ const issues = useMemo(() => {
164
+ const out = [];
165
+ if (isAllocation && openCfg) {
166
+ const found = validateAllocation(amount, fmt.currency, openDocs, allocation.allocations, { allowUnapplied: true });
167
+ out.push(...allocationIssueMessages(found, openDocs));
168
+ }
169
+ else if (lineCfg) {
170
+ out.push(...localIssues(lines, { requireLines: lineCfg.required ?? true }));
171
+ }
172
+ if (credit?.level === 'block') {
173
+ out.push({
174
+ field: type.party?.field,
175
+ severity: 'error',
176
+ message: credit.holdReason
177
+ ? `${t('documentEditor.credit_hold', { defaultValue: 'Crédito retenido' })}: ${credit.holdReason}`
178
+ : t('documentEditor.credit_exceeded', { defaultValue: 'El total supera el crédito disponible.' }),
179
+ });
180
+ }
181
+ out.push(...Object.values(contributed), ...serverIssues);
182
+ return out;
183
+ }, [isAllocation, openCfg, amount, fmt.currency, openDocs, allocation, lineCfg, lines, credit, type.party?.field, contributed, serverIssues, t]);
184
+ const blocking = issues.some((i) => i.severity === 'error');
185
+ const shownIssues = attempted ? issues : issues.filter((i) => i.severity === 'warning');
186
+ const payload = useCallback(() => {
187
+ const p = { ...(type.defaults ?? {}), ...header };
188
+ if (forms.type_field && !type.submit_action)
189
+ p[forms.type_field] = type.value ?? type.key;
190
+ if (lineCfg) {
191
+ p[lineCfg.field] = isAllocation && openCfg ? allocationPayload(allocation.allocations, openCfg) : serializeLineItems(lines);
192
+ }
193
+ return p;
194
+ }, [type, header, forms.type_field, lineCfg, isAllocation, openCfg, allocation, lines]);
195
+ const baseUrl = type.endpoint ?? endpoint ?? `/dynamic/${model}`;
196
+ /** Crea o actualiza el BORRADOR (idempotente por draftId). La vista previa lo necesita. */
197
+ const saveDraft = useCallback(async () => {
198
+ const res = draftId ? await api.put(`${baseUrl}/${draftId}`, payload()) : await api.post(baseUrl, payload());
199
+ if (res.data?.success === false)
200
+ throw { response: { data: res.data } };
201
+ const rec = (res.data?.data ?? res.data ?? {});
202
+ const id = draftId ?? (rec.id != null ? String(rec.id) : null);
203
+ if (id)
204
+ setDraftId(id);
205
+ emitRecordMutation(model, draftId ? 'update' : 'create');
206
+ return { ...rec, id: id ?? rec.id };
207
+ }, [api, baseUrl, draftId, model, payload]);
208
+ const handleError = (err) => {
209
+ setFormError(actionErrorBannerMessage(err, allFields, t, lang));
210
+ const map = extractFieldErrors(err);
211
+ if (map) {
212
+ const labels = {};
213
+ for (const f of allFields)
214
+ labels[f.key] = tl(f.label);
215
+ const localized = localizeFieldErrorMap(map, t, { labels, language: lang });
216
+ setFieldErrors(localized);
217
+ if (groups.advanced.some((f) => localized[f.key]))
218
+ setAdvancedOpen(true);
219
+ }
220
+ };
221
+ // ---- Cargar desde… --------------------------------------------------------
222
+ const loadFromSource = useCallback(async (src, id) => {
223
+ if (!lineCfg || !id)
224
+ return;
225
+ setBusy('source');
226
+ try {
227
+ if (!isAllocation) {
228
+ let rels = relCache.current.get(src.model);
229
+ if (!rels) {
230
+ rels = api
231
+ .get(`/metadata/table/${src.model}`)
232
+ .then((r) => r?.data?.data?.relations ?? r?.data?.relations ?? []);
233
+ relCache.current.set(src.model, rels);
234
+ }
235
+ const call = prefillFromFieldRelationRequest({ fieldKey: lineCfg.field, refModel: src.model, refId: id, spec: { $prefillFromRecord: src.lines } }, await rels);
236
+ if (!call)
237
+ throw new Error(`${src.model}: falta la relación «${src.lines}»`);
238
+ const res = await api.get(call.endpoint, { params: call.params });
239
+ setLines(linesFromSource(res?.data?.data ?? [], src.map, { kind }));
240
+ setLinesFromSourceDoc(kind === 'credit');
241
+ }
242
+ if (src.header || src.link_field) {
243
+ const h = await api.get(`/data/${src.model}`, { params: { f_id: `eq:${id}`, per_page: 1 } });
244
+ const rec = ((h?.data?.data ?? [])[0] ?? {});
245
+ setHeader((prev) => {
246
+ const next = { ...prev };
247
+ for (const [to, from] of Object.entries(src.header ?? {}))
248
+ if (rec[from] != null)
249
+ next[to] = rec[from];
250
+ if (src.link_field)
251
+ next[src.link_field] = id;
252
+ return next;
253
+ });
254
+ }
255
+ setServerIssues([]);
256
+ }
257
+ catch (err) {
258
+ toast.error(t('documentEditor.source_failed', { defaultValue: 'No se pudo cargar el documento' }), {
259
+ description: err instanceof Error ? err.message : undefined,
260
+ });
261
+ }
262
+ finally {
263
+ setBusy(null);
264
+ }
265
+ }, [api, lineCfg, isAllocation, kind, t]);
266
+ // Prefill al abrir desde un documento origen (acción de fila «Crear NC», «Facturar venta»…).
267
+ const autoLoaded = useRef(false);
268
+ useEffect(() => {
269
+ if (autoLoaded.current || !initialSource)
270
+ return;
271
+ const src = sources.find((s) => s.key === initialSource.key);
272
+ if (!src)
273
+ return;
274
+ autoLoaded.current = true;
275
+ void loadFromSource(src, initialSource.id);
276
+ }, [initialSource, sources, loadFromSource]);
277
+ // ---- Vista previa -----------------------------------------------------------
278
+ const runPreview = async () => {
279
+ if (!type.preview)
280
+ return;
281
+ setBusy('preview');
282
+ setFormError(undefined);
283
+ try {
284
+ const rec = await saveDraft();
285
+ if (!rec.id)
286
+ return;
287
+ const res = await api.post(`${baseUrl}/${rec.id}/action/${type.preview.action}`, { analyze: true });
288
+ const d = res.data?.data ?? res.data ?? {};
289
+ const checks = Array.isArray(d.checks) ? d.checks : Array.isArray(d.issues) ? d.issues : [];
290
+ setServerIssues(checks
291
+ .filter((c) => c && c.severity !== 'ok' && c.message)
292
+ .map((c) => ({ field: c.field, severity: c.severity === 'error' ? 'error' : 'warning', message: String(c.message) })));
293
+ setPreview({ xml: d.xml, html: d.html, pdf_url: d.pdf_url, pdf_base64: d.pdf_base64 });
294
+ }
295
+ catch (err) {
296
+ handleError(err);
297
+ }
298
+ finally {
299
+ setBusy(null);
300
+ }
301
+ };
302
+ const togglePreview = (open) => {
303
+ setPreviewOpen(open);
304
+ if (open && !preview && busy === null)
305
+ void runPreview();
306
+ };
307
+ // ---- Guardar (acción primaria única) ----------------------------------------
308
+ const validateHeader = () => {
309
+ const fields = allFields.filter(visible);
310
+ const bag = validateValues(fields, header);
311
+ if (!bagHasErrors(bag)) {
312
+ setFieldErrors({});
313
+ return true;
314
+ }
315
+ const labels = {};
316
+ for (const f of fields)
317
+ labels[f.key] = tl(f.label);
318
+ const localized = localizeFieldErrorMap(bag, t, { labels, language: lang });
319
+ setFieldErrors(localized);
320
+ if (groups.advanced.some((f) => localized[f.key]))
321
+ setAdvancedOpen(true);
322
+ return false;
323
+ };
324
+ const submit = async () => {
325
+ setAttempted(true);
326
+ const headerOk = validateHeader();
327
+ if (!headerOk || blocking) {
328
+ setFormError(t('documentEditor.fix_issues', { defaultValue: 'Corrige los puntos marcados antes de guardar.' }));
329
+ return;
330
+ }
331
+ setBusy('save');
332
+ setFormError(undefined);
333
+ try {
334
+ let saved;
335
+ if (type.submit_action) {
336
+ const res = await api.post(`${baseUrl}/action/${type.submit_action}`, payload());
337
+ if (res.data?.success === false)
338
+ throw { response: { data: res.data } };
339
+ saved = res.data?.data ?? res.data ?? undefined;
340
+ emitRecordMutation(model, 'create');
341
+ }
342
+ else {
343
+ saved = await saveDraft();
344
+ }
345
+ toast.success(t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
346
+ onSaved?.(saved);
347
+ }
348
+ catch (err) {
349
+ handleError(err);
350
+ }
351
+ finally {
352
+ setBusy(null);
353
+ }
354
+ };
355
+ // ---- Contribuciones de otros addons -----------------------------------------
356
+ const ctx = useMemo(() => ({ kind: type.key, model, values: header, mode: 'create' }), [type.key, model, header]);
357
+ const setValue = useCallback((key, value) => setHeader((h) => (h[key] === value ? h : { ...h, [key]: value })), []);
358
+ const report = useCallback((key, issue) => {
359
+ setContributed((prev) => {
360
+ const cur = prev[key];
361
+ if (!issue) {
362
+ if (!cur)
363
+ return prev;
364
+ const { [key]: _drop, ...rest } = prev;
365
+ return rest;
366
+ }
367
+ if (cur && cur.severity === issue.severity && cur.message === issue.message)
368
+ return prev;
369
+ return { ...prev, [key]: { ...issue, field: key } };
370
+ });
371
+ }, []);
372
+ const contribProps = useMemo(() => ({ ctx, setValue, issues: { report: report } }), [ctx, setValue, report]);
373
+ const headerContribs = useDocumentContributions(ctx, 'header.fields');
374
+ const partyContribs = useDocumentContributions(ctx, 'party.card');
375
+ const bodyContribs = useDocumentContributions(ctx, 'body.sections');
376
+ const sideContribs = useDocumentContributions(ctx, 'side.panels');
377
+ const footerContribs = useDocumentContributions(ctx, 'footer.actions');
378
+ // ---- Render -------------------------------------------------------------------
379
+ const updateField = (key, value) => {
380
+ setHeader((h) => ({ ...h, [key]: value }));
381
+ setFieldErrors((prev) => clearFieldErrorTree(prev, key));
382
+ };
383
+ const fieldCell = (field, fullWidth = false) => visible(field) ? (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(withFriendlyOptions(field), header[field.key], (v) => updateField(field.key, v), header, undefined, fieldErrors), fieldErrors[field.key] && _jsx("p", { className: "mt-1 text-xs text-destructive", children: fieldErrors[field.key] })] }, field.key)) : null;
384
+ const summaryRows = partySummaryRows(party, type.party?.summary, Object.fromEntries(allFields.map((f) => [f.key, tl(f.label)])));
385
+ const advancedSummary = groups.advanced
386
+ .filter(visible)
387
+ .map((f) => {
388
+ const v = header[f.key];
389
+ if (v == null || v === '')
390
+ return null;
391
+ const opt = f.options?.find((o) => String(o.value) === String(v));
392
+ return opt ? String(opt.value) : String(v);
393
+ })
394
+ .filter(Boolean)
395
+ .slice(0, 4)
396
+ .join(' · ');
397
+ const totalsRows = isAllocation
398
+ ? [
399
+ { label: t('documentEditor.received', { defaultValue: 'Recibido' }), value: fmt.money(amount) },
400
+ { label: t('documentEditor.applied', { defaultValue: 'Aplicado' }), value: fmt.money(allocation.applied) },
401
+ ...(allocation.unapplied > 0
402
+ ? [{ label: t('documentEditor.unapplied', { defaultValue: 'Sin aplicar (saldo a favor)' }), value: fmt.money(allocation.unapplied) }]
403
+ : []),
404
+ ]
405
+ : [
406
+ { label: t('documentEditor.subtotal', { defaultValue: 'Subtotal' }), value: fmt.money(totals.subtotal) },
407
+ ...(totals.discount > 0 ? [{ label: t('documentEditor.discount', { defaultValue: 'Descuento' }), value: `− ${fmt.money(totals.discount)}` }] : []),
408
+ ...taxes.map((x) => ({
409
+ label: `${t('documentEditor.tax', { defaultValue: 'Impuesto' })} ${roundMoney(x.rate * 100)} %`,
410
+ value: fmt.money(x.tax),
411
+ })),
412
+ { label: t('documentEditor.total', { defaultValue: 'Total' }), value: fmt.money(totals.total), emphasis: true },
413
+ ];
414
+ const activeSource = sources.find((s) => s.key === sourceKey);
415
+ const linesTitle = lineCfg?.title
416
+ ? tl(lineCfg.title)
417
+ : isAllocation
418
+ ? t('documentEditor.allocation_title', { defaultValue: 'Documentos a pagar' })
419
+ : t('documentEditor.lines_title', { defaultValue: 'Conceptos' });
420
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", "data-slot": "document-editor", "data-kind": kind, children: [_jsxs("div", { className: "-mx-1 min-h-0 flex-1 space-y-8 overflow-y-auto px-1 py-2", children: [_jsx(FormErrorBanner, { message: formError }), _jsxs(EditorSection, { slot: "editor-essentials", children: [_jsxs(FieldGrid, { children: [groups.party && fieldCell(groups.party), groups.essential.map((f) => fieldCell(f))] }), party && (_jsx(PartyCard, { name: party.name ?? party.legal_name, rows: summaryRows, credit: credit, fmt: fmt, children: partyContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id))) }))] }), sources.length > 0 && !isAllocation && (_jsx(EditorSection, { slot: "load-from-source", title: t('documentEditor.load_from', { defaultValue: 'Cargar desde…' }), hint: busy === 'source' ? t('documentEditor.loading_source', { defaultValue: 'Cargando…' }) : undefined, children: _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [sources.length > 1 && (_jsxs("select", { "aria-label": t('documentEditor.source_kind', { defaultValue: 'Tipo de documento origen' }), className: "h-9 rounded-md border bg-background px-2 text-sm", value: sourceKey, onChange: (e) => {
421
+ setSourceKey(e.target.value);
422
+ setSourceId('');
423
+ }, children: [_jsx("option", { value: "", children: t('documentEditor.choose_source', { defaultValue: 'Elige…' }) }), sources.map((s) => (_jsx("option", { value: s.key, children: tl(s.label) }, s.key)))] })), (activeSource ?? (sources.length === 1 ? sources[0] : undefined)) && (_jsx(SourcePicker, { source: (activeSource ?? sources[0]), value: sourceId, header: header, label: tl((activeSource ?? sources[0]).label), onPick: (id) => {
424
+ if (!activeSource)
425
+ setSourceKey(sources[0].key);
426
+ setSourceId(id);
427
+ const src = activeSource ?? sources[0];
428
+ if (id)
429
+ void loadFromSource(src, id);
430
+ } }))] }) })), lineCfg && (_jsxs(EditorSection, { slot: isAllocation ? 'editor-allocation' : 'editor-lines', title: linesTitle, hint: isAllocation && openBalance > 0 && amountKey ? (_jsx("button", { type: "button", className: "underline-offset-2 hover:underline", onClick: () => updateField(amountKey, openBalance), children: t('documentEditor.pay_all', { defaultValue: 'Cobrar todo ({{amount}})', amount: fmt.money(openBalance) }) })) : undefined, children: [isAllocation ? (allocPartyId ? (_jsx(PaymentAllocator, { documents: openDocs, result: allocation, value: alloc, onChange: setAlloc, currency: currency, loading: docsLoading, today: today })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: t('documentEditor.pick_party_first', { defaultValue: 'Elige el cliente para ver sus documentos con saldo.' }) }))) : (_jsx(DocumentLinesGrid, { value: lines, onChange: (next) => {
431
+ setLines(next);
432
+ setServerIssues([]);
433
+ }, columns: lineCfg.columns ?? ['discount', 'tax'], priceSource: lineCfg.price_source ?? (kind === 'purchase' ? 'cost' : 'sale'), discountMode: lineCfg.discount_mode, mode: linesFromSourceDoc ? 'from_source' : 'free', policy: kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined, search: search, currency: currency })), _jsx(TotalsPanel, { rows: totalsRows })] })), (groups.advanced.length > 0 || headerContribs.length > 0) && (_jsxs(CollapsibleSection, { slot: "advanced-options", title: t('documentEditor.advanced', { defaultValue: 'Opciones fiscales' }), summary: advancedSummary || t('documentEditor.advanced_defaults', { defaultValue: 'Valores sugeridos' }), open: advancedOpen, onOpenChange: setAdvancedOpen, children: [groups.advanced.length > 0 && _jsx(FieldGrid, { children: groups.advanced.map((f) => fieldCell(f)) }), headerContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id)))] })), [...bodyContribs, ...sideContribs].map(({ id, component: C }) => (_jsx("section", { "data-contribution": id, children: _jsx(C, { ...contribProps }) }, id))), groups.notes.length > 0 && _jsx(FieldGrid, { children: groups.notes.map((f) => fieldCell(f, true)) }), previewEnabled && (_jsx(CollapsibleSection, { slot: "preview", title: tl(type.preview.label ?? 'Vista previa'), open: previewOpen, onOpenChange: togglePreview, children: busy === 'preview' ? (_jsxs("p", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }), t('documentEditor.preview_loading', { defaultValue: 'Generando vista previa…' })] })) : (_jsxs(_Fragment, { children: [_jsx(PreviewPanel, { preview: preview }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => void runPreview(), disabled: busy !== null, children: t('documentEditor.preview_refresh', { defaultValue: 'Actualizar vista previa' }) })] })) })), _jsx(ValidationChecklist, { issues: shownIssues, title: t('documentEditor.review', { defaultValue: 'Revisa antes de guardar' }) })] }), _jsxs("footer", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-4", "data-slot": "editor-actions", children: [footerContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id))), _jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, disabled: busy === 'save', children: t('common.cancel', { defaultValue: 'Cancelar' }) }), _jsxs(Button, { type: "button", "data-primary": "true", onClick: () => void submit(), disabled: busy !== null, children: [busy === 'save' && _jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }), type.submit_label ? tl(type.submit_label) : t('common.save', { defaultValue: 'Guardar' })] })] })] }));
434
+ }
435
+ /** Selector del documento origen (mismo widget de relación que el resto de formularios). */
436
+ function SourcePicker({ source, value, header, label, onPick, }) {
437
+ const field = useMemo(() => ({
438
+ key: `__source_${source.key}`,
439
+ label,
440
+ type: 'dynamic_select',
441
+ ref: source.model,
442
+ option_filter: source.option_filter,
443
+ }), [source, label]);
444
+ return (_jsx("div", { className: "min-w-[240px] flex-1", "data-source": source.key, children: renderField(field, value, (v) => onPick(v == null || v === '' ? '' : String(v)), header) }));
445
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"document-lines.d.ts","sourceRoot":"","sources":["../../src/business/document-lines.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AACpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,aAAa,CAAA;AAEtD,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAC5B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,cAAc,GAAG,SAAS,EACnC,MAAM,EAAE,WAAW,GACpB,MAAM,CAKR;AAED,uGAAuG;AACvG,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,QAAQ,CAmBV;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IACF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACxB,GACP,QAAQ,EAAE,CAeZ;AAED,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEhE,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,iBAAiB,CAAA;CAAO,GACxF,eAAe,CAKjB"}
1
+ {"version":3,"file":"document-lines.d.ts","sourceRoot":"","sources":["../../src/business/document-lines.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AACpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,aAAa,CAAA;AAEtD,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAC5B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,cAAc,GAAG,SAAS,EACnC,MAAM,EAAE,WAAW,GACpB,MAAM,CAKR;AAED,uGAAuG;AACvG,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,QAAQ,CAuBV;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IACF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACxB,GACP,QAAQ,EAAE,CAeZ;AAED,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEhE,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,iBAAiB,CAAA;CAAO,GACxF,eAAe,CAKjB"}
@@ -18,17 +18,21 @@ export function applyProductToLine(line, product, variant, opts = {}) {
18
18
  const list = variant?.price ?? product.price;
19
19
  const cost = variant?.cost ?? product.cost;
20
20
  const catalog = source === 'cost' ? (cost ?? list) : list;
21
+ // Nunca cantidad 0: un renglón vacío (o con la celda borrada) entra con 1.
22
+ const qty = opts.quantity ?? toAmount(line.quantity);
21
23
  return {
22
24
  ...line,
23
25
  kind: 'item',
24
26
  product_id: variant?.id ?? product.id,
25
27
  sku: variant?.sku ?? product.sku,
26
28
  description: variant ? `${product.name} · ${variant.label}` : product.name,
27
- quantity: opts.quantity ?? (toAmount(line.quantity) || 1),
29
+ quantity: qty !== 0 ? qty : 1,
28
30
  unit_price: priceFromProduct(product, variant, source),
29
31
  catalog_price: catalog,
30
32
  cost,
31
33
  tax_rate: product.tax_rate ?? line.tax_rate ?? 0,
34
+ unit: product.unit ?? line.unit,
35
+ extensions: product.extensions ? { ...(line.extensions ?? {}), ...product.extensions } : line.extensions,
32
36
  available: availableStock(src, opts.warehouseId) ?? availableStock(product, opts.warehouseId),
33
37
  };
34
38
  }
@@ -19,4 +19,9 @@ export * from './inspection-checklist';
19
19
  export * from './vehicle-picker';
20
20
  export * from './relate-documents';
21
21
  export * from './print-send-dialog';
22
+ export * from './catalog-product-search';
23
+ export * from './document-editor-model';
24
+ export * from './document-editor-panels';
25
+ export * from './payment-allocator';
26
+ export * from './document-editor';
22
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA"}
@@ -19,3 +19,8 @@ export * from './inspection-checklist';
19
19
  export * from './vehicle-picker';
20
20
  export * from './relate-documents';
21
21
  export * from './print-send-dialog';
22
+ export * from './catalog-product-search';
23
+ export * from './document-editor-model';
24
+ export * from './document-editor-panels';
25
+ export * from './payment-allocator';
26
+ export * from './document-editor';
@@ -30,6 +30,13 @@ export interface LineItem {
30
30
  dot?: string;
31
31
  /** Existencia disponible conocida (para la política de sobreventa). */
32
32
  available?: number;
33
+ /**
34
+ * Campos de extensión del renglón que aportan OTROS addons (p. ej.
35
+ * fiscal_mexico: `mx_clave_prod_serv`, `mx_clave_unidad`), copiados del
36
+ * producto al elegirlo. Viajan en `fiscal_data` (la bolsa de ModelExtension
37
+ * del host) — el SDK no conoce sus claves.
38
+ */
39
+ extensions?: Record<string, string>;
33
40
  }
34
41
  /** Qué hacer si `quantity > available`. */
35
42
  export type StockPolicy = 'allow' | 'warn' | 'block';
@@ -1 +1 @@
1
- {"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"line-items.d.ts","sourceRoot":"","sources":["../../src/business/line-items.ts"],"names":[],"mappings":"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"}
@@ -144,6 +144,8 @@ export function serializeLineItems(lines) {
144
144
  if (typeof v === 'string' && v.trim())
145
145
  out[k] = v.trim();
146
146
  }
147
+ if (l.extensions && Object.keys(l.extensions).length > 0)
148
+ out.fiscal_data = { ...l.extensions };
147
149
  }
148
150
  return out;
149
151
  });
@@ -161,7 +163,7 @@ export function parseLineItems(rows) {
161
163
  product_id: str(o.product_id),
162
164
  sku: str(o.sku),
163
165
  description: typeof o.description === 'string' ? o.description : '',
164
- quantity: o.quantity == null ? 1 : toAmount(o.quantity),
166
+ quantity: o.quantity == null || o.quantity === '' ? 1 : toAmount(o.quantity),
165
167
  unit_price: toAmount(o.unit_price),
166
168
  discount: toAmount(o.discount),
167
169
  discount_kind: o.discount_kind === 'amount' ? 'amount' : undefined,
@@ -173,6 +175,9 @@ export function parseLineItems(rows) {
173
175
  technician_id: str(o.technician_id),
174
176
  lot: str(o.lot),
175
177
  dot: str(o.dot),
178
+ extensions: o.fiscal_data && typeof o.fiscal_data === 'object'
179
+ ? Object.fromEntries(Object.entries(o.fiscal_data).filter(([, v]) => typeof v === 'string'))
180
+ : undefined,
176
181
  });
177
182
  });
178
183
  }
@@ -0,0 +1,19 @@
1
+ import type { AllocationResult, AllocationStrategy, OpenDocument } from '../primitives/allocation';
2
+ export interface PaymentAllocatorValue {
3
+ strategy: AllocationStrategy;
4
+ manual: Record<string, number>;
5
+ }
6
+ export interface PaymentAllocatorProps {
7
+ documents: OpenDocument[];
8
+ /** Resultado de `allocatePayment(amount, documents, value.strategy, value.manual)`. */
9
+ result: AllocationResult;
10
+ value: PaymentAllocatorValue;
11
+ onChange: (next: PaymentAllocatorValue) => void;
12
+ currency?: string;
13
+ loading?: boolean;
14
+ readOnly?: boolean;
15
+ /** Hoy (para los días de atraso; inyectable en tests). */
16
+ today?: Date;
17
+ }
18
+ export declare function PaymentAllocator({ documents, result, value, onChange, currency, loading, readOnly, today }: PaymentAllocatorProps): import("react").JSX.Element;
19
+ //# sourceMappingURL=payment-allocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-allocator.d.ts","sourceRoot":"","sources":["../../src/business/payment-allocator.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIlG,MAAM,WAAW,qBAAqB;IAClC,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,YAAY,EAAE,CAAA;IACzB,uFAAuF;IACvF,MAAM,EAAE,gBAAgB,CAAA;IACxB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,QAAQ,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,IAAI,CAAA;CACf;AAID,wBAAgB,gBAAgB,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,qBAAqB,+BA6EjI"}
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // PaymentAllocator — UI del reparto de un pago entre los documentos abiertos de
3
+ // la contraparte (cobro multi-factura / REP, CxP «Aplicar pago»). Las reglas
4
+ // son las de primitives/allocation (allocatePayment / validateAllocation); aquí
5
+ // solo se pintan y se editan. Por defecto reparte solo, del más vencido al más
6
+ // reciente; al teclear un monto pasa a manual sin perder lo ya repartido.
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Badge, Button } from '@asteby/metacore-ui/primitives';
9
+ import { Input } from '@asteby/metacore-ui';
10
+ import { manualFromAllocations, overdueDays } from './document-editor-model';
11
+ import { useFormatter } from './format';
12
+ const num = (s) => (s.trim() === '' ? 0 : Number(s.replace(',', '.')) || 0);
13
+ export function PaymentAllocator({ documents, result, value, onChange, currency, loading, readOnly, today }) {
14
+ const { t } = useTranslation();
15
+ const fmt = useFormatter({ currency });
16
+ const byDoc = new Map(result.allocations.map((a) => [a.document_id, a]));
17
+ if (loading) {
18
+ return _jsx("p", { className: "text-sm text-muted-foreground", children: t('paymentAllocator.loading', { defaultValue: 'Buscando documentos con saldo…' }) });
19
+ }
20
+ if (documents.length === 0) {
21
+ return (_jsx("p", { className: "text-sm text-muted-foreground", "data-slot": "payment-allocator-empty", children: t('paymentAllocator.empty', { defaultValue: 'No hay documentos con saldo pendiente.' }) }));
22
+ }
23
+ const setAmount = (id, amount) => onChange({ strategy: 'manual', manual: { ...manualFromAllocations(result.allocations), [id]: amount } });
24
+ return (_jsxs("div", { className: "space-y-2", "data-slot": "payment-allocator", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 text-xs text-muted-foreground", children: [_jsx("span", { children: value.strategy === 'manual'
25
+ ? t('paymentAllocator.manual', { defaultValue: 'Reparto manual' })
26
+ : t('paymentAllocator.auto', { defaultValue: 'Se aplica primero a lo más vencido' }) }), !readOnly && value.strategy === 'manual' && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange({ strategy: 'oldest_due_first', manual: {} }), children: t('paymentAllocator.autoapply', { defaultValue: 'Autoaplicar' }) }))] }), _jsx("ul", { className: "divide-y rounded-md border", children: documents.map((d) => {
27
+ const a = byDoc.get(d.id);
28
+ const late = overdueDays(d, today);
29
+ return (_jsxs("li", { className: "flex flex-wrap items-center gap-3 px-3 py-2 text-sm", "data-document": d.id, children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium", children: d.number }), late > 0 && (_jsx(Badge, { variant: "secondary", children: t('paymentAllocator.overdue', { defaultValue: 'Vencida · {{days}} d', days: late }) })), d.payment_method && _jsx("span", { className: "text-xs text-muted-foreground", children: d.payment_method })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [t('paymentAllocator.balance', { defaultValue: 'Saldo' }), " ", fmt.money(d.balance, d.currency), a && a.balance_after > 0 && (_jsxs(_Fragment, { children: [' · ', t('paymentAllocator.installment', { defaultValue: 'Parcialidad {{n}}', n: a.installment }), ' · ', t('paymentAllocator.remaining', { defaultValue: 'Queda' }), " ", fmt.money(a.balance_after, d.currency)] }))] })] }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", "aria-label": t('paymentAllocator.apply_to', { defaultValue: 'Aplicar a {{number}}', number: d.number }), className: "h-9 w-32 text-right tabular-nums", value: a ? String(a.amount) : '', placeholder: "0.00", disabled: readOnly, onChange: (e) => setAmount(d.id, num(e.target.value)) })] }, d.id));
30
+ }) })] }));
31
+ }
@@ -61,6 +61,10 @@ export interface ProductResult {
61
61
  name: string;
62
62
  quantity: number;
63
63
  }>;
64
+ /** Unidad de venta del catálogo (pza, juego, kg…). */
65
+ unit?: string;
66
+ /** Campos de extensión del producto (fiscal_data.*) que se copian al renglón. */
67
+ extensions?: Record<string, string>;
64
68
  }
65
69
  /** Existencia disponible: de la variante o del producto, en un almacén o en total. */
66
70
  export declare function availableStock(source: {