@asteby/metacore-runtime-react 49.2.0 → 49.2.1
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 +11 -0
- package/dist/business/document-editor-model.d.ts +21 -0
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +56 -0
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +42 -22
- package/dist/dynamic-row-actions.d.ts +6 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +9 -4
- package/package.json +1 -1
- package/src/__tests__/r6-editor-source-seeds.test.tsx +137 -0
- package/src/__tests__/row-action-link-navigation.test.ts +10 -6
- package/src/business/document-editor-model.ts +68 -0
- package/src/business/document-editor.tsx +45 -16
- package/src/dynamic-row-actions.tsx +12 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 49.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c4a7056: «Nota de crédito» / crear-desde: navega con /m/$model + search (abre el editor con la factura, no solo la lista).
|
|
8
|
+
- c4a7056: DocumentEditor: «Cargar desde» venta, OT o cotización deja el cliente elegido y los selectores muestran nombre o folio, no el UUID.
|
|
9
|
+
|
|
10
|
+
- La cabecera del origen (`sources[].header`, p. ej. `customer_id`, y `link_field`) se carga antes que los renglones, así que el cliente y su tarjeta llegan aunque los renglones del origen fallen.
|
|
11
|
+
- Los selectores que el editor llena por código (cliente, documento origen, «Factura a abonar» de la NC) se siembran con la etiqueta que ya conoce: el objeto hermano `{value,label}` que sirve el host, el nombre de la contraparte cargada o el folio del origen. El selector «Cargar desde…» también muestra el folio.
|
|
12
|
+
- La vista previa local muestra esas etiquetas, y un campo con `options` en objeto ya no rompe el resumen.
|
|
13
|
+
|
|
3
14
|
## 49.2.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
|
@@ -96,6 +96,27 @@ export declare function sourceLoadSummary(rows: unknown): {
|
|
|
96
96
|
};
|
|
97
97
|
/** Fuente que precarga desde lo pendiente servido por el host (no desde la relación cruda). */
|
|
98
98
|
export declare function sourceTracksRemaining(src: Pick<DocumentFormSource, 'line_link_field' | 'remaining_qty_field' | 'remaining_endpoint'>): boolean;
|
|
99
|
+
/** Opción ya resuelta de un selector de la cabecera que el editor llenó por código. */
|
|
100
|
+
export interface SeedLabel {
|
|
101
|
+
value: string;
|
|
102
|
+
label: string;
|
|
103
|
+
}
|
|
104
|
+
/** Nombre visible de un registro: la columna declarada por el campo, luego folio/nombre. */
|
|
105
|
+
export declare function recordLabel(rec: Record<string, any> | null | undefined, preferred?: string): string | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Etiquetas de lo que «Cargar desde…» escribe en la cabecera, para que el
|
|
108
|
+
* selector muestre el nombre y no el UUID sin abrirse:
|
|
109
|
+
* - `header` (`customer_id ← customer_id`): el objeto hermano `{value,label}`
|
|
110
|
+
* que el host sirve junto a la FK (`customer`);
|
|
111
|
+
* - `link_field`: el propio documento origen, con su folio.
|
|
112
|
+
*/
|
|
113
|
+
export declare function sourceHeaderSeeds(src: Pick<DocumentFormSource, 'header' | 'link_field'>, rec: Record<string, any>, id: string, fields?: readonly ActionFieldDef[]): Record<string, SeedLabel>;
|
|
114
|
+
/**
|
|
115
|
+
* Registro para `renderField` con las etiquetas sembradas como objeto hermano
|
|
116
|
+
* (`customer_id` → `customer: {value,label}`), el mismo contrato que sirve el
|
|
117
|
+
* host en una fila. Conserva el registro desde el que se abrió.
|
|
118
|
+
*/
|
|
119
|
+
export declare function seedRecord(seeds: Record<string, SeedLabel>, record?: Record<string, any>): Record<string, any> | undefined;
|
|
99
120
|
export interface EditorIssue {
|
|
100
121
|
field?: string;
|
|
101
122
|
severity: 'error' | 'warning';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-editor-model.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-model.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"document-editor-model.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -248,6 +248,62 @@ export function sourceLoadSummary(rows) {
|
|
|
248
248
|
export function sourceTracksRemaining(src) {
|
|
249
249
|
return !!(src.line_link_field || src.remaining_qty_field || src.remaining_endpoint);
|
|
250
250
|
}
|
|
251
|
+
/** Columnas con las que se nombra un documento (folio antes que nombre). */
|
|
252
|
+
const DOCUMENT_LABEL_COLUMNS = ['number', 'folio', 'order_number', 'name', 'title', 'code'];
|
|
253
|
+
function declaredOptionLabel(f) {
|
|
254
|
+
const opts = f?.options;
|
|
255
|
+
const l = opts && !Array.isArray(opts) && typeof opts === 'object' ? opts.label : undefined;
|
|
256
|
+
return typeof l === 'string' && l ? l : undefined;
|
|
257
|
+
}
|
|
258
|
+
/** Nombre visible de un registro: la columna declarada por el campo, luego folio/nombre. */
|
|
259
|
+
export function recordLabel(rec, preferred) {
|
|
260
|
+
if (!rec)
|
|
261
|
+
return undefined;
|
|
262
|
+
for (const c of preferred ? [preferred, ...DOCUMENT_LABEL_COLUMNS] : DOCUMENT_LABEL_COLUMNS) {
|
|
263
|
+
const v = rec[c];
|
|
264
|
+
if ((typeof v === 'string' && v.trim()) || typeof v === 'number')
|
|
265
|
+
return String(v);
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Etiquetas de lo que «Cargar desde…» escribe en la cabecera, para que el
|
|
271
|
+
* selector muestre el nombre y no el UUID sin abrirse:
|
|
272
|
+
* - `header` (`customer_id ← customer_id`): el objeto hermano `{value,label}`
|
|
273
|
+
* que el host sirve junto a la FK (`customer`);
|
|
274
|
+
* - `link_field`: el propio documento origen, con su folio.
|
|
275
|
+
*/
|
|
276
|
+
export function sourceHeaderSeeds(src, rec, id, fields = []) {
|
|
277
|
+
const out = {};
|
|
278
|
+
for (const [to, from] of Object.entries(src.header ?? {})) {
|
|
279
|
+
if (!from.endsWith('_id'))
|
|
280
|
+
continue;
|
|
281
|
+
const v = refValue(rec[from]);
|
|
282
|
+
const label = refLabel(rec[from]) ?? refLabel(rec[from.slice(0, -3)]);
|
|
283
|
+
if (v && label)
|
|
284
|
+
out[to] = { value: v, label };
|
|
285
|
+
}
|
|
286
|
+
if (src.link_field) {
|
|
287
|
+
const label = recordLabel(rec, declaredOptionLabel(fields.find((f) => f.key === src.link_field)));
|
|
288
|
+
if (label)
|
|
289
|
+
out[src.link_field] = { value: id, label };
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Registro para `renderField` con las etiquetas sembradas como objeto hermano
|
|
295
|
+
* (`customer_id` → `customer: {value,label}`), el mismo contrato que sirve el
|
|
296
|
+
* host en una fila. Conserva el registro desde el que se abrió.
|
|
297
|
+
*/
|
|
298
|
+
export function seedRecord(seeds, record) {
|
|
299
|
+
const keys = Object.keys(seeds).filter((k) => k.endsWith('_id'));
|
|
300
|
+
if (keys.length === 0)
|
|
301
|
+
return record;
|
|
302
|
+
const out = { ...(record ?? {}) };
|
|
303
|
+
for (const k of keys)
|
|
304
|
+
out[k.slice(0, -3)] = seeds[k];
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
251
307
|
/** Revisión local (instantánea) de los renglones. Sin ruido: solo lo que hay que mirar. */
|
|
252
308
|
export function localIssues(lines, opts = {}) {
|
|
253
309
|
const issues = [];
|
|
@@ -1 +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;
|
|
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;AAgD1G,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;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;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,EACL,UAAkB,GACrB,EAAE,mBAAmB,+BA+tBrB"}
|
|
@@ -31,7 +31,7 @@ import { DocumentLinesGrid } from './line-items-editor';
|
|
|
31
31
|
import { computeTotals, serializeLineItems, taxBreakdown } from './line-items';
|
|
32
32
|
import { roundMoney, toAmount, useFormatter } from './format';
|
|
33
33
|
import { createCatalogProductSearch } from './catalog-product-search';
|
|
34
|
-
import { allocationAmountField, allocationIssueMessages, allocationPayload, creditStatus, editorLinesConfig, linesFromSource, localIssues, sourceLoadSummary, sourceTracksRemaining, partyDefaults, partySummaryRows, splitEditorFields, toOpenDocuments, friendlyOptionLabel, withFriendlyOptions, } from './document-editor-model';
|
|
34
|
+
import { allocationAmountField, allocationIssueMessages, allocationPayload, creditStatus, editorLinesConfig, linesFromSource, localIssues, sourceLoadSummary, sourceTracksRemaining, partyDefaults, partySummaryRows, recordLabel, seedRecord, sourceHeaderSeeds, splitEditorFields, toOpenDocuments, friendlyOptionLabel, withFriendlyOptions, } from './document-editor-model';
|
|
35
35
|
import { CollapsibleSection, DraftPreview, EditorSection, PartyCard, PreviewPanel, TotalsPanel, ValidationChecklist, } from './document-editor-panels';
|
|
36
36
|
import { PaymentAllocator } from './payment-allocator';
|
|
37
37
|
import { actionErrorBannerMessage, buildFieldDefaults, prefillFromFieldRelationRequest, renderField } from '../action-modal-dispatcher';
|
|
@@ -65,6 +65,9 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
65
65
|
const [extFields, setExtFields] = useState([]);
|
|
66
66
|
const [party, setParty] = useState(null);
|
|
67
67
|
const [partyLabels, setPartyLabels] = useState({});
|
|
68
|
+
// Etiquetas de los selectores que el editor llena por código (contraparte y
|
|
69
|
+
// documento origen de «Cargar desde…»): sin ellas el selector cerrado pinta el UUID.
|
|
70
|
+
const [seeds, setSeeds] = useState({});
|
|
68
71
|
const [openDocs, setOpenDocs] = useState([]);
|
|
69
72
|
const [docsLoading, setDocsLoading] = useState(false);
|
|
70
73
|
const [alloc, setAlloc] = useState({ strategy: 'oldest_due_first', manual: {} });
|
|
@@ -84,6 +87,7 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
84
87
|
// renglón para que el servidor descuente lo ya facturado/devuelto y valide.
|
|
85
88
|
const [loadedSource, setLoadedSource] = useState(null);
|
|
86
89
|
const [sourceNote, setSourceNote] = useState();
|
|
90
|
+
const [sourceSeed, setSourceSeed] = useState(null);
|
|
87
91
|
const relCache = useRef(new Map());
|
|
88
92
|
const taxRate = defaultTaxRate ?? orgTaxRate;
|
|
89
93
|
const search = useMemo(() => searchProducts ?? createCatalogProductSearch(api, { model: productModel, defaultTaxRate: taxRate }), [searchProducts, api, productModel, taxRate]);
|
|
@@ -132,6 +136,11 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
132
136
|
return;
|
|
133
137
|
setParty(rec);
|
|
134
138
|
setHeader((h) => ({ ...h, ...partyDefaults(rec, fieldKeys.current, h) }));
|
|
139
|
+
const label = recordLabel(rec, 'name') ?? recordLabel(rec, 'legal_name');
|
|
140
|
+
if (label && type.party) {
|
|
141
|
+
const key = type.party.field;
|
|
142
|
+
setSeeds((s) => ({ ...s, [key]: { value: String(partyId), label } }));
|
|
143
|
+
}
|
|
135
144
|
})
|
|
136
145
|
.catch(() => !cancelled && setParty(null));
|
|
137
146
|
return () => {
|
|
@@ -294,6 +303,28 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
294
303
|
return;
|
|
295
304
|
setBusy('source');
|
|
296
305
|
try {
|
|
306
|
+
// Cabecera primero (cliente, moneda, vínculo): llega aunque los
|
|
307
|
+
// renglones del origen fallen, y la tarjeta del cliente se carga.
|
|
308
|
+
if (src.header || src.link_field) {
|
|
309
|
+
const h = await api.get(`/data/${src.model}`, { params: { f_id: `eq:${id}`, per_page: 1 } });
|
|
310
|
+
const rec = ((h?.data?.data ?? [])[0] ?? {});
|
|
311
|
+
setHeader((prev) => {
|
|
312
|
+
const next = { ...prev };
|
|
313
|
+
for (const [to, from] of Object.entries(src.header ?? {})) {
|
|
314
|
+
const v = rec[from];
|
|
315
|
+
if (v == null || v === '')
|
|
316
|
+
continue;
|
|
317
|
+
next[to] = typeof v === 'object' && !Array.isArray(v) ? v.value ?? v.id ?? v : v;
|
|
318
|
+
}
|
|
319
|
+
if (src.link_field)
|
|
320
|
+
next[src.link_field] = id;
|
|
321
|
+
return next;
|
|
322
|
+
});
|
|
323
|
+
const seeded = sourceHeaderSeeds(src, rec, id, type.fields);
|
|
324
|
+
setSeeds((s) => ({ ...s, ...seeded }));
|
|
325
|
+
const label = src.link_field ? seeded[src.link_field]?.label : recordLabel(rec);
|
|
326
|
+
setSourceSeed(label ? { value: id, label } : null);
|
|
327
|
+
}
|
|
297
328
|
if (!isAllocation) {
|
|
298
329
|
const rows = await fetchSourceRows(src, id);
|
|
299
330
|
const loaded = linesFromSource(rows, src.map, { kind, discountMode: lineCfg.discount_mode, defaultTaxRate: taxRate });
|
|
@@ -313,19 +344,6 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
313
344
|
})
|
|
314
345
|
: t('documentEditor.source_loaded', { defaultValue: '{{count}} renglones cargados', count: loaded.length }));
|
|
315
346
|
}
|
|
316
|
-
if (src.header || src.link_field) {
|
|
317
|
-
const h = await api.get(`/data/${src.model}`, { params: { f_id: `eq:${id}`, per_page: 1 } });
|
|
318
|
-
const rec = ((h?.data?.data ?? [])[0] ?? {});
|
|
319
|
-
setHeader((prev) => {
|
|
320
|
-
const next = { ...prev };
|
|
321
|
-
for (const [to, from] of Object.entries(src.header ?? {}))
|
|
322
|
-
if (rec[from] != null)
|
|
323
|
-
next[to] = rec[from];
|
|
324
|
-
if (src.link_field)
|
|
325
|
-
next[src.link_field] = id;
|
|
326
|
-
return next;
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
347
|
setServerIssues([]);
|
|
330
348
|
}
|
|
331
349
|
catch (err) {
|
|
@@ -336,7 +354,7 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
336
354
|
finally {
|
|
337
355
|
setBusy(null);
|
|
338
356
|
}
|
|
339
|
-
}, [api, lineCfg, isAllocation, kind, t, fetchSourceRows, taxRate]);
|
|
357
|
+
}, [api, lineCfg, isAllocation, kind, t, fetchSourceRows, taxRate, type.fields]);
|
|
340
358
|
// Prefill al abrir desde un documento origen (acción de fila «Crear NC», «Facturar venta»…).
|
|
341
359
|
const autoLoaded = useRef(false);
|
|
342
360
|
useEffect(() => {
|
|
@@ -454,7 +472,8 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
454
472
|
setHeader((h) => ({ ...h, [key]: value }));
|
|
455
473
|
setFieldErrors((prev) => clearFieldErrorTree(prev, key));
|
|
456
474
|
};
|
|
457
|
-
const
|
|
475
|
+
const fieldRecord = useMemo(() => seedRecord(seeds, record), [seeds, record]);
|
|
476
|
+
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, fieldRecord, fieldErrors), fieldErrors[field.key] && _jsx("p", { className: "mt-1 text-xs text-destructive", children: fieldErrors[field.key] })] }, field.key)) : null;
|
|
458
477
|
const summaryRows = partySummaryRows(party, type.party?.summary, {
|
|
459
478
|
...Object.fromEntries(Object.entries(partyLabels).map(([k, v]) => [k, tl(v)])),
|
|
460
479
|
...Object.fromEntries(allFields.map((f) => [f.key, tl(f.label)])),
|
|
@@ -465,7 +484,7 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
465
484
|
const v = header[f.key];
|
|
466
485
|
if (v == null || v === '')
|
|
467
486
|
return null;
|
|
468
|
-
const opt = f.options
|
|
487
|
+
const opt = Array.isArray(f.options) ? f.options.find((o) => String(o.value) === String(v)) : undefined;
|
|
469
488
|
return opt ? String(opt.value) : String(v);
|
|
470
489
|
})
|
|
471
490
|
.filter(Boolean)
|
|
@@ -501,7 +520,7 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
501
520
|
const loadFrom = 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…' }) : sourceNote, children: _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [sources.length > 1 && (_jsx("div", { role: "radiogroup", "aria-label": t('documentEditor.source_kind', { defaultValue: 'Tipo de documento origen' }), className: "inline-flex flex-wrap gap-1 rounded-md border p-1", children: sources.map((s) => (_jsx("button", { type: "button", role: "radio", "aria-checked": s.key === sourceKey, "data-source-kind": s.key, onClick: () => {
|
|
502
521
|
setSourceKey(s.key);
|
|
503
522
|
setSourceId('');
|
|
504
|
-
}, className: `rounded px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${s.key === sourceKey ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-accent'}`, children: tl(s.label) }, s.key))) })), activeSource && (_jsx(SourcePicker, { source: activeSource, value: sourceId, header: header, label: tl(activeSource.label), onPick: (id) => {
|
|
523
|
+
}, className: `rounded px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${s.key === sourceKey ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-accent'}`, children: tl(s.label) }, s.key))) })), activeSource && (_jsx(SourcePicker, { source: activeSource, value: sourceId, seed: sourceSeed, header: header, label: tl(activeSource.label), onPick: (id) => {
|
|
505
524
|
setSourceKey(activeSource.key);
|
|
506
525
|
setSourceId(id);
|
|
507
526
|
if (id)
|
|
@@ -519,15 +538,16 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
519
538
|
.filter(visible)
|
|
520
539
|
.map((f) => {
|
|
521
540
|
const v = header[f.key];
|
|
522
|
-
const opt = f.options
|
|
523
|
-
|
|
541
|
+
const opt = Array.isArray(f.options) ? f.options.find((o) => String(o.value) === String(v)) : undefined;
|
|
542
|
+
const seeded = seeds[f.key] && String(seeds[f.key].value) === String(v) ? seeds[f.key].label : undefined;
|
|
543
|
+
return { label: tl(f.label), value: v == null || v === '' ? '' : opt ? friendlyOptionLabel(tl(String(opt.label ?? v))) : seeded ?? String(v) };
|
|
524
544
|
})
|
|
525
545
|
.filter((r) => r.value), lines: lines, totals: totalsRows, fmt: fmt }) })) : null;
|
|
526
546
|
const checklist = _jsx(ValidationChecklist, { issues: shownIssues, title: t('documentEditor.review', { defaultValue: 'Revisa antes de guardar' }) });
|
|
527
547
|
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", "data-slot": "document-editor", "data-kind": kind, "data-layout": fullscreen ? 'fullscreen' : 'dialog', children: [fullscreen ? (_jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-2 lg:grid lg:grid-cols-[minmax(0,1fr)_340px] lg:gap-8", children: [_jsxs("div", { className: "min-w-0 space-y-8", "data-slot": "editor-main", children: [_jsx(FormErrorBanner, { message: formError }), essentials, loadFrom, linesSection, advanced, contributedSections, notes] }), _jsxs("aside", { className: "mt-8 space-y-6 lg:sticky lg:top-0 lg:mt-0 lg:self-start", "data-slot": "editor-aside", children: [_jsx("div", { className: "rounded-lg border p-4", children: totalsPanel }), checklist, previewSection] })] })) : (_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 }), essentials, loadFrom, linesSection, advanced, contributedSections, notes, previewSection, checklist] })), _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' })] })] })] }));
|
|
528
548
|
}
|
|
529
549
|
/** Selector del documento origen (mismo widget de relación que el resto de formularios). */
|
|
530
|
-
function SourcePicker({ source, value, header, label, onPick, }) {
|
|
550
|
+
function SourcePicker({ source, value, seed, header, label, onPick, }) {
|
|
531
551
|
const field = useMemo(() => ({
|
|
532
552
|
key: `__source_${source.key}`,
|
|
533
553
|
label,
|
|
@@ -535,5 +555,5 @@ function SourcePicker({ source, value, header, label, onPick, }) {
|
|
|
535
555
|
ref: source.model,
|
|
536
556
|
option_filter: source.option_filter,
|
|
537
557
|
}), [source, label]);
|
|
538
|
-
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) }));
|
|
558
|
+
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, seed ? { id: seed.value, label: seed.label } : undefined) }));
|
|
539
559
|
}
|
|
@@ -4,6 +4,12 @@ import type { TableMetadata } from './types';
|
|
|
4
4
|
* metida en `to` el router la tomaba como parte del path y la ruta no casaba.
|
|
5
5
|
*/
|
|
6
6
|
export declare function linkNavigation(url: string): {
|
|
7
|
+
to: '/m/$model';
|
|
8
|
+
params: {
|
|
9
|
+
model: string;
|
|
10
|
+
};
|
|
11
|
+
search?: Record<string, string>;
|
|
12
|
+
} | {
|
|
7
13
|
to: string;
|
|
8
14
|
search?: Record<string, string>;
|
|
9
15
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,SAAS,CAAA;AAE3E;;;GAGG;AACH,wBAAgB,cAAc,
|
|
1
|
+
{"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,SAAS,CAAA;AAE3E;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,GAAG,EAAE,MAAM,GACZ;IAAE,EAAE,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CASnI;AAGD,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CA6IhD"}
|
|
@@ -36,9 +36,14 @@ import { RequiresAddonDialog, resolveRequiresAddon } from './requires-addon';
|
|
|
36
36
|
*/
|
|
37
37
|
export function linkNavigation(url) {
|
|
38
38
|
const i = url.indexOf('?');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const path = i < 0 ? url : url.slice(0, i);
|
|
40
|
+
const search = i < 0 ? undefined : Object.fromEntries(new URLSearchParams(url.slice(i + 1)));
|
|
41
|
+
// /m/<tabla>?create=&from=&from_id= → ruta parametrizada (retest r6b: el menú
|
|
42
|
+
// «Nota de crédito» de la factura aterrizaba en la lista sin abrir el editor).
|
|
43
|
+
const m = path.match(/^\/m\/([^/?#]+)\/?$/);
|
|
44
|
+
if (m)
|
|
45
|
+
return { to: '/m/$model', params: { model: m[1] }, search };
|
|
46
|
+
return search ? { to: path, search } : { to: path };
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* Owns the state + dialogs + dispatch logic for per-row actions, shared by
|
|
@@ -78,7 +83,7 @@ export function useDynamicRowActions({ model, endpoint, mutationEndpoint, metada
|
|
|
78
83
|
}
|
|
79
84
|
const linkDef = metadata?.actions?.find((a) => a.key === action && a.type === 'link');
|
|
80
85
|
if (linkDef?.linkUrl) {
|
|
81
|
-
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_, field) =>
|
|
86
|
+
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_, field) => String(row[field] ?? ''));
|
|
82
87
|
navigate(linkNavigation(url));
|
|
83
88
|
return;
|
|
84
89
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
// Retest Pitsline r6 (P0 UX): «Cargar desde» venta/OT/cotización debe dejar el
|
|
3
|
+
// CLIENTE elegido (su nombre en el selector y su tarjeta), aunque los renglones
|
|
4
|
+
// del origen fallen; y el selector «Factura a abonar» de la NC abierta desde una
|
|
5
|
+
// factura muestra el folio, no el UUID. El selector es el dynamic_select real
|
|
6
|
+
// (cerrado: no consulta opciones), así que la etiqueta sale de lo que el editor
|
|
7
|
+
// ya sabe del registro.
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
9
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
10
|
+
|
|
11
|
+
vi.mock('@tanstack/react-router', () => ({ useNavigate: () => () => {} }))
|
|
12
|
+
vi.mock('react-i18next', () => {
|
|
13
|
+
const value = {
|
|
14
|
+
t: (k: string, o?: Record<string, any>) => {
|
|
15
|
+
let s: string = o?.defaultValue ?? k
|
|
16
|
+
for (const [key, v] of Object.entries(o ?? {})) s = s.replace(`{{${key}}}`, String(v))
|
|
17
|
+
return s
|
|
18
|
+
},
|
|
19
|
+
i18n: { language: 'es' },
|
|
20
|
+
}
|
|
21
|
+
return { useTranslation: () => value }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
import { DocumentFormDialog } from '../document-form-dialog'
|
|
25
|
+
import { ApiProvider, type ApiClient } from '../api-context'
|
|
26
|
+
import { TaxRateContext } from '../org-runtime-context'
|
|
27
|
+
import { seedRecord, sourceHeaderSeeds } from '../business/document-editor-model'
|
|
28
|
+
import type { DocumentFormsManifest, DocumentFormType } from '../types'
|
|
29
|
+
|
|
30
|
+
afterEach(cleanup)
|
|
31
|
+
|
|
32
|
+
const CUSTOMER = '6f1c2a9e-3b7d-4c55-9a51-0d2b8e7f4a10'
|
|
33
|
+
const INVOICE = '0b9d7c1e-5a4f-4e2b-8c3d-7f6e5d4c3b2a'
|
|
34
|
+
|
|
35
|
+
const invoice: DocumentFormType = {
|
|
36
|
+
key: 'invoice',
|
|
37
|
+
label: 'Factura',
|
|
38
|
+
layout: 'editor',
|
|
39
|
+
party: { field: 'customer_id', model: 'customers.Customer', summary: ['tax_id'] },
|
|
40
|
+
fields: [
|
|
41
|
+
{ key: 'customer_id', label: 'Cliente', type: 'dynamic_select', ref: 'customers.Customer', required: true } as any,
|
|
42
|
+
{ key: 'work_order_id', label: 'Orden de trabajo', type: 'dynamic_select', ref: 'workshop.WorkOrder' } as any,
|
|
43
|
+
],
|
|
44
|
+
lines: { title: 'Renglones' },
|
|
45
|
+
sources: [
|
|
46
|
+
{ key: 'work_order', label: 'Orden de trabajo', model: 'workshop.WorkOrder', lines: 'items', header: { customer_id: 'customer_id' }, link_field: 'work_order_id', line_link_field: 'work_order_item_id' },
|
|
47
|
+
],
|
|
48
|
+
submit_label: 'Crear factura',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const creditNote: DocumentFormType = {
|
|
52
|
+
key: 'credit_note',
|
|
53
|
+
label: 'Nota de crédito',
|
|
54
|
+
layout: 'editor',
|
|
55
|
+
fields: [
|
|
56
|
+
{ key: 'invoice_id', label: 'Factura a abonar', type: 'dynamic_select', ref: 'customers.Invoice', required: true, options: { source: 'customers.Invoice', value: 'id', label: 'number' } } as any,
|
|
57
|
+
],
|
|
58
|
+
lines: { kind: 'credit', title: 'Conceptos' },
|
|
59
|
+
sources: [{ key: 'invoice', label: 'Factura', model: 'customers.Invoice', lines: 'items', link_field: 'invoice_id', line_link_field: 'invoice_item_id' }],
|
|
60
|
+
submit_label: 'Crear nota de crédito',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeApi(rows: Record<string, unknown[]>) {
|
|
64
|
+
const get = vi.fn(async (url: string) => {
|
|
65
|
+
if (url === '/data/customers.Customer') return { data: { data: [{ id: CUSTOMER, name: 'Transportes del Bajío', tax_id: 'TBA850312KJ9' }] } }
|
|
66
|
+
if (url.startsWith('/data/')) return { data: { data: rows[url.slice(6)] ?? [] } }
|
|
67
|
+
// Los renglones del origen no cargan (host sin source-lines ni relación).
|
|
68
|
+
if (url.endsWith('/source-lines') || url.startsWith('/metadata/table/')) throw new Error('boom')
|
|
69
|
+
return { data: { data: [] } }
|
|
70
|
+
})
|
|
71
|
+
const client = { get, post: vi.fn(), put: vi.fn(), delete: vi.fn() }
|
|
72
|
+
return client as unknown as ApiClient
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function open(model: string, type: DocumentFormType, api: ApiClient, initialSource: { key: string; id: string }) {
|
|
76
|
+
const forms: DocumentFormsManifest = { lines_field: 'items', types: [type] }
|
|
77
|
+
return render(
|
|
78
|
+
<ApiProvider client={api}>
|
|
79
|
+
<TaxRateContext.Provider value={0.16}>
|
|
80
|
+
<DocumentFormDialog open onOpenChange={() => {}} model={model} forms={forms} initialType={type.key} initialSource={initialSource} currency="MXN" />
|
|
81
|
+
</TaxRateContext.Provider>
|
|
82
|
+
</ApiProvider>,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const trigger = (key: string) => document.getElementById(key) as HTMLElement
|
|
87
|
+
|
|
88
|
+
describe('«Cargar desde» llena el cliente', () => {
|
|
89
|
+
it('OT: el selector muestra el cliente por nombre y su tarjeta carga aunque los renglones fallen', async () => {
|
|
90
|
+
const api = makeApi({ 'workshop.WorkOrder': [{ id: 'wo-1', order_number: 'OT-00031', customer_id: CUSTOMER }] })
|
|
91
|
+
open('customers.Invoice', invoice, api, { key: 'work_order', id: 'wo-1' })
|
|
92
|
+
await waitFor(() => expect(trigger('customer_id').textContent).toContain('Transportes del Bajío'))
|
|
93
|
+
expect(trigger('customer_id').textContent).not.toContain(CUSTOMER)
|
|
94
|
+
expect(document.querySelector('[data-slot="party-card"]')).toBeTruthy()
|
|
95
|
+
expect(trigger('work_order_id').textContent).toContain('OT-00031')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('el nombre del cliente que el host sirve junto a la FK se usa de inmediato', async () => {
|
|
99
|
+
const api = makeApi({
|
|
100
|
+
'workshop.WorkOrder': [{ id: 'wo-1', customer_id: CUSTOMER, customer: { value: CUSTOMER, label: 'Transportes del Bajío' } }],
|
|
101
|
+
})
|
|
102
|
+
open('customers.Invoice', invoice, api, { key: 'work_order', id: 'wo-1' })
|
|
103
|
+
await waitFor(() => expect(trigger('customer_id').textContent).toContain('Transportes del Bajío'))
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('NC desde la factura', () => {
|
|
108
|
+
it('«Factura a abonar» muestra el folio, no el UUID', async () => {
|
|
109
|
+
const api = makeApi({ 'customers.Invoice': [{ id: INVOICE, number: 'FAC-00014', customer_id: CUSTOMER }] })
|
|
110
|
+
open('fiscal_mexico.CreditNote', creditNote, api, { key: 'invoice', id: INVOICE })
|
|
111
|
+
await waitFor(() => expect(trigger('invoice_id').textContent).toContain('FAC-00014'))
|
|
112
|
+
expect(trigger('invoice_id').textContent).not.toContain(INVOICE)
|
|
113
|
+
expect(screen.queryByText(INVOICE)).toBeNull()
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('reglas puras', () => {
|
|
118
|
+
it('sourceHeaderSeeds: hermano {value,label} de la FK y folio del origen', () => {
|
|
119
|
+
const seeds = sourceHeaderSeeds(
|
|
120
|
+
invoice.sources![0],
|
|
121
|
+
{ id: 'wo-1', order_number: 'OT-7', customer_id: 'c-1', customer: { value: 'c-1', label: 'Ana' } },
|
|
122
|
+
'wo-1',
|
|
123
|
+
invoice.fields,
|
|
124
|
+
)
|
|
125
|
+
expect(seeds).toEqual({ customer_id: { value: 'c-1', label: 'Ana' }, work_order_id: { value: 'wo-1', label: 'OT-7' } })
|
|
126
|
+
expect(sourceHeaderSeeds(invoice.sources![0], { id: 'wo-1', customer_id: 'c-1' }, 'wo-1')).toEqual({})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('seedRecord: siembra el hermano y conserva el registro de origen', () => {
|
|
130
|
+
expect(seedRecord({}, undefined)).toBeUndefined()
|
|
131
|
+
expect(seedRecord({ customer_id: { value: 'c-1', label: 'Ana' } }, { id: 'inv-1', number: 'F-1' })).toEqual({
|
|
132
|
+
id: 'inv-1',
|
|
133
|
+
number: 'F-1',
|
|
134
|
+
customer: { value: 'c-1', label: 'Ana' },
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { linkNavigation } from '../dynamic-row-actions'
|
|
3
3
|
|
|
4
|
-
// «Crear desde»: el botón de la fila del origen es una acción link con query
|
|
5
|
-
// (`/m/invoices?create=invoice&from=sales_order&from_id={id}`). En `to` el
|
|
6
|
-
// router la tomaba como parte del path y `$model` quedaba "invoices?create=…".
|
|
7
4
|
describe('linkNavigation', () => {
|
|
8
|
-
it('
|
|
5
|
+
it('parte /m/<tabla>?create… en ruta parametrizada + search (retest r6b)', () => {
|
|
9
6
|
expect(linkNavigation('/m/invoices?create=invoice&from=sales_order&from_id=so-1')).toEqual({
|
|
10
|
-
to: '/m
|
|
7
|
+
to: '/m/$model',
|
|
8
|
+
params: { model: 'invoices' },
|
|
11
9
|
search: { create: 'invoice', from: 'sales_order', from_id: 'so-1' },
|
|
12
10
|
})
|
|
11
|
+
expect(linkNavigation('/m/credit_notes?create=credit_note&from=invoice&from_id=fac-1')).toEqual({
|
|
12
|
+
to: '/m/$model',
|
|
13
|
+
params: { model: 'credit_notes' },
|
|
14
|
+
search: { create: 'credit_note', from: 'invoice', from_id: 'fac-1' },
|
|
15
|
+
})
|
|
13
16
|
})
|
|
14
|
-
it('
|
|
17
|
+
it('deja paths que no son /m/<tabla> como pathname', () => {
|
|
15
18
|
expect(linkNavigation('/m/quotes/abc')).toEqual({ to: '/m/quotes/abc' })
|
|
19
|
+
expect(linkNavigation('/settings')).toEqual({ to: '/settings' })
|
|
16
20
|
})
|
|
17
21
|
})
|
|
@@ -292,6 +292,74 @@ export function sourceTracksRemaining(src: Pick<DocumentFormSource, 'line_link_f
|
|
|
292
292
|
return !!(src.line_link_field || src.remaining_qty_field || src.remaining_endpoint)
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/** Opción ya resuelta de un selector de la cabecera que el editor llenó por código. */
|
|
296
|
+
export interface SeedLabel {
|
|
297
|
+
value: string
|
|
298
|
+
label: string
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Columnas con las que se nombra un documento (folio antes que nombre). */
|
|
302
|
+
const DOCUMENT_LABEL_COLUMNS = ['number', 'folio', 'order_number', 'name', 'title', 'code']
|
|
303
|
+
|
|
304
|
+
function declaredOptionLabel(f: ActionFieldDef | undefined): string | undefined {
|
|
305
|
+
const opts = (f as { options?: unknown } | undefined)?.options
|
|
306
|
+
const l = opts && !Array.isArray(opts) && typeof opts === 'object' ? (opts as { label?: unknown }).label : undefined
|
|
307
|
+
return typeof l === 'string' && l ? l : undefined
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Nombre visible de un registro: la columna declarada por el campo, luego folio/nombre. */
|
|
311
|
+
export function recordLabel(rec: Record<string, any> | null | undefined, preferred?: string): string | undefined {
|
|
312
|
+
if (!rec) return undefined
|
|
313
|
+
for (const c of preferred ? [preferred, ...DOCUMENT_LABEL_COLUMNS] : DOCUMENT_LABEL_COLUMNS) {
|
|
314
|
+
const v = rec[c]
|
|
315
|
+
if ((typeof v === 'string' && v.trim()) || typeof v === 'number') return String(v)
|
|
316
|
+
}
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Etiquetas de lo que «Cargar desde…» escribe en la cabecera, para que el
|
|
322
|
+
* selector muestre el nombre y no el UUID sin abrirse:
|
|
323
|
+
* - `header` (`customer_id ← customer_id`): el objeto hermano `{value,label}`
|
|
324
|
+
* que el host sirve junto a la FK (`customer`);
|
|
325
|
+
* - `link_field`: el propio documento origen, con su folio.
|
|
326
|
+
*/
|
|
327
|
+
export function sourceHeaderSeeds(
|
|
328
|
+
src: Pick<DocumentFormSource, 'header' | 'link_field'>,
|
|
329
|
+
rec: Record<string, any>,
|
|
330
|
+
id: string,
|
|
331
|
+
fields: readonly ActionFieldDef[] = [],
|
|
332
|
+
): Record<string, SeedLabel> {
|
|
333
|
+
const out: Record<string, SeedLabel> = {}
|
|
334
|
+
for (const [to, from] of Object.entries(src.header ?? {})) {
|
|
335
|
+
if (!from.endsWith('_id')) continue
|
|
336
|
+
const v = refValue(rec[from])
|
|
337
|
+
const label = refLabel(rec[from]) ?? refLabel(rec[from.slice(0, -3)])
|
|
338
|
+
if (v && label) out[to] = { value: v, label }
|
|
339
|
+
}
|
|
340
|
+
if (src.link_field) {
|
|
341
|
+
const label = recordLabel(rec, declaredOptionLabel(fields.find((f) => f.key === src.link_field)))
|
|
342
|
+
if (label) out[src.link_field] = { value: id, label }
|
|
343
|
+
}
|
|
344
|
+
return out
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Registro para `renderField` con las etiquetas sembradas como objeto hermano
|
|
349
|
+
* (`customer_id` → `customer: {value,label}`), el mismo contrato que sirve el
|
|
350
|
+
* host en una fila. Conserva el registro desde el que se abrió.
|
|
351
|
+
*/
|
|
352
|
+
export function seedRecord(
|
|
353
|
+
seeds: Record<string, SeedLabel>,
|
|
354
|
+
record?: Record<string, any>,
|
|
355
|
+
): Record<string, any> | undefined {
|
|
356
|
+
const keys = Object.keys(seeds).filter((k) => k.endsWith('_id'))
|
|
357
|
+
if (keys.length === 0) return record
|
|
358
|
+
const out: Record<string, any> = { ...(record ?? {}) }
|
|
359
|
+
for (const k of keys) out[k.slice(0, -3)] = seeds[k]
|
|
360
|
+
return out
|
|
361
|
+
}
|
|
362
|
+
|
|
295
363
|
export interface EditorIssue {
|
|
296
364
|
field?: string
|
|
297
365
|
severity: 'error' | 'warning'
|
|
@@ -42,11 +42,15 @@ import {
|
|
|
42
42
|
sourceTracksRemaining,
|
|
43
43
|
partyDefaults,
|
|
44
44
|
partySummaryRows,
|
|
45
|
+
recordLabel,
|
|
46
|
+
seedRecord,
|
|
47
|
+
sourceHeaderSeeds,
|
|
45
48
|
splitEditorFields,
|
|
46
49
|
toOpenDocuments,
|
|
47
50
|
friendlyOptionLabel,
|
|
48
51
|
withFriendlyOptions,
|
|
49
52
|
type EditorIssue,
|
|
53
|
+
type SeedLabel,
|
|
50
54
|
} from './document-editor-model'
|
|
51
55
|
import {
|
|
52
56
|
CollapsibleSection,
|
|
@@ -140,6 +144,9 @@ export function DocumentEditor({
|
|
|
140
144
|
const [extFields, setExtFields] = useState<ActionFieldDef[]>([])
|
|
141
145
|
const [party, setParty] = useState<Record<string, any> | null>(null)
|
|
142
146
|
const [partyLabels, setPartyLabels] = useState<Record<string, string>>({})
|
|
147
|
+
// Etiquetas de los selectores que el editor llena por código (contraparte y
|
|
148
|
+
// documento origen de «Cargar desde…»): sin ellas el selector cerrado pinta el UUID.
|
|
149
|
+
const [seeds, setSeeds] = useState<Record<string, SeedLabel>>({})
|
|
143
150
|
const [openDocs, setOpenDocs] = useState<OpenDocument[]>([])
|
|
144
151
|
const [docsLoading, setDocsLoading] = useState(false)
|
|
145
152
|
const [alloc, setAlloc] = useState<PaymentAllocatorValue>({ strategy: 'oldest_due_first', manual: {} })
|
|
@@ -159,6 +166,7 @@ export function DocumentEditor({
|
|
|
159
166
|
// renglón para que el servidor descuente lo ya facturado/devuelto y valide.
|
|
160
167
|
const [loadedSource, setLoadedSource] = useState<DocumentFormSource | null>(null)
|
|
161
168
|
const [sourceNote, setSourceNote] = useState<string | undefined>()
|
|
169
|
+
const [sourceSeed, setSourceSeed] = useState<SeedLabel | null>(null)
|
|
162
170
|
const relCache = useRef(new Map<string, Promise<any[]>>())
|
|
163
171
|
|
|
164
172
|
const taxRate = defaultTaxRate ?? orgTaxRate
|
|
@@ -223,6 +231,11 @@ export function DocumentEditor({
|
|
|
223
231
|
if (cancelled) return
|
|
224
232
|
setParty(rec)
|
|
225
233
|
setHeader((h) => ({ ...h, ...partyDefaults(rec, fieldKeys.current, h) }))
|
|
234
|
+
const label = recordLabel(rec, 'name') ?? recordLabel(rec, 'legal_name')
|
|
235
|
+
if (label && type.party) {
|
|
236
|
+
const key = type.party.field
|
|
237
|
+
setSeeds((s) => ({ ...s, [key]: { value: String(partyId), label } }))
|
|
238
|
+
}
|
|
226
239
|
})
|
|
227
240
|
.catch(() => !cancelled && setParty(null))
|
|
228
241
|
return () => {
|
|
@@ -392,6 +405,26 @@ export function DocumentEditor({
|
|
|
392
405
|
if (!lineCfg || !id) return
|
|
393
406
|
setBusy('source')
|
|
394
407
|
try {
|
|
408
|
+
// Cabecera primero (cliente, moneda, vínculo): llega aunque los
|
|
409
|
+
// renglones del origen fallen, y la tarjeta del cliente se carga.
|
|
410
|
+
if (src.header || src.link_field) {
|
|
411
|
+
const h = await api.get(`/data/${src.model}`, { params: { f_id: `eq:${id}`, per_page: 1 } })
|
|
412
|
+
const rec = ((h?.data?.data ?? [])[0] ?? {}) as Record<string, any>
|
|
413
|
+
setHeader((prev) => {
|
|
414
|
+
const next = { ...prev }
|
|
415
|
+
for (const [to, from] of Object.entries(src.header ?? {})) {
|
|
416
|
+
const v = rec[from]
|
|
417
|
+
if (v == null || v === '') continue
|
|
418
|
+
next[to] = typeof v === 'object' && !Array.isArray(v) ? v.value ?? v.id ?? v : v
|
|
419
|
+
}
|
|
420
|
+
if (src.link_field) next[src.link_field] = id
|
|
421
|
+
return next
|
|
422
|
+
})
|
|
423
|
+
const seeded = sourceHeaderSeeds(src, rec, id, type.fields)
|
|
424
|
+
setSeeds((s) => ({ ...s, ...seeded }))
|
|
425
|
+
const label = src.link_field ? seeded[src.link_field]?.label : recordLabel(rec)
|
|
426
|
+
setSourceSeed(label ? { value: id, label } : null)
|
|
427
|
+
}
|
|
395
428
|
if (!isAllocation) {
|
|
396
429
|
const rows = await fetchSourceRows(src, id)
|
|
397
430
|
const loaded = linesFromSource(rows, src.map, { kind, discountMode: lineCfg.discount_mode, defaultTaxRate: taxRate })
|
|
@@ -413,16 +446,6 @@ export function DocumentEditor({
|
|
|
413
446
|
: t('documentEditor.source_loaded', { defaultValue: '{{count}} renglones cargados', count: loaded.length }),
|
|
414
447
|
)
|
|
415
448
|
}
|
|
416
|
-
if (src.header || src.link_field) {
|
|
417
|
-
const h = await api.get(`/data/${src.model}`, { params: { f_id: `eq:${id}`, per_page: 1 } })
|
|
418
|
-
const rec = ((h?.data?.data ?? [])[0] ?? {}) as Record<string, any>
|
|
419
|
-
setHeader((prev) => {
|
|
420
|
-
const next = { ...prev }
|
|
421
|
-
for (const [to, from] of Object.entries(src.header ?? {})) if (rec[from] != null) next[to] = rec[from]
|
|
422
|
-
if (src.link_field) next[src.link_field] = id
|
|
423
|
-
return next
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
449
|
setServerIssues([])
|
|
427
450
|
} catch (err) {
|
|
428
451
|
toast.error(t('documentEditor.source_failed', { defaultValue: 'No se pudo cargar el documento' }), {
|
|
@@ -432,7 +455,7 @@ export function DocumentEditor({
|
|
|
432
455
|
setBusy(null)
|
|
433
456
|
}
|
|
434
457
|
},
|
|
435
|
-
[api, lineCfg, isAllocation, kind, t, fetchSourceRows, taxRate],
|
|
458
|
+
[api, lineCfg, isAllocation, kind, t, fetchSourceRows, taxRate, type.fields],
|
|
436
459
|
)
|
|
437
460
|
|
|
438
461
|
// Prefill al abrir desde un documento origen (acción de fila «Crear NC», «Facturar venta»…).
|
|
@@ -549,13 +572,14 @@ export function DocumentEditor({
|
|
|
549
572
|
setFieldErrors((prev) => clearFieldErrorTree(prev, key))
|
|
550
573
|
}
|
|
551
574
|
|
|
575
|
+
const fieldRecord = useMemo(() => seedRecord(seeds, record), [seeds, record])
|
|
552
576
|
const fieldCell = (field: ActionFieldDef, fullWidth = false) =>
|
|
553
577
|
visible(field) ? (
|
|
554
578
|
<FieldCell key={field.key} fullWidth={fullWidth}>
|
|
555
579
|
<FieldLabel htmlFor={field.key} required={field.required}>
|
|
556
580
|
{tl(field.label)}
|
|
557
581
|
</FieldLabel>
|
|
558
|
-
{renderField(withFriendlyOptions(field), header[field.key], (v: any) => updateField(field.key, v), header,
|
|
582
|
+
{renderField(withFriendlyOptions(field), header[field.key], (v: any) => updateField(field.key, v), header, fieldRecord, fieldErrors)}
|
|
559
583
|
{fieldErrors[field.key] && <p className="mt-1 text-xs text-destructive">{fieldErrors[field.key]}</p>}
|
|
560
584
|
</FieldCell>
|
|
561
585
|
) : null
|
|
@@ -569,7 +593,7 @@ export function DocumentEditor({
|
|
|
569
593
|
.map((f) => {
|
|
570
594
|
const v = header[f.key]
|
|
571
595
|
if (v == null || v === '') return null
|
|
572
|
-
const opt = f.options
|
|
596
|
+
const opt = Array.isArray(f.options) ? f.options.find((o) => String(o.value) === String(v)) : undefined
|
|
573
597
|
return opt ? String(opt.value) : String(v)
|
|
574
598
|
})
|
|
575
599
|
.filter(Boolean)
|
|
@@ -658,6 +682,7 @@ export function DocumentEditor({
|
|
|
658
682
|
key={activeSource.key}
|
|
659
683
|
source={activeSource}
|
|
660
684
|
value={sourceId}
|
|
685
|
+
seed={sourceSeed}
|
|
661
686
|
header={header}
|
|
662
687
|
label={tl(activeSource.label)}
|
|
663
688
|
onPick={(id) => {
|
|
@@ -769,8 +794,9 @@ export function DocumentEditor({
|
|
|
769
794
|
.filter(visible)
|
|
770
795
|
.map((f) => {
|
|
771
796
|
const v = header[f.key]
|
|
772
|
-
const opt = f.options
|
|
773
|
-
|
|
797
|
+
const opt = Array.isArray(f.options) ? f.options.find((o) => String(o.value) === String(v)) : undefined
|
|
798
|
+
const seeded = seeds[f.key] && String(seeds[f.key].value) === String(v) ? seeds[f.key].label : undefined
|
|
799
|
+
return { label: tl(f.label), value: v == null || v === '' ? '' : opt ? friendlyOptionLabel(tl(String(opt.label ?? v))) : seeded ?? String(v) }
|
|
774
800
|
})
|
|
775
801
|
.filter((r) => r.value)}
|
|
776
802
|
lines={lines}
|
|
@@ -835,12 +861,15 @@ export function DocumentEditor({
|
|
|
835
861
|
function SourcePicker({
|
|
836
862
|
source,
|
|
837
863
|
value,
|
|
864
|
+
seed,
|
|
838
865
|
header,
|
|
839
866
|
label,
|
|
840
867
|
onPick,
|
|
841
868
|
}: {
|
|
842
869
|
source: DocumentFormSource
|
|
843
870
|
value: string
|
|
871
|
+
/** Folio del origen ya cargado: el selector cerrado lo muestra en vez del UUID. */
|
|
872
|
+
seed?: SeedLabel | null
|
|
844
873
|
header: Record<string, any>
|
|
845
874
|
label: string
|
|
846
875
|
onPick: (id: string) => void
|
|
@@ -858,7 +887,7 @@ function SourcePicker({
|
|
|
858
887
|
)
|
|
859
888
|
return (
|
|
860
889
|
<div className="min-w-[240px] flex-1" data-source={source.key}>
|
|
861
|
-
{renderField(field, value, (v: any) => onPick(v == null || v === '' ? '' : String(v)), header)}
|
|
890
|
+
{renderField(field, value, (v: any) => onPick(v == null || v === '' ? '' : String(v)), header, seed ? { id: seed.value, label: seed.label } : undefined)}
|
|
862
891
|
</div>
|
|
863
892
|
)
|
|
864
893
|
}
|
|
@@ -44,12 +44,20 @@ import type { TableMetadata, ActionMetadata, RequiresAddon } from './types'
|
|
|
44
44
|
* Destino de una acción link. La query (`?create=…&from_id=…`) va como `search`:
|
|
45
45
|
* metida en `to` el router la tomaba como parte del path y la ruta no casaba.
|
|
46
46
|
*/
|
|
47
|
-
export function linkNavigation(
|
|
47
|
+
export function linkNavigation(
|
|
48
|
+
url: string,
|
|
49
|
+
): { to: '/m/$model'; params: { model: string }; search?: Record<string, string> } | { to: string; search?: Record<string, string> } {
|
|
48
50
|
const i = url.indexOf('?')
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const path = i < 0 ? url : url.slice(0, i)
|
|
52
|
+
const search = i < 0 ? undefined : Object.fromEntries(new URLSearchParams(url.slice(i + 1)))
|
|
53
|
+
// /m/<tabla>?create=&from=&from_id= → ruta parametrizada (retest r6b: el menú
|
|
54
|
+
// «Nota de crédito» de la factura aterrizaba en la lista sin abrir el editor).
|
|
55
|
+
const m = path.match(/^\/m\/([^/?#]+)\/?$/)
|
|
56
|
+
if (m) return { to: '/m/$model', params: { model: m[1] }, search }
|
|
57
|
+
return search ? { to: path, search } : { to: path }
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
|
|
53
61
|
export interface UseDynamicRowActionsParams {
|
|
54
62
|
/** Model key as registered on the backend (e.g. "issue"). */
|
|
55
63
|
model: string
|
|
@@ -138,7 +146,7 @@ export function useDynamicRowActions({
|
|
|
138
146
|
}
|
|
139
147
|
const linkDef = metadata?.actions?.find((a) => a.key === action && a.type === 'link')
|
|
140
148
|
if (linkDef?.linkUrl) {
|
|
141
|
-
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_: string, field: string) =>
|
|
149
|
+
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_: string, field: string) => String(row[field] ?? ''))
|
|
142
150
|
navigate(linkNavigation(url) as any)
|
|
143
151
|
return
|
|
144
152
|
}
|