@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 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":"AAMA,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGrE,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACzF,OAAO,KAAK,EACR,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,EACrB,gBAAgB,EACnB,MAAM,UAAU,CAAA;AAEjB,eAAO,MAAM,gBAAgB,iBAAiB,CAAA;AAK9C,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI7G;AAED,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,CAAC,GAAG,iBAAiB,GAAG,SAAS,CAK1I;AAeD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,EAAE,aAAa,GAAE,WAAW,CAAC,MAAM,CAAa,GAAG,OAAO,CAM1G;AAED,MAAM,WAAW,iBAAiB;IAC9B,iFAAiF;IACjF,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,8CAA8C;IAC9C,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,uCAAuC;IACvC,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,yCAAyC;IACzC,KAAK,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAC7B,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CAAO,GACnH,iBAAiB,CAgBnB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,cAAc,GAAG,cAAc,CAGrE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACjC,iBAAiB,EAAE,SAAS,MAAM,EAAE,EACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACjC,OAAO,GAAE,SAAS,MAAM,EAAO,EAC/B,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACnC,KAAK,SAAI,GACV,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAYtD;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+FAA+F;IAC/F,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,CAAA;CACjC;AAED,wBAAgB,YAAY,CACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACjC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAC9F,aAAa,EAAE,MAAM,GACtB,YAAY,GAAG,IAAI,CAarB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC3B,IAAI,EAAE,OAAO,EACb,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAChC,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAAC,YAAY,CAAC,EAAE,iBAAiB,CAAC,eAAe,CAAC,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5H,QAAQ,EAAE,CAsCZ;AA8BD,gHAAgH;AAChH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,CAAC,GAAG,OAAO,CAGrH;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAIpF;AAED,+FAA+F;AAC/F,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,iBAAiB,GAAG,qBAAqB,GAAG,oBAAoB,CAAC,GAAG,OAAO,CAE9I;AAED,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,OAAO,EAAE,MAAM,CAAA;CAClB;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,WAAW,EAAE,CA0BzH;AAID,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,yBAAyB,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CA0B/G;AAED,wFAAwF;AACxF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEhG;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,EAAE,GAAG,EAAE,yBAAyB,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAEpI;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,GAAG,SAAS,CAK3F;AAUD,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,EAAE,IAAI,EAAE,SAAS,YAAY,EAAE,GAAG,WAAW,EAAE,CAUxH;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,EAAE,KAAK,GAAE,IAAiB,GAAG,MAAM,CAM/F"}
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;AA4C1G,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,+BAysBrB"}
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 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;
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?.find((o) => String(o.value) === String(v));
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?.find((o) => String(o.value) === String(v));
523
- return { label: tl(f.label), value: v == null || v === '' ? '' : opt ? friendlyOptionLabel(tl(String(opt.label ?? v))) : String(v) };
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,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAI3F;AAED,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"}
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
- if (i < 0)
40
- return { to: url };
41
- return { to: url.slice(0, i), search: Object.fromEntries(new URLSearchParams(url.slice(i + 1))) };
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) => encodeURIComponent(String(row[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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "49.2.0",
3
+ "version": "49.2.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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('separa la query como search', () => {
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/invoices',
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('sin query deja el destino igual', () => {
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, undefined, fieldErrors)}
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?.find((o) => String(o.value) === String(v))
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?.find((o) => String(o.value) === String(v))
773
- return { label: tl(f.label), value: v == null || v === '' ? '' : opt ? friendlyOptionLabel(tl(String(opt.label ?? v))) : String(v) }
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(url: string): { to: string; search?: Record<string, string> } {
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
- if (i < 0) return { to: url }
50
- return { to: url.slice(0, i), search: Object.fromEntries(new URLSearchParams(url.slice(i + 1))) }
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) => encodeURIComponent(String(row[field] ?? '')))
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
  }