@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,279 @@
1
+ // Reglas puras del DocumentEditor (sin React ni host): qué se ve a primera
2
+ // vista y qué va en «Opciones fiscales», qué copiar de la contraparte, cómo
3
+ // traer renglones de un documento origen, cómo repartir un cobro entre los
4
+ // documentos abiertos y qué revisar antes de guardar. Genérico: factura,
5
+ // cotización, pedido, OC, nota de crédito y cobro/REP. El SDK no conoce CFDI.
6
+ import { roundMoney, toAmount } from './format';
7
+ import { computeTotals, makeLine } from './line-items';
8
+ import { optionPassesRule, getOptionFilter } from '../option-filter';
9
+ export const EXTENSION_PREFIX = 'fiscal_data.';
10
+ /** Secciones de campo que el editor pliega en «Opciones fiscales». */
11
+ const ADVANCED_SECTIONS = new Set(['fiscal', 'advanced']);
12
+ /** true si el tipo se abre en el DocumentEditor (`layout: "editor"`). */
13
+ export function isEditorLayout(type) {
14
+ return type?.layout === 'editor';
15
+ }
16
+ /** Paso de renglones efectivo del tipo (o undefined si no tiene). */
17
+ export function editorLinesConfig(type, forms) {
18
+ if (!type.lines)
19
+ return undefined;
20
+ const cfg = type.lines === true ? {} : type.lines;
21
+ const fallback = cfg.kind === 'allocation' ? 'allocations' : 'lines';
22
+ return { ...cfg, field: cfg.field ?? forms.lines_field ?? fallback };
23
+ }
24
+ /** Lee `a.b.c` de un registro (incluye `fiscal_data.x` dentro del jsonb). */
25
+ function readPath(rec, path) {
26
+ if (!rec)
27
+ return undefined;
28
+ if (path in rec)
29
+ return rec[path];
30
+ return path.split('.').reduce((acc, k) => (acc == null ? undefined : acc[k]), rec);
31
+ }
32
+ const hasValue = (v) => v !== undefined && v !== null && v !== '';
33
+ function declaredDefault(f) {
34
+ return f.defaultValue ?? f.default;
35
+ }
36
+ /**
37
+ * Divulgación progresiva: ¿el campo va plegado en «Opciones fiscales»?
38
+ * - campos de extensión (`fiscal_data.*`) que aportan otros addons;
39
+ * - `section: "fiscal" | "advanced"`;
40
+ * - catálogos opcionales que ya traen un default (método, serie, relación…):
41
+ * el usuario no necesita tocarlos para guardar.
42
+ * Lo obligatorio sin default, la contraparte y el monto siempre están a la vista.
43
+ */
44
+ export function isAdvancedField(f, essentialKeys = new Set()) {
45
+ if (essentialKeys.has(f.key))
46
+ return false;
47
+ if (f.key.startsWith(EXTENSION_PREFIX))
48
+ return true;
49
+ if (f.section && ADVANCED_SECTIONS.has(f.section))
50
+ return true;
51
+ const hasOptions = Array.isArray(f.options) && f.options.length > 0;
52
+ return !f.required && hasOptions && hasValue(declaredDefault(f));
53
+ }
54
+ /** Reparte los campos del tipo (más los de extensión) en grupos de pantalla. */
55
+ export function splitEditorFields(fields, opts = {}) {
56
+ const essentialKeys = new Set(opts.essentialKeys ?? []);
57
+ const out = { essential: [], advanced: [], notes: [] };
58
+ const seen = new Set();
59
+ for (const f of [...fields, ...(opts.extensionFields ?? [])]) {
60
+ if (seen.has(f.key))
61
+ continue;
62
+ seen.add(f.key);
63
+ if (opts.partyField && f.key === opts.partyField)
64
+ out.party = f;
65
+ else if (f.type === 'textarea' || f.widget === 'textarea')
66
+ out.notes.push(f);
67
+ else if (isAdvancedField(f, essentialKeys))
68
+ out.advanced.push(f);
69
+ else
70
+ out.essential.push(f);
71
+ }
72
+ return out;
73
+ }
74
+ /**
75
+ * Etiqueta clara con el código como dato secundario:
76
+ * `03 · Transferencia electrónica` → `Transferencia electrónica · 03`.
77
+ * Solo reordena el patrón «CÓDIGO · texto»; cualquier otra etiqueta pasa igual.
78
+ */
79
+ export function friendlyOptionLabel(label) {
80
+ const m = /^\s*([A-Z0-9]{1,5})\s*[·\-–]\s*(.+)$/.exec(label);
81
+ return m && /\d/.test(m[1]) ? `${m[2].trim()} · ${m[1]}` : label;
82
+ }
83
+ /** El mismo campo con sus opciones en formato «texto · código». */
84
+ export function withFriendlyOptions(f) {
85
+ if (!Array.isArray(f.options) || f.options.length === 0)
86
+ return f;
87
+ return { ...f, options: f.options.map((o) => ({ ...o, label: friendlyOptionLabel(String(o.label ?? o.value)) })) };
88
+ }
89
+ /**
90
+ * Defaults del encabezado que salen de la contraparte: los campos de extensión
91
+ * con la MISMA clave en ambos modelos (p. ej. `fiscal_data.uso_cfdi` de Customer
92
+ * → `fiscal_data.uso_cfdi` del documento) y la moneda.
93
+ * Solo llena lo que está vacío: nunca pisa lo que el usuario ya capturó.
94
+ */
95
+ export function partyDefaults(party, documentFieldKeys, current) {
96
+ if (!party)
97
+ return {};
98
+ const out = {};
99
+ for (const key of documentFieldKeys) {
100
+ if (!key.startsWith(EXTENSION_PREFIX) || hasValue(current[key]))
101
+ continue;
102
+ const v = readPath(party, key);
103
+ if (hasValue(v))
104
+ out[key] = v;
105
+ }
106
+ if (!hasValue(current.currency_code) && typeof party.currency_code === 'string' && party.currency_code) {
107
+ out.currency_code = party.currency_code;
108
+ }
109
+ return out;
110
+ }
111
+ /** Filas de la tarjeta de contraparte: resumen declarado + extensiones (máximo `limit`). */
112
+ export function partySummaryRows(party, summary = [], labels = {}, limit = 6) {
113
+ if (!party)
114
+ return [];
115
+ const keys = new Set(summary);
116
+ const fd = party.fiscal_data;
117
+ if (fd && typeof fd === 'object')
118
+ for (const k of Object.keys(fd))
119
+ keys.add(EXTENSION_PREFIX + k);
120
+ const rows = [];
121
+ for (const key of keys) {
122
+ const v = readPath(party, key);
123
+ if (!hasValue(v) || typeof v === 'object')
124
+ continue;
125
+ rows.push({ key, label: labels[key] ?? key.replace(EXTENSION_PREFIX, ''), value: String(v) });
126
+ }
127
+ return rows.slice(0, limit);
128
+ }
129
+ export function creditStatus(party, cols, documentTotal) {
130
+ if (!party || !cols)
131
+ return null;
132
+ const n = (k) => (k && hasValue(party[k]) ? toAmount(party[k]) : undefined);
133
+ const limit = n(cols.limit);
134
+ const balance = n(cols.balance);
135
+ const overdue = n(cols.overdue);
136
+ const holdReason = cols.hold_reason ? party[cols.hold_reason] || undefined : undefined;
137
+ if (limit == null && balance == null && overdue == null && !holdReason)
138
+ return null;
139
+ const available = limit != null ? roundMoney(limit - (balance ?? 0)) : undefined;
140
+ let level = 'ok';
141
+ if (holdReason || (available != null && limit > 0 && available < documentTotal))
142
+ level = 'block';
143
+ else if ((overdue ?? 0) > 0 || (limit != null && limit > 0 && (balance ?? 0) + documentTotal > 0.8 * limit))
144
+ level = 'warn';
145
+ return { limit, balance, overdue, available, holdReason, level };
146
+ }
147
+ /**
148
+ * Renglones de un documento origen → LineItem. `map` traduce nombres
149
+ * (`description: product_name`, `discount: discount_pct`). Si el origen trae
150
+ * `tax_amount` y `subtotal` pero no la tasa, la deriva. `credit` (NC) topa la
151
+ * cantidad en lo facturado (o en `max_quantity` si el origen la trae).
152
+ */
153
+ export function linesFromSource(rows, map = {}, opts = {}) {
154
+ if (!Array.isArray(rows))
155
+ return [];
156
+ return rows.map((raw) => {
157
+ const r = (raw ?? {});
158
+ const get = (k) => r[map[k] ?? k];
159
+ const qty = toAmount(get('quantity') ?? 1);
160
+ let rate = get('tax_rate');
161
+ if (!hasValue(rate)) {
162
+ const sub = toAmount(get('subtotal'));
163
+ const tax = toAmount(get('tax_amount'));
164
+ rate = sub > 0 ? Math.round((tax / sub) * 10000) / 10000 : 0;
165
+ }
166
+ const r2 = toAmount(rate);
167
+ const line = makeLine({
168
+ product_id: get('product_id') ?? undefined,
169
+ sku: get('sku') ?? undefined,
170
+ description: String(get('description') ?? get('product_name') ?? ''),
171
+ quantity: qty,
172
+ unit_price: toAmount(get('unit_price')),
173
+ discount: toAmount(get('discount') ?? 0),
174
+ tax_rate: r2 > 1 ? r2 / 100 : r2,
175
+ unit: get('unit') ?? undefined,
176
+ });
177
+ if (opts.kind === 'credit')
178
+ line.max_quantity = hasValue(get('max_quantity')) ? toAmount(get('max_quantity')) : qty;
179
+ return line;
180
+ });
181
+ }
182
+ /** Revisión local (instantánea) de los renglones. Sin ruido: solo lo que hay que mirar. */
183
+ export function localIssues(lines, opts = {}) {
184
+ const issues = [];
185
+ const items = lines.filter((l) => l.kind === 'item');
186
+ if ((opts.requireLines ?? true) && items.length === 0) {
187
+ issues.push({ field: 'lines', severity: 'error', message: 'Agrega al menos un renglón.' });
188
+ }
189
+ items.forEach((l, i) => {
190
+ if (!l.description.trim())
191
+ issues.push({ field: `lines.${i}`, severity: 'error', message: `Renglón ${i + 1}: falta la descripción.` });
192
+ if (toAmount(l.unit_price) <= 0)
193
+ issues.push({ field: `lines.${i}`, severity: 'warning', message: `Renglón ${i + 1}: precio en cero.` });
194
+ if (opts.requireTax && toAmount(l.tax_rate) === 0) {
195
+ issues.push({ field: `lines.${i}`, severity: 'warning', message: `Renglón ${i + 1}: sin impuesto.` });
196
+ }
197
+ });
198
+ if (computeTotals(lines).total < 0)
199
+ issues.push({ severity: 'error', message: 'El total no puede ser negativo.' });
200
+ return issues;
201
+ }
202
+ // ---- Cobro / REP (lines.kind = allocation) ----------------------------------
203
+ /** Documento abierto (fila de /data) → OpenDocument para PaymentAllocator. */
204
+ export function toOpenDocuments(rows, cfg, currency) {
205
+ if (!Array.isArray(rows))
206
+ return [];
207
+ const rules = getOptionFilter(cfg);
208
+ const out = [];
209
+ for (const raw of rows) {
210
+ if (!raw || typeof raw !== 'object')
211
+ continue;
212
+ const r = raw;
213
+ if (rules.length) {
214
+ const opt = { id: r.id, value: r.id, label: String(r[cfg.number_field] ?? ''), name: '', meta: r };
215
+ if (!rules.every((rule) => optionPassesRule(opt, rule)))
216
+ continue;
217
+ }
218
+ const balance = roundMoney(toAmount(readPath(r, cfg.balance_field)));
219
+ if (balance <= 0)
220
+ continue;
221
+ out.push({
222
+ id: String(r.id),
223
+ number: String(readPath(r, cfg.number_field) ?? r.id),
224
+ issued_at: String(readPath(r, cfg.issued_field ?? 'issued_at') ?? r.created_at ?? ''),
225
+ due_at: (cfg.due_field ? readPath(r, cfg.due_field) : null) ?? null,
226
+ total: roundMoney(toAmount(cfg.total_field ? readPath(r, cfg.total_field) : balance)),
227
+ balance,
228
+ currency: String(r.currency_code ?? r.currency ?? currency),
229
+ payment_method: cfg.method_field ? (readPath(r, cfg.method_field) ?? null) : null,
230
+ installments_paid: hasValue(r.installments_paid) ? toAmount(r.installments_paid) : undefined,
231
+ });
232
+ }
233
+ return out;
234
+ }
235
+ /** Monto actual de cada documento, para pasar a modo manual sin perder lo repartido. */
236
+ export function manualFromAllocations(allocations) {
237
+ return Object.fromEntries(allocations.map((a) => [a.document_id, a.amount]));
238
+ }
239
+ /** Renglones del payload: solo los dos campos que declara la acción (`invoice_id`, `amount`). */
240
+ export function allocationPayload(allocations, cfg) {
241
+ return allocations.filter((a) => a.amount > 0).map((a) => ({ [cfg.line_document_field]: a.document_id, [cfg.line_amount_field]: a.amount }));
242
+ }
243
+ /**
244
+ * Campo del encabezado con el monto recibido: `amount` si existe; si no, el
245
+ * primer campo numérico/moneda. Convención documentada (el schema no lo nombra).
246
+ */
247
+ export function allocationAmountField(fields) {
248
+ return (fields.find((f) => f.key === 'amount')?.key ??
249
+ fields.find((f) => ['number', 'numeric', 'decimal', 'currency', 'money'].includes(f.type) || f.widget === 'currency')?.key);
250
+ }
251
+ const ALLOCATION_MESSAGES = {
252
+ non_positive: 'Captura el monto recibido.',
253
+ over_balance: 'Se aplica más que el saldo del documento.',
254
+ over_amount: 'Lo aplicado supera el monto recibido.',
255
+ currency_mismatch: 'El documento está en otra moneda.',
256
+ unapplied: 'Queda monto sin aplicar.',
257
+ };
258
+ /** AllocationIssue → mensaje en español claro (con el número del documento). */
259
+ export function allocationIssueMessages(issues, docs) {
260
+ const byId = new Map(docs.map((d) => [d.id, d]));
261
+ return issues.map((i) => {
262
+ const doc = i.document_id ? byId.get(i.document_id) : undefined;
263
+ return {
264
+ field: i.code === 'non_positive' ? 'amount' : 'allocations',
265
+ severity: i.severity,
266
+ message: doc ? `${doc.number}: ${ALLOCATION_MESSAGES[i.code]}` : ALLOCATION_MESSAGES[i.code],
267
+ };
268
+ });
269
+ }
270
+ /** Días de atraso de un documento abierto (0 si no está vencido o no tiene vencimiento). */
271
+ export function overdueDays(doc, today = new Date()) {
272
+ if (!doc.due_at)
273
+ return 0;
274
+ const due = new Date(doc.due_at);
275
+ if (Number.isNaN(due.getTime()))
276
+ return 0;
277
+ const ms = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) - Date.UTC(due.getUTCFullYear(), due.getUTCMonth(), due.getUTCDate());
278
+ return Math.max(0, Math.floor(ms / 86_400_000));
279
+ }
@@ -0,0 +1,64 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { CreditStatus, EditorIssue } from './document-editor-model';
3
+ import type { Formatter } from './format';
4
+ /** Bloque con título discreto y espaciado generoso (sin bordes pesados). */
5
+ export declare function EditorSection({ title, hint, children, slot }: {
6
+ title?: ReactNode;
7
+ hint?: ReactNode;
8
+ children: ReactNode;
9
+ slot?: string;
10
+ }): import("react").JSX.Element;
11
+ export interface CollapsibleSectionProps {
12
+ title: ReactNode;
13
+ /** Resumen de una línea cuando está cerrada (p. ej. «G03 · PUE»). */
14
+ summary?: ReactNode;
15
+ open?: boolean;
16
+ defaultOpen?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ children: ReactNode;
19
+ slot?: string;
20
+ }
21
+ /**
22
+ * Sección plegable. El contenido queda MONTADO aunque esté cerrada (`hidden`):
23
+ * las contribuciones de otros addons siguen aplicando defaults y reportando
24
+ * revisiones sin que el usuario abra la sección.
25
+ */
26
+ export declare function CollapsibleSection({ title, summary, open, defaultOpen, onOpenChange, children, slot }: CollapsibleSectionProps): import("react").JSX.Element;
27
+ export interface PartyCardProps {
28
+ name?: string;
29
+ rows: Array<{
30
+ key: string;
31
+ label: string;
32
+ value: string;
33
+ }>;
34
+ credit?: CreditStatus | null;
35
+ fmt: Formatter;
36
+ children?: ReactNode;
37
+ }
38
+ /** Tarjeta compacta de la contraparte: nombre, 3-6 datos clave y estado de crédito. */
39
+ export declare function PartyCard({ name, rows, credit, fmt, children }: PartyCardProps): import("react").JSX.Element;
40
+ export interface TotalsRow {
41
+ label: string;
42
+ value: string;
43
+ emphasis?: boolean;
44
+ }
45
+ /** Totales alineados a la derecha; la última fila con `emphasis` es el total. */
46
+ export declare function TotalsPanel({ rows }: {
47
+ rows: TotalsRow[];
48
+ }): import("react").JSX.Element;
49
+ /** Lista de revisión: errores (rojo) y avisos (ámbar). Vacía → no pinta nada. */
50
+ export declare function ValidationChecklist({ issues, title }: {
51
+ issues: EditorIssue[];
52
+ title?: string;
53
+ }): import("react").JSX.Element | null;
54
+ export interface PreviewContent {
55
+ xml?: string;
56
+ html?: string;
57
+ pdf_url?: string;
58
+ pdf_base64?: string;
59
+ }
60
+ /** Contenido de la vista previa (PDF, HTML o XML, en ese orden de preferencia). */
61
+ export declare function PreviewPanel({ preview }: {
62
+ preview: PreviewContent | null;
63
+ }): import("react").JSX.Element | null;
64
+ //# sourceMappingURL=document-editor-panels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-editor-panels.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-panels.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGvD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BAYzI;AAED,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,SAAS,CAAA;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,WAAmB,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,uBAAuB,+BA4BtI;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1D,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAC5B,GAAG,EAAE,SAAS,CAAA;IACd,QAAQ,CAAC,EAAE,SAAS,CAAA;CACvB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,cAAc,+BA4B9E;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,EAAE,CAAA;CAAE,+BAW1D;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,KAAiC,EAAE,EAAE;IAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,sCAe3H;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;CAAE,sCAO3E"}
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Piezas visuales del DocumentEditor: secciones, sección plegable, tarjeta de
3
+ // contraparte, totales, revisión y vista previa. Sin estado de negocio: el
4
+ // editor (o un remote federado) les pasa los datos ya calculados. Los contratos
5
+ // de datos viven en primitives/panels.ts.
6
+ import { useId, useState } from 'react';
7
+ import { Badge } from '@asteby/metacore-ui/primitives';
8
+ import { AlertTriangle, ChevronDown, XCircle } from 'lucide-react';
9
+ /** Bloque con título discreto y espaciado generoso (sin bordes pesados). */
10
+ export function EditorSection({ title, hint, children, slot }) {
11
+ return (_jsxs("section", { className: "space-y-3", "data-slot": slot, children: [(title || hint) && (_jsxs("header", { className: "flex items-baseline justify-between gap-3", children: [title && _jsx("h3", { className: "text-sm font-semibold", children: title }), hint && _jsx("span", { className: "text-xs text-muted-foreground", children: hint })] })), children] }));
12
+ }
13
+ /**
14
+ * Sección plegable. El contenido queda MONTADO aunque esté cerrada (`hidden`):
15
+ * las contribuciones de otros addons siguen aplicando defaults y reportando
16
+ * revisiones sin que el usuario abra la sección.
17
+ */
18
+ export function CollapsibleSection({ title, summary, open, defaultOpen = false, onOpenChange, children, slot }) {
19
+ const [inner, setInner] = useState(defaultOpen);
20
+ const isOpen = open ?? inner;
21
+ const id = useId();
22
+ const toggle = () => {
23
+ setInner(!isOpen);
24
+ onOpenChange?.(!isOpen);
25
+ };
26
+ return (_jsxs("section", { className: "rounded-lg border border-dashed", "data-slot": slot, "data-state": isOpen ? 'open' : 'closed', children: [_jsxs("button", { type: "button", "aria-expanded": isOpen, "aria-controls": id, onClick: toggle, className: "flex w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm", children: [_jsx("span", { className: "font-medium", children: title }), _jsxs("span", { className: "flex min-w-0 items-center gap-2 text-xs text-muted-foreground", children: [!isOpen && summary && _jsx("span", { className: "truncate", children: summary }), _jsx(ChevronDown, { className: `size-4 shrink-0 transition-transform ${isOpen ? 'rotate-180' : ''}`, "aria-hidden": true })] })] }), _jsx("div", { id: id, hidden: !isOpen, className: "space-y-4 px-4 pb-4", children: children })] }));
27
+ }
28
+ /** Tarjeta compacta de la contraparte: nombre, 3-6 datos clave y estado de crédito. */
29
+ export function PartyCard({ name, rows, credit, fmt, children }) {
30
+ return (_jsxs("div", { className: "rounded-md bg-muted/40 px-4 py-3 text-sm", "data-slot": "party-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [name && _jsx("span", { className: "font-medium", children: name }), credit && (_jsx(Badge, { variant: credit.level === 'block' ? 'destructive' : credit.level === 'warn' ? 'secondary' : 'outline', "data-slot": "party-credit", children: credit.holdReason
31
+ ? credit.holdReason
32
+ : credit.available != null
33
+ ? `Crédito disponible ${fmt.money(credit.available)}`
34
+ : `Saldo ${fmt.money(credit.balance ?? 0)}` }))] }), rows.length > 0 && (_jsx("dl", { className: "mt-2 grid grid-cols-2 gap-x-6 gap-y-1 sm:grid-cols-3", children: rows.map((r) => (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-xs text-muted-foreground", children: r.label }), _jsx("dd", { className: "truncate", children: r.value })] }, r.key))) })), children] }));
35
+ }
36
+ /** Totales alineados a la derecha; la última fila con `emphasis` es el total. */
37
+ export function TotalsPanel({ rows }) {
38
+ return (_jsx("dl", { className: "ml-auto w-full max-w-xs space-y-1 text-sm tabular-nums", "data-slot": "totals", children: rows.map((r) => (_jsxs("div", { className: `flex justify-between gap-6 ${r.emphasis ? 'border-t pt-2 text-base font-semibold' : ''}`, children: [_jsx("dt", { className: r.emphasis ? '' : 'text-muted-foreground', children: r.label }), _jsx("dd", { children: r.value })] }, r.label))) }));
39
+ }
40
+ /** Lista de revisión: errores (rojo) y avisos (ámbar). Vacía → no pinta nada. */
41
+ export function ValidationChecklist({ issues, title = 'Revisa antes de guardar' }) {
42
+ if (issues.length === 0)
43
+ return null;
44
+ return (_jsxs("div", { className: "rounded-md bg-muted/30 px-4 py-3", "data-slot": "validation-checklist", role: "status", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: title }), _jsx("ul", { className: "space-y-1 text-sm", children: issues.map((i, k) => (_jsxs("li", { className: `flex items-start gap-2 ${i.severity === 'error' ? 'text-destructive' : 'text-amber-700'}`, children: [i.severity === 'error' ? _jsx(XCircle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }) : _jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0", "aria-hidden": true }), i.message] }, `${i.field ?? ''}:${k}`))) })] }));
45
+ }
46
+ /** Contenido de la vista previa (PDF, HTML o XML, en ese orden de preferencia). */
47
+ export function PreviewPanel({ preview }) {
48
+ if (!preview)
49
+ return null;
50
+ const pdf = preview.pdf_url ?? (preview.pdf_base64 ? `data:application/pdf;base64,${preview.pdf_base64}` : undefined);
51
+ if (pdf)
52
+ return _jsx("iframe", { title: "Vista previa", src: pdf, className: "h-96 w-full rounded border", "data-slot": "preview-pdf" });
53
+ if (preview.html)
54
+ return _jsx("iframe", { title: "Vista previa", srcDoc: preview.html, className: "h-96 w-full rounded border", "data-slot": "preview-html" });
55
+ if (preview.xml)
56
+ return _jsx("pre", { className: "max-h-96 overflow-auto rounded bg-muted p-3 text-[11px] leading-tight", "data-slot": "preview-xml", children: preview.xml });
57
+ return null;
58
+ }
@@ -0,0 +1,27 @@
1
+ import { type DocumentLinesGridProps } from './line-items-editor';
2
+ import type { DocumentFormType, DocumentFormsManifest } from '../types';
3
+ export interface DocumentEditorProps {
4
+ model: string;
5
+ /** Endpoint de alta del modelo. Default `/dynamic/<model>`. */
6
+ endpoint?: string;
7
+ forms: DocumentFormsManifest;
8
+ type: DocumentFormType;
9
+ /** Registro desde el que se abrió (acción de fila): siembra `default_from_record`. */
10
+ record?: Record<string, any>;
11
+ /** Documento origen a precargar al abrir (`sources[].key` + id). */
12
+ initialSource?: {
13
+ key: string;
14
+ id: string;
15
+ };
16
+ onSaved?: (record?: unknown) => void;
17
+ onCancel: () => void;
18
+ searchProducts?: DocumentLinesGridProps['search'];
19
+ /** Modelo de catálogo del buscador por defecto. Default `products.Product`. */
20
+ productModel?: string;
21
+ defaultTaxRate?: number;
22
+ currency?: string;
23
+ /** Hoy (días de atraso del reparto; inyectable en tests). */
24
+ today?: Date;
25
+ }
26
+ export declare function DocumentEditor({ model, endpoint, forms, type, record, initialSource, onSaved, onCancel, searchProducts, productModel, defaultTaxRate, currency, today, }: DocumentEditorProps): import("react").JSX.Element;
27
+ //# sourceMappingURL=document-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-editor.d.ts","sourceRoot":"","sources":["../../src/business/document-editor.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAAqB,KAAK,sBAAsB,EAAwB,MAAM,qBAAqB,CAAA;AAwC1G,OAAO,KAAK,EAAsC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAE3G,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,IAAI,EAAE,gBAAgB,CAAA;IACtB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,IAAI,CAAA;CACf;AAID,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,OAAO,EACP,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,KAAK,GACR,EAAE,mBAAmB,+BA2kBrB"}