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.
- package/.claude/launch.json +15 -0
- package/dist/ai/prompts/attribute.prompt.js +28 -0
- package/dist/ai/prompts/description.prompt.js +33 -0
- package/dist/ai/schemas/attribute.schema.js +35 -0
- package/dist/ai/schemas/description.schema.js +27 -0
- package/dist/components/atoms/InheritanceTag/InheritanceTag.stories.js +5 -5
- package/dist/components/atoms/InheritanceTag/index.js +6 -6
- package/dist/components/organisms/InputGroup/index.js +12 -1
- package/dist/components/organisms/LinkVariantsModal/index.js +269 -356
- package/dist/components/organisms/LinkVariantsModal/styles.js +14 -5
- package/dist/components/organisms/PropagateChangesModal/index.js +12 -13
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +5 -5
- package/dist/components/pages/ProviderProductEdition/context/reducers/inputs.js +5 -2
- package/dist/components/pages/ProviderProductEdition/index.js +96 -36
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +5 -5
- package/dist/components/pages/RetailerProductEdition/context/reducers/inputs.js +5 -2
- package/dist/components/pages/RetailerProductEdition/index.js +100 -38
- package/package.json +1 -1
- package/src/components/atoms/InheritanceTag/InheritanceTag.stories.js +3 -3
- package/src/components/atoms/InheritanceTag/index.js +6 -6
- package/src/components/organisms/InputGroup/index.js +12 -0
- package/src/components/organisms/LinkVariantsModal/index.js +260 -287
- package/src/components/organisms/LinkVariantsModal/styles.js +65 -32
- package/src/components/organisms/PropagateChangesModal/index.js +19 -17
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +4 -4
- package/src/components/pages/ProviderProductEdition/context/reducers/inputs.js +13 -1
- package/src/components/pages/ProviderProductEdition/index.js +61 -20
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +4 -4
- package/src/components/pages/RetailerProductEdition/context/reducers/inputs.js +13 -1
- 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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
|
59
|
+
"Ese producto ya tiene un padre. Desvincúlalo primero.",
|
|
52
60
|
VARIANT_CHILD_IS_PARENT:
|
|
53
|
-
"Ese producto ya tiene
|
|
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
|
|
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
|
|
469
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
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
|
|
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
|
-
? {
|
|
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
|
-
//
|
|
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
|
-
(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
827
|
-
|
|
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
|
-
|
|
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
|
-
|
|
912
|
+
{offered.categoryRetailers
|
|
913
|
+
.map((item) => ({
|
|
879
914
|
...item,
|
|
880
915
|
clave: `${item.retailerId}-${item.categoryId}`,
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
</
|
|
912
|
-
</
|
|
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
|
|
1018
|
-
|
|
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
|
|
1099
|
-
|
|
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
|
|
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
|
|
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
|
|
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">
|