contentoh-components-library 21.6.32 → 21.6.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.claude/launch.json +15 -0
  2. package/dist/ai/prompts/attribute.prompt.js +28 -0
  3. package/dist/ai/prompts/description.prompt.js +33 -0
  4. package/dist/ai/schemas/attribute.schema.js +35 -0
  5. package/dist/ai/schemas/description.schema.js +27 -0
  6. package/dist/components/atoms/InheritanceTag/InheritanceTag.stories.js +5 -5
  7. package/dist/components/atoms/InheritanceTag/index.js +6 -6
  8. package/dist/components/organisms/InputGroup/index.js +12 -1
  9. package/dist/components/organisms/LinkVariantsModal/index.js +269 -356
  10. package/dist/components/organisms/LinkVariantsModal/styles.js +14 -5
  11. package/dist/components/organisms/PropagateChangesModal/index.js +12 -13
  12. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +5 -5
  13. package/dist/components/pages/ProviderProductEdition/context/reducers/inputs.js +5 -2
  14. package/dist/components/pages/ProviderProductEdition/index.js +96 -36
  15. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +5 -5
  16. package/dist/components/pages/RetailerProductEdition/context/reducers/inputs.js +5 -2
  17. package/dist/components/pages/RetailerProductEdition/index.js +100 -38
  18. package/package.json +1 -1
  19. package/src/components/atoms/InheritanceTag/InheritanceTag.stories.js +3 -3
  20. package/src/components/atoms/InheritanceTag/index.js +6 -6
  21. package/src/components/organisms/InputGroup/index.js +12 -0
  22. package/src/components/organisms/LinkVariantsModal/index.js +260 -287
  23. package/src/components/organisms/LinkVariantsModal/styles.js +65 -32
  24. package/src/components/organisms/PropagateChangesModal/index.js +19 -17
  25. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +4 -4
  26. package/src/components/pages/ProviderProductEdition/context/reducers/inputs.js +13 -1
  27. package/src/components/pages/ProviderProductEdition/index.js +61 -20
  28. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +4 -4
  29. package/src/components/pages/RetailerProductEdition/context/reducers/inputs.js +13 -1
  30. package/src/components/pages/RetailerProductEdition/index.js +74 -23
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import axios from "axios";
3
3
 
4
4
  import { readPayload, ApiError } from "./api";
5
- import { SingleSelect } from "../../molecules/SingleSelect";
6
5
  import { getProductImage, getRetailerPicture } from "../../../_utils/helper";
7
6
 
8
7
  import {
@@ -14,19 +13,18 @@ import {
14
13
  SearchRow,
15
14
  List,
16
15
  AttributeList,
17
- Section,
16
+ Direction,
18
17
  Block,
19
18
  Choices,
20
19
  Tabs,
21
20
  Paging,
22
21
  ImageGrid,
23
- AddRetailer,
24
22
  Message,
25
23
  Footer,
26
24
  } from "./styles";
27
25
 
28
26
  /*
29
- Modal para ligar productos como padre/variante.
27
+ Modal para ligar productos como padre/hijo.
30
28
 
31
29
  props (* requerido):
32
30
  - *articleId: producto desde el que se liga (el que está en edición)
@@ -36,21 +34,31 @@ import {
36
34
  - *onClose: cerrar sin cambios
37
35
  - onLinked: se llama con la respuesta del backend cuando la liga tuvo éxito
38
36
 
39
- Los dos modos son los de POST /variants/link:
40
- parent → este producto pasa a ser variante del que se elija
41
- children → los elegidos pasan a ser variantes de este producto
37
+ Los dos modos son los de POST /variants/link, y NO son simétricos:
42
38
 
43
- En ambos el padre baja al hijo, nunca al revés. Lo que cambia es quién es quién.
39
+ parent → este producto pasa a ser hijo del que se elija.
40
+ No baja nada del padre. La única pregunta es si los atributos de
41
+ este producto se planchan sobre el padre (`pushChildToParent`) o
42
+ si solo se crea el vínculo.
43
+
44
+ children → los elegidos pasan a ser hijos de este producto.
45
+ Aquí sí baja del padre al hijo, con la selección de `inherit`.
46
+
47
+ La asimetría no es un descuido: al modo `parent` se llega desde un producto ya
48
+ capturado al que se le crea un padre para encabezar la familia. Bajarle encima
49
+ lo del padre borraría justo el trabajo desde el que se está ligando.
44
50
  */
45
51
 
46
52
  // El backend devuelve códigos estables justamente para no mandar textos de UI
47
53
  // desde el servidor. La traducción a lenguaje humano vive aquí.
48
54
  const ERROR_TEXT = {
49
- VARIANT_SELF_LINK: "Un producto no puede ser variante de sí mismo.",
55
+ VARIANT_SELF_LINK: "Un producto no puede ser hijo de sí mismo.",
56
+ // Se dice el hecho concreto y no la regla que hay detrás: "las familias son de
57
+ // un solo nivel" obliga al usuario a entender el modelo para leer el error.
50
58
  VARIANT_PARENT_IS_CHILD:
51
- "Ese producto ya es variante de otro. Las familias son de un solo nivel.",
59
+ "Ese producto ya tiene un padre. Desvincúlalo primero.",
52
60
  VARIANT_CHILD_IS_PARENT:
53
- "Ese producto ya tiene variantes propias, así que no puede colgar de otro.",
61
+ "Ese producto ya tiene hijos, así que no se le puede asignar un padre.",
54
62
  VARIANT_ALREADY_LINKED:
55
63
  "Ese producto ya pertenece a otra familia. Desvincúlalo primero.",
56
64
  VARIANT_NOT_OWNED_BY_USER: "Ese producto está fuera de tu alcance.",
@@ -76,7 +84,6 @@ export const LinkVariantsModal = ({
76
84
  activeRetailerId,
77
85
  token,
78
86
  endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
79
- filtersEndpoint = process.env.REACT_APP_CREATE_PRODUCTS_FILTERS,
80
87
  onClose,
81
88
  onLinked,
82
89
  }) => {
@@ -94,7 +101,7 @@ export const LinkVariantsModal = ({
94
101
 
95
102
  // Lo que el padre ofrece en cada dimensión, y lo que el usuario eligió de
96
103
  // cada una. Se guardan por separado porque se eligen por separado: heredar
97
- // todos los atributos no dice nada sobre qué fotos quiere la variante.
104
+ // todos los atributos no dice nada sobre qué fotos quiere el hijo.
98
105
  const [offered, setOffered] = useState({
99
106
  attributes: [],
100
107
  descriptions: [],
@@ -131,11 +138,19 @@ export const LinkVariantsModal = ({
131
138
  // Desmarcado a propósito: aplicar la misma herencia a varios productos es una
132
139
  // decisión, no un valor por omisión. Marcado de entrada se ligaban todos igual
133
140
  // sin que nadie lo hubiera elegido.
141
+ /*
142
+ Modo `parent`: la unica decision de la pantalla.
143
+
144
+ null = sin decidir, true = planchar los atributos del hijo sobre el padre,
145
+ false = solo vincular sin mover nada. No hay una tercera opcion: bajar del
146
+ padre al hijo es la otra direccion y vive en el modo children.
147
+ */
148
+ const [pushToParent, setPushToParent] = useState(null);
149
+
134
150
  const [applyToAll, setApplyToAll] = useState(false);
135
151
  const [activeChild, setActiveChild] = useState(null);
136
152
  const [perChild, setPerChild] = useState({});
137
153
 
138
- // Cadenas que el padre NO tiene y el usuario decidió agregar a la variante.
139
154
  /*
140
155
  Imágenes que la base lista pero cuyo archivo no está en S3.
141
156
 
@@ -149,21 +164,6 @@ export const LinkVariantsModal = ({
149
164
  */
150
165
  const [brokenImages, setBrokenImages] = useState([]);
151
166
 
152
- const [extraRetailers, setExtraRetailers] = useState([]);
153
- const [newRetailer, setNewRetailer] = useState("");
154
- const [newDepartment, setNewDepartment] = useState("");
155
- const [newCategory, setNewCategory] = useState("");
156
-
157
- // Las categorías no son una lista plana: dependen de la cadena y del
158
- // departamento. Es la misma cascada que usa el alta de productos, y se sirve
159
- // del mismo endpoint para no acabar con dos árboles de categorías distintos.
160
- const [filters, setFilters] = useState({
161
- retailers: [],
162
- departments: [],
163
- categories: [],
164
- });
165
- const [loadingFilters, setLoadingFilters] = useState(false);
166
-
167
167
  const [loading, setLoading] = useState(false);
168
168
  const [error, setError] = useState(null);
169
169
 
@@ -195,6 +195,7 @@ export const LinkVariantsModal = ({
195
195
  setTotal(0);
196
196
  setHasMore(false);
197
197
  setError(null);
198
+ setPushToParent(null);
198
199
  setStep("search");
199
200
  }, [mode]);
200
201
 
@@ -258,6 +259,17 @@ export const LinkVariantsModal = ({
258
259
  };
259
260
 
260
261
  const goToInheritance = async () => {
262
+ /*
263
+ En modo `parent` no hay nada que elegir del padre —no baja nada—, asi que
264
+ no se pide su catalogo de atributos: seria una llamada cuyo resultado no
265
+ se pinta. La unica pregunta es si se plancha lo del hijo hacia arriba.
266
+ */
267
+ if (mode === "parent") {
268
+ setPushToParent(null);
269
+ setStep("inherit");
270
+ return;
271
+ }
272
+
261
273
  setLoading(true);
262
274
  setError(null);
263
275
  try {
@@ -290,7 +302,6 @@ export const LinkVariantsModal = ({
290
302
  attributes: null,
291
303
  images: null,
292
304
  });
293
- setExtraRetailers([]);
294
305
  setBrokenImages([]);
295
306
  setPerChild({});
296
307
  setApplyToAll(false);
@@ -338,126 +349,6 @@ export const LinkVariantsModal = ({
338
349
  }));
339
350
  };
340
351
 
341
- const loadFilters = useCallback(
342
- async (retailer, department) => {
343
- setLoadingFilters(true);
344
- try {
345
- const res = await axios.get(
346
- `${filtersEndpoint}?retailerSelected=${
347
- retailer ?? ""
348
- }&departmentSelected=${department ?? ""}`,
349
- { headers: { Authorization: token } }
350
- );
351
-
352
- // readPayload YA devuelve el cuerpo parseado. Volver a buscar `body`
353
- // dentro dejaba la lista siempre vacía, y por eso el selector de cadenas
354
- // no mostraba nada.
355
- const lista = readPayload(res)?.data ?? [];
356
-
357
- setFilters((current) => {
358
- if (!retailer) {
359
- return {
360
- ...current,
361
- retailers: lista.map((item) => ({
362
- value: item.id,
363
- label: item.name,
364
- })),
365
- };
366
- }
367
- if (!department) {
368
- return {
369
- ...current,
370
- departments: lista.map((item) => ({
371
- value: item.category,
372
- label: item.category,
373
- })),
374
- categories: [],
375
- };
376
- }
377
- // El tercer nivel no viene como {id, name} sino como
378
- // {category_id, subcategory}: con el mapeo de los otros dos, la
379
- // categoría salía en la lista pero sin texto. Mismo criterio que usa
380
- // el alta de productos, incluido el separador legible.
381
- return {
382
- ...current,
383
- categories: lista.map((item) => ({
384
- value: item.category_id,
385
- label: (item.subcategory ?? "").replace("|", " > "),
386
- })),
387
- };
388
- });
389
- } catch (error) {
390
- console.error("linkVariants/filters: ", error);
391
- } finally {
392
- setLoadingFilters(false);
393
- }
394
- },
395
- [filtersEndpoint, token]
396
- );
397
-
398
- // Las cadenas del catálogo se piden al entrar al paso de herencia, no al abrir
399
- // el modal: quien solo busca y cancela no llega a necesitarlas.
400
- useEffect(() => {
401
- if (step === "inherit" && !filters.retailers.length) {
402
- loadFilters(null, null);
403
- }
404
- }, [step, filters.retailers.length, loadFilters]);
405
-
406
- /*
407
- Quita una cadena agregada a mano. Solo esas: las del padre se desmarcan con
408
- su casilla, pero siguen existiendo y volverán a aparecer.
409
- */
410
- const removeExtraRetailer = (clave) => {
411
- setExtraRetailers((current) =>
412
- current.filter((item) => item.clave !== clave)
413
- );
414
- setChosen((current) => ({
415
- ...current,
416
- categoryRetailers: current.categoryRetailers.filter(
417
- (item) => item !== clave
418
- ),
419
- }));
420
- };
421
-
422
- const addRetailer = () => {
423
- const retailerId = Number(newRetailer);
424
- const categoryId = Number(newCategory);
425
- if (!retailerId || !categoryId) return;
426
-
427
- const clave = `${retailerId}-${categoryId}`;
428
- const yaEsta =
429
- offered.categoryRetailers.some(
430
- (item) => `${item.retailerId}-${item.categoryId}` === clave
431
- ) || extraRetailers.some((item) => item.clave === clave);
432
-
433
- if (yaEsta) return;
434
-
435
- const retailer = filters.retailers.find(
436
- (item) => Number(item.value) === retailerId
437
- );
438
- const category = filters.categories.find(
439
- (item) => Number(item.value) === categoryId
440
- );
441
-
442
- setExtraRetailers((current) => [
443
- ...current,
444
- {
445
- clave,
446
- retailerId,
447
- categoryId,
448
- retailerName: retailer?.label ?? `Cadena ${retailerId}`,
449
- categoryName: category?.label ?? `Categoría ${categoryId}`,
450
- },
451
- ]);
452
- setChosen((current) => ({
453
- ...current,
454
- categoryRetailers: [...current.categoryRetailers, clave],
455
- }));
456
- setNewRetailer("");
457
- setNewDepartment("");
458
- setNewCategory("");
459
- };
460
-
461
352
  /*
462
353
  Convierte una selección de pantalla en el bloque `inherit` de la API.
463
354
 
@@ -465,8 +356,9 @@ export const LinkVariantsModal = ({
465
356
  entre la carga del modal y el guardado alguien agregó un valor al padre,
466
357
  "todos" sigue significando todos. La lista sería una foto ya vieja.
467
358
 
468
- Las parejas cadena+categoría viajan como objetos, no como ids, porque pueden
469
- ser cadenas que el padre no tiene: ahí no hay nada que heredar, se asigna.
359
+ Las parejas cadena+categoría viajan como objetos y no como ids porque el
360
+ backend las da de alta en el hijo por (cadena, categoría); todas salen de las
361
+ que ya tiene el padre, que son las únicas que se pueden heredar.
470
362
  */
471
363
  // Las que de verdad se pueden heredar: la base puede listar filas cuyo archivo
472
364
  // ya no existe, y esas no cuentan ni para la lista ni para el "todas".
@@ -475,14 +367,11 @@ export const LinkVariantsModal = ({
475
367
  );
476
368
 
477
369
  const buildInherit = (seleccion) => {
478
- const parejas = [
479
- ...offered.categoryRetailers.map((item) => ({
480
- clave: `${item.retailerId}-${item.categoryId}`,
481
- retailerId: item.retailerId,
482
- categoryId: item.categoryId,
483
- })),
484
- ...(seleccion.extraRetailers ?? []),
485
- ];
370
+ const parejas = offered.categoryRetailers.map((item) => ({
371
+ clave: `${item.retailerId}-${item.categoryId}`,
372
+ retailerId: item.retailerId,
373
+ categoryId: item.categoryId,
374
+ }));
486
375
 
487
376
  return {
488
377
  attributes:
@@ -514,7 +403,6 @@ export const LinkVariantsModal = ({
514
403
  images: [],
515
404
  categoryRetailers: [],
516
405
  },
517
- extraRetailers: [],
518
406
  });
519
407
 
520
408
  /*
@@ -524,13 +412,12 @@ export const LinkVariantsModal = ({
524
412
  const switchChild = (id) => {
525
413
  setPerChild((current) => ({
526
414
  ...current,
527
- [activeChild]: { modes, chosen, extraRetailers },
415
+ [activeChild]: { modes, chosen },
528
416
  }));
529
417
 
530
418
  const guardado = perChild[id] ?? seleccionVacia();
531
419
  setModes(guardado.modes);
532
420
  setChosen(guardado.chosen);
533
- setExtraRetailers(guardado.extraRetailers ?? []);
534
421
  setActiveChild(id);
535
422
  };
536
423
 
@@ -538,7 +425,7 @@ export const LinkVariantsModal = ({
538
425
  setLoading(true);
539
426
  setError(null);
540
427
 
541
- const inherit = buildInherit({ modes, chosen, extraRetailers });
428
+ const inherit = buildInherit({ modes, chosen });
542
429
 
543
430
  // Con las pestañas abiertas cada producto manda la suya. La selección de la
544
431
  // pestaña a la vista todavía no está en perChild, así que se agrega aquí: si
@@ -547,9 +434,7 @@ export const LinkVariantsModal = ({
547
434
  !applyToAll && mode === "children"
548
435
  ? selected.reduce((acc, id) => {
549
436
  const guardado =
550
- id === activeChild
551
- ? { modes, chosen, extraRetailers }
552
- : perChild[id];
437
+ id === activeChild ? { modes, chosen } : perChild[id];
553
438
  if (guardado) acc[id] = buildInherit(guardado);
554
439
  return acc;
555
440
  }, {})
@@ -557,7 +442,12 @@ export const LinkVariantsModal = ({
557
442
 
558
443
  const body =
559
444
  mode === "parent"
560
- ? { mode, articleId, parentArticleId: selected[0], inherit }
445
+ ? {
446
+ mode,
447
+ articleId,
448
+ parentArticleId: selected[0],
449
+ pushChildToParent: pushToParent === true,
450
+ }
561
451
  : {
562
452
  mode,
563
453
  parentArticleId: articleId,
@@ -569,7 +459,7 @@ export const LinkVariantsModal = ({
569
459
  try {
570
460
  const data = await request("link", body);
571
461
  // El modo importa para quien escucha: en `children` este producto GANA
572
- // variantes; en `parent` pasa a SER una. Son efectos opuestos sobre la
462
+ // hijos; en `parent` pasa a SER uno. Son efectos opuestos sobre la
573
463
  // misma ficha, así que quien refresque el estado necesita distinguirlos.
574
464
  onLinked?.({
575
465
  ...data,
@@ -589,9 +479,7 @@ export const LinkVariantsModal = ({
589
479
  const decidida = (dimension) => {
590
480
  if (modes[dimension] !== null) return true;
591
481
  if (dimension === "categoryRetailers") {
592
- return (
593
- offered.categoryRetailers.length === 0 && extraRetailers.length === 0
594
- );
482
+ return offered.categoryRetailers.length === 0;
595
483
  }
596
484
  // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
597
485
  return dimension === "images"
@@ -610,17 +498,13 @@ export const LinkVariantsModal = ({
610
498
  // en todos, así que todos tienen que estar decididos. Si no, un producto se
611
499
  // ligaría sin heredar nada solo porque nadie abrió su pestaña.
612
500
  const decididoPara = (id) => {
613
- const guardado =
614
- id === activeChild ? { modes, chosen, extraRetailers } : perChild[id];
501
+ const guardado = id === activeChild ? { modes, chosen } : perChild[id];
615
502
  if (!guardado) return false;
616
503
 
617
504
  return DIMENSIONES.every((dimension) => {
618
505
  if (guardado.modes[dimension] !== null) return true;
619
506
  if (dimension === "categoryRetailers") {
620
- return (
621
- offered.categoryRetailers.length === 0 &&
622
- (guardado.extraRetailers ?? []).length === 0
623
- );
507
+ return offered.categoryRetailers.length === 0;
624
508
  }
625
509
  return dimension === "images"
626
510
  ? imagenesUtiles.length === 0
@@ -630,7 +514,9 @@ export const LinkVariantsModal = ({
630
514
 
631
515
  const todoDecidido =
632
516
  step === "inherit" &&
633
- (applyToAll || mode !== "children"
517
+ (mode === "parent"
518
+ ? pushToParent !== null
519
+ : applyToAll
634
520
  ? DIMENSIONES.every(decidida)
635
521
  : selected.every(decididoPara));
636
522
 
@@ -646,7 +532,41 @@ export const LinkVariantsModal = ({
646
532
  const selectionLabel =
647
533
  mode === "parent"
648
534
  ? "Se elige un solo producto padre."
649
- : `${selected.length} producto(s) pasarán a ser variantes.`;
535
+ : `${selected.length} producto(s) pasarán a ser hijos de este.`;
536
+
537
+ /*
538
+ Quién es el padre y quién el hijo.
539
+
540
+ Es la pregunta que el modal no contestaba: la dirección la fija el modo, que
541
+ se eligió en la pantalla anterior y ya no está a la vista, así que al llegar
542
+ aquí no había forma de saber si lo que se marca baja o sube. En modo `parent`
543
+ el producto abierto es el que RECIBE, que es justo lo contrario de lo que
544
+ sugiere tenerlo en el título del modal.
545
+ */
546
+ const nombreDe = (id) =>
547
+ candidates.find((item) => item.articleId === id)?.name || `Producto ${id}`;
548
+
549
+ const esteEsPadre = mode === "children";
550
+
551
+ // Cuántos hijos tiene ya el padre elegido. Es informativo: sale del mismo
552
+ // candidato que se pintó en la búsqueda, sin una llamada extra.
553
+ const hijosDelPadre =
554
+ mode === "parent" && selected.length
555
+ ? candidates.find((item) => item.articleId === selected[0])
556
+ ?.variantsCount ?? 0
557
+ : 0;
558
+
559
+ const nombreEsteProducto = articleName || `Producto ${articleId}`;
560
+ const nombrePadre = esteEsPadre
561
+ ? nombreEsteProducto
562
+ : selected.length
563
+ ? nombreDe(selected[0])
564
+ : "—";
565
+ const nombreHijo = !esteEsPadre
566
+ ? nombreEsteProducto
567
+ : selected.length === 1
568
+ ? nombreDe(selected[0])
569
+ : `${selected.length} productos elegidos`;
650
570
 
651
571
  return (
652
572
  <Overlay onClick={onClose}>
@@ -669,6 +589,10 @@ export const LinkVariantsModal = ({
669
589
  </button>
670
590
  </Header>
671
591
 
592
+ {/* Las dos etiquetas dicen qué se va a relacionar, no qué pasa a ser
593
+ este producto. "Este producto es hijo de otro" obligaba al usuario a
594
+ deducir el modelo de la familia para entender el botón; qué es un
595
+ nivel y qué no es asunto nuestro, no suyo. */}
672
596
  {step === "search" && (
673
597
  <ModeSwitch>
674
598
  <button
@@ -676,14 +600,14 @@ export const LinkVariantsModal = ({
676
600
  className={mode === "parent" ? "active" : ""}
677
601
  onClick={() => setMode("parent")}
678
602
  >
679
- Este producto es variante de otro
603
+ Relacionar padre a este producto
680
604
  </button>
681
605
  <button
682
606
  type="button"
683
607
  className={mode === "children" ? "active" : ""}
684
608
  onClick={() => setMode("children")}
685
609
  >
686
- Agregar variantes a este producto
610
+ Relacionar hijos a este producto
687
611
  </button>
688
612
  </ModeSwitch>
689
613
  )}
@@ -743,7 +667,7 @@ export const LinkVariantsModal = ({
743
667
  </span>
744
668
  {candidate.variantsCount > 0 && (
745
669
  <span className="count">
746
- {candidate.variantsCount} variante
670
+ {candidate.variantsCount} hijo
747
671
  {candidate.variantsCount === 1 ? "" : "s"}
748
672
  </span>
749
673
  )}
@@ -774,8 +698,119 @@ export const LinkVariantsModal = ({
774
698
  </>
775
699
  )}
776
700
 
777
- {step === "inherit" && (
701
+ {step === "inherit" && mode === "parent" && (
702
+ <>
703
+ <Direction>
704
+ <div className="lado">
705
+ <span className="rol">Producto hijo</span>
706
+ <strong title={nombreEsteProducto}>
707
+ {nombreEsteProducto}
708
+ </strong>
709
+ <span className="actual">el que tienes abierto</span>
710
+ </div>
711
+ <span className="flecha" aria-hidden="true">
712
+ →
713
+ </span>
714
+ <div className="lado">
715
+ <span className="rol">Producto padre</span>
716
+ <strong title={nombrePadre}>{nombrePadre}</strong>
717
+ </div>
718
+ </Direction>
719
+
720
+ <Message>
721
+ Aquí <strong>no baja nada del padre</strong>: «
722
+ {nombreEsteProducto}» conserva todo lo que ya tiene capturado. Lo
723
+ único que se decide es si esos valores{" "}
724
+ <strong>suben al padre</strong>.
725
+ </Message>
726
+
727
+ {/* Informativo, no bloqueante: si el padre ya encabeza una
728
+ familia, planchar cambia de dónde heredarán sus hijos de aquí
729
+ en adelante. Los valores que ya tienen NO cambian —nadie
730
+ propaga solo— pero el usuario merece saberlo antes. */}
731
+ {hijosDelPadre > 0 && (
732
+ <Message className="aviso">
733
+ «{nombrePadre}» ya tiene <strong>{hijosDelPadre}</strong>{" "}
734
+ {hijosDelPadre === 1 ? "hijo" : "hijos"}. Si planchas, cambias
735
+ los valores desde los que ellos heredan. Lo que ya tengan
736
+ capturado no se toca.
737
+ </Message>
738
+ )}
739
+
740
+ <Block>
741
+ <h3>
742
+ <span className="paso">1</span> ¿Qué hacemos con los atributos?
743
+ </h3>
744
+ <Choices>
745
+ <label>
746
+ <input
747
+ type="radio"
748
+ name="push-to-parent"
749
+ checked={pushToParent === true}
750
+ onChange={() => setPushToParent(true)}
751
+ />
752
+ <span>
753
+ Planchar los de este producto sobre el padre
754
+ <span className="hint">
755
+ El padre se queda con estos valores y pasan a contar como
756
+ heredados. El UPC y el SKU nunca suben.
757
+ </span>
758
+ </span>
759
+ </label>
760
+ <label>
761
+ <input
762
+ type="radio"
763
+ name="push-to-parent"
764
+ checked={pushToParent === false}
765
+ onChange={() => setPushToParent(false)}
766
+ />
767
+ <span>
768
+ Solo vincular, sin mover nada
769
+ <span className="hint">
770
+ Se crea el parentesco y ya. Ni el padre ni este producto
771
+ cambian de valor.
772
+ </span>
773
+ </span>
774
+ </label>
775
+ </Choices>
776
+ </Block>
777
+ </>
778
+ )}
779
+
780
+ {step === "inherit" && mode === "children" && (
778
781
  <>
782
+ <Direction>
783
+ <div className="lado">
784
+ <span className="rol">Producto padre</span>
785
+ <strong title={nombrePadre}>{nombrePadre}</strong>
786
+ {esteEsPadre && (
787
+ <span className="actual">el que tienes abierto</span>
788
+ )}
789
+ </div>
790
+ <span className="flecha" aria-hidden="true">
791
+ →
792
+ </span>
793
+ <div className="lado">
794
+ <span className="rol">
795
+ {esteEsPadre && selected.length > 1
796
+ ? `${selected.length} productos hijo`
797
+ : "Producto hijo"}
798
+ </span>
799
+ <strong title={nombreHijo}>{nombreHijo}</strong>
800
+ {!esteEsPadre && (
801
+ <span className="actual">el que tienes abierto</span>
802
+ )}
803
+ </div>
804
+ </Direction>
805
+
806
+ <Message>
807
+ Todo lo que marques abajo <strong>baja del padre al hijo</strong>
808
+ . Nada sube en el otro sentido:{" "}
809
+ {esteEsPadre
810
+ ? "este producto no cambia."
811
+ : `«${nombrePadre}» no cambia.`}
812
+ </Message>
813
+
779
814
  {mode === "children" && selected.length > 1 && (
780
815
  <>
781
816
  <label className="apply-all">
@@ -823,8 +858,8 @@ export const LinkVariantsModal = ({
823
858
  </>
824
859
  )}
825
860
 
826
- {/* El orden no es casual: primero se decide en qué cadenas vive la
827
- variante, porque de eso depende qué atributos le aplican. Las
861
+ {/* El orden no es casual: primero se decide en qué cadenas vive el
862
+ hijo, porque de eso depende qué atributos le aplican. Las
828
863
  imágenes van al final: son la decisión más visual y la que menos
829
864
  arrastra a las demás. */}
830
865
 
@@ -867,111 +902,47 @@ export const LinkVariantsModal = ({
867
902
  <span>
868
903
  Elegir cuáles
869
904
  <span className="hint">
870
- También puedes agregar una cadena que el padre no tenga.
905
+ Solo se puede heredar de las que ya tiene el padre.
871
906
  </span>
872
907
  </span>
873
908
  </label>
874
909
  </Choices>
875
910
 
876
911
  <List>
877
- {[
878
- ...offered.categoryRetailers.map((item) => ({
912
+ {offered.categoryRetailers
913
+ .map((item) => ({
879
914
  ...item,
880
915
  clave: `${item.retailerId}-${item.categoryId}`,
881
- delPadre: true,
882
- })),
883
- ...extraRetailers.map((item) => ({
884
- ...item,
885
- delPadre: false,
886
- })),
887
- ].map((item) => (
888
- <li key={item.clave}>
889
- <label>
890
- <input
891
- type="checkbox"
892
- disabled={modes.categoryRetailers === "all"}
893
- checked={chosen.categoryRetailers.includes(
894
- item.clave
895
- )}
896
- onChange={() =>
897
- toggleChoice("categoryRetailers", item.clave)
898
- }
899
- />
900
- <img
901
- className="logo"
902
- src={getRetailerPicture(item.retailerId, 24, 24)}
903
- alt=""
904
- />
905
- <span>
906
- {item.retailerName}
907
- <span className="meta">
908
- {item.categoryName || "Sin categoría"}
909
- {!item.delPadre && " · no la tiene el padre"}
916
+ }))
917
+ .map((item) => (
918
+ <li key={item.clave}>
919
+ <label>
920
+ <input
921
+ type="checkbox"
922
+ disabled={modes.categoryRetailers === "all"}
923
+ checked={chosen.categoryRetailers.includes(
924
+ item.clave
925
+ )}
926
+ onChange={() =>
927
+ toggleChoice("categoryRetailers", item.clave)
928
+ }
929
+ />
930
+ <img
931
+ className="logo"
932
+ src={getRetailerPicture(item.retailerId, 24, 24)}
933
+ alt=""
934
+ />
935
+ <span>
936
+ {item.retailerName}
937
+ <span className="meta">
938
+ {item.categoryName || "Sin categoría"}
939
+ </span>
910
940
  </span>
911
- </span>
912
- </label>
913
-
914
- {/* Solo las agregadas a mano se pueden quitar. Las del
915
- padre se desmarcan con su casilla, pero siguen ahí. */}
916
- {!item.delPadre && (
917
- <button
918
- type="button"
919
- className="quitar"
920
- title="Quitar esta cadena"
921
- aria-label={`Quitar ${item.retailerName}`}
922
- onClick={() => removeExtraRetailer(item.clave)}
923
- >
924
- ×
925
- </button>
926
- )}
927
- </li>
928
- ))}
941
+ </label>
942
+ </li>
943
+ ))}
929
944
  </List>
930
945
 
931
- <AddRetailer>
932
- <SingleSelect
933
- id="link-variants-retailer"
934
- placeholder="Agregar otra cadena…"
935
- options={filters.retailers}
936
- value={newRetailer}
937
- loading={loadingFilters && !filters.retailers.length}
938
- onChange={(valor) => {
939
- setNewRetailer(valor ?? "");
940
- setNewDepartment("");
941
- setNewCategory("");
942
- if (valor) loadFilters(valor, null);
943
- }}
944
- />
945
- <SingleSelect
946
- id="link-variants-department"
947
- placeholder="Departamento…"
948
- options={filters.departments}
949
- value={newDepartment}
950
- disabled={!newRetailer}
951
- loading={loadingFilters && !!newRetailer}
952
- onChange={(valor) => {
953
- setNewDepartment(valor ?? "");
954
- setNewCategory("");
955
- if (valor) loadFilters(newRetailer, valor);
956
- }}
957
- />
958
- <SingleSelect
959
- id="link-variants-category"
960
- placeholder="Categoría…"
961
- options={filters.categories}
962
- value={newCategory}
963
- disabled={!newDepartment}
964
- loading={loadingFilters && !!newDepartment}
965
- onChange={(valor) => setNewCategory(valor ?? "")}
966
- />
967
- <button
968
- type="button"
969
- onClick={addRetailer}
970
- disabled={!newRetailer || !newCategory || loadingFilters}
971
- >
972
- Agregar
973
- </button>
974
- </AddRetailer>
975
946
  </Block>
976
947
 
977
948
  <Block>
@@ -1014,8 +985,8 @@ export const LinkVariantsModal = ({
1014
985
  <span>
1015
986
  Elegir cuáles
1016
987
  <span className="hint">
1017
- Lo que no elijas conserva el valor que ya tiene la
1018
- variante.
988
+ Lo que no elijas conserva el valor que ya tiene el
989
+ hijo.
1019
990
  </span>
1020
991
  </span>
1021
992
  </label>
@@ -1095,8 +1066,8 @@ export const LinkVariantsModal = ({
1095
1066
  <span>
1096
1067
  Elegir cuáles
1097
1068
  <span className="hint">
1098
- Lo que no elijas conserva el valor que ya tiene la
1099
- variante.
1069
+ Lo que no elijas conserva el valor que ya tiene el
1070
+ hijo.
1100
1071
  </span>
1101
1072
  </span>
1102
1073
  </label>
@@ -1189,7 +1160,7 @@ export const LinkVariantsModal = ({
1189
1160
  <span>
1190
1161
  Elegir cuáles
1191
1162
  <span className="hint">
1192
- Útil cuando la variante cambia de color o de empaque.
1163
+ Útil cuando el hijo cambia de color o de empaque.
1193
1164
  </span>
1194
1165
  </span>
1195
1166
  </label>
@@ -1231,10 +1202,10 @@ export const LinkVariantsModal = ({
1231
1202
  )}
1232
1203
 
1233
1204
  {/* Es la diferencia entre link y propagate, y es la que sorprende:
1234
- aquí lo elegido se escribe encima aunque la variante lo hubiera
1205
+ aquí lo elegido se escribe encima aunque el hijo lo hubiera
1235
1206
  editado a mano. Decirlo antes evita el reclamo después. */}
1236
1207
  <Message>
1237
- Lo que elijas heredar reemplaza el valor actual de la variante,
1208
+ Lo que elijas heredar reemplaza el valor actual del hijo,
1238
1209
  incluso si alguien lo había capturado a mano.
1239
1210
  </Message>
1240
1211
  </>
@@ -1254,6 +1225,8 @@ export const LinkVariantsModal = ({
1254
1225
  ? `Faltan ${
1255
1226
  selected.filter((id) => !decididoPara(id)).length
1256
1227
  } producto(s) por revisar.`
1228
+ : mode === "parent"
1229
+ ? "Elige si los atributos suben al padre o no."
1257
1230
  : `Falta decidir: ${faltantes.join(", ")}.`}
1258
1231
  </span>
1259
1232
  <div className="actions">