contentoh-components-library 21.6.50 → 21.6.52
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/.env.test +0 -1
- package/dist/components/atoms/IconFile/index.js +2 -85
- package/dist/components/atoms/IconFile/styles.js +1 -1
- package/dist/components/atoms/ProductImage/ProductImage.stories.js +2 -70
- package/dist/components/atoms/ProductImage/index.js +27 -100
- package/dist/components/atoms/ProductImage/styles.js +1 -3
- package/dist/components/molecules/EditionActiveImage/index.js +2 -4
- package/dist/components/molecules/GalleryElement/index.js +4 -13
- package/dist/components/molecules/GalleryHeader/index.js +24 -96
- package/dist/components/molecules/GalleryHeader/styles.js +1 -4
- package/dist/components/molecules/ImageSelector/index.js +0 -1
- package/dist/components/organisms/ImagePreviewer/index.js +0 -1
- package/dist/components/organisms/ProductImageModal/index.js +2 -3
- package/dist/components/organisms/PropagateChangesModal/index.js +6 -1
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +10 -8
- package/dist/components/pages/ProviderProductEdition/index.js +189 -72
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +25 -338
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +2 -12
- package/dist/components/pages/RetailerProductEdition/index.js +579 -631
- package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +0 -1
- package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +145 -45
- package/dist/global-files/data.js +3 -16
- package/dist/index.js +90 -142
- package/package.json +1 -1
- package/src/components/atoms/IconFile/index.js +1 -28
- package/src/components/atoms/IconFile/styles.js +0 -8
- package/src/components/atoms/ProductImage/ProductImage.stories.js +0 -38
- package/src/components/atoms/ProductImage/index.js +20 -95
- package/src/components/atoms/ProductImage/styles.js +7 -92
- package/src/components/molecules/EditionActiveImage/index.js +2 -2
- package/src/components/molecules/GalleryElement/index.js +0 -9
- package/src/components/molecules/GalleryHeader/index.js +13 -65
- package/src/components/molecules/GalleryHeader/styles.js +1 -2
- package/src/components/molecules/ImageSelector/index.js +0 -1
- package/src/components/organisms/ImagePreviewer/index.js +0 -1
- package/src/components/organisms/ProductImageModal/index.js +0 -1
- package/src/components/organisms/PropagateChangesModal/index.js +10 -3
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +2 -1
- package/src/components/pages/ProviderProductEdition/index.js +111 -6
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +2 -202
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +0 -12
- package/src/components/pages/RetailerProductEdition/index.js +258 -363
- package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +0 -1
- package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +104 -51
- package/src/global-files/data.js +2 -17
- package/src/index.js +0 -4
- package/.claude/launch.json +0 -14
- package/dist/ai/prompts/attribute.prompt.js +0 -28
- package/dist/ai/prompts/description.prompt.js +0 -33
- package/dist/ai/schemas/attribute.schema.js +0 -35
- package/dist/ai/schemas/description.schema.js +0 -27
- package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +0 -118
- package/dist/components/atoms/AssetPreview/index.js +0 -150
- package/dist/components/atoms/AssetPreview/styles.js +0 -28
- package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +0 -147
- package/dist/components/molecules/GalleryAssetElement/index.js +0 -178
- package/dist/components/molecules/GalleryAssetElement/styles.js +0 -26
- package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +0 -56
- package/dist/components/molecules/GalleryTrayTabs/index.js +0 -60
- package/dist/components/molecules/GalleryTrayTabs/styles.js +0 -20
- package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +0 -171
- package/dist/components/organisms/DigitalAssetsTray/index.js +0 -211
- package/dist/components/organisms/DigitalAssetsTray/styles.js +0 -22
- package/dist/global-files/digital-assets/assetTypes.js +0 -480
- package/dist/global-files/digital-assets/assets.actions.js +0 -64
- package/dist/global-files/digital-assets/assets.adapter.js +0 -304
- package/dist/global-files/digital-assets/assets.mock.js +0 -136
- package/dist/global-files/digital-assets/assets.reducer.js +0 -220
- package/dist/global-files/digital-assets/uploadAssets.js +0 -179
- package/dist/global-files/digital-assets/useDigitalAssets.js +0 -522
- package/docs/activos-digitales.md +0 -273
- package/src/components/atoms/AssetPreview/AssetPreview.stories.js +0 -78
- package/src/components/atoms/AssetPreview/index.js +0 -119
- package/src/components/atoms/AssetPreview/styles.js +0 -190
- package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +0 -102
- package/src/components/molecules/GalleryAssetElement/index.js +0 -162
- package/src/components/molecules/GalleryAssetElement/styles.js +0 -159
- package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +0 -32
- package/src/components/molecules/GalleryTrayTabs/index.js +0 -41
- package/src/components/molecules/GalleryTrayTabs/styles.js +0 -52
- package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +0 -101
- package/src/components/organisms/DigitalAssetsTray/index.js +0 -189
- package/src/components/organisms/DigitalAssetsTray/styles.js +0 -183
- package/src/global-files/digital-assets/assetTypes.js +0 -419
- package/src/global-files/digital-assets/assets.actions.js +0 -50
- package/src/global-files/digital-assets/assets.adapter.js +0 -229
- package/src/global-files/digital-assets/assets.mock.js +0 -122
- package/src/global-files/digital-assets/assets.reducer.js +0 -197
- package/src/global-files/digital-assets/uploadAssets.js +0 -109
- package/src/global-files/digital-assets/useDigitalAssets.js +0 -329
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import axios from "axios";
|
|
4
4
|
import { useDropzone } from "react-dropzone";
|
|
@@ -30,8 +30,6 @@ import {
|
|
|
30
30
|
getServicesData,
|
|
31
31
|
} from "../../../global-files/data";
|
|
32
32
|
import { useCloseModal } from "../../../global-files/customHooks";
|
|
33
|
-
import { useDigitalAssets } from "../../../global-files/digital-assets/useDigitalAssets";
|
|
34
|
-
import { countUnsavedAssets } from "../../../global-files/digital-assets/assets.reducer";
|
|
35
33
|
|
|
36
34
|
import { Button } from "../../atoms/GeneralButton";
|
|
37
35
|
import { GenericModal } from "../../atoms/GenericModal";
|
|
@@ -41,14 +39,12 @@ import { ButtonV2 } from "../../atoms/ButtonV2";
|
|
|
41
39
|
|
|
42
40
|
import { GalleryElement } from "../../molecules/GalleryElement";
|
|
43
41
|
import { GalleryHeader } from "../../molecules/GalleryHeader";
|
|
44
|
-
import { GalleryTrayTabs, TRAYS } from "../../molecules/GalleryTrayTabs";
|
|
45
42
|
import { HeaderTop } from "../../molecules/HeaderTop";
|
|
46
43
|
import { TagAndInput } from "../../molecules/TagAndInput/index";
|
|
47
44
|
|
|
48
45
|
import { FullProductNameHeader } from "../../organisms/FullProductNameHeader";
|
|
49
46
|
import { FullTabsMenu } from "../../organisms/FullTabsMenu";
|
|
50
47
|
import { ImageDataTable } from "../../organisms/ImageDataTable";
|
|
51
|
-
import { DigitalAssetsTray } from "../../organisms/DigitalAssetsTray";
|
|
52
48
|
import { ImagePreviewer } from "../../organisms/ImagePreviewer";
|
|
53
49
|
import { InputGroup } from "../../organisms/InputGroup";
|
|
54
50
|
import { ProductImageModal } from "../../organisms/ProductImageModal";
|
|
@@ -105,19 +101,62 @@ function obtenerDuracionVideoBase64(base64Data) {
|
|
|
105
101
|
});
|
|
106
102
|
}
|
|
107
103
|
|
|
104
|
+
function convertirVideoABase64(url) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const xhr = new XMLHttpRequest();
|
|
107
|
+
xhr.open("GET", url, true);
|
|
108
|
+
xhr.responseType = "blob";
|
|
109
|
+
xhr.onload = function () {
|
|
110
|
+
if (xhr.status === 200) {
|
|
111
|
+
const blob = xhr.response;
|
|
112
|
+
const reader = new FileReader();
|
|
113
|
+
reader.onloadend = function () {
|
|
114
|
+
resolve(reader.result);
|
|
115
|
+
};
|
|
116
|
+
reader.readAsDataURL(blob);
|
|
117
|
+
} else {
|
|
118
|
+
reject(new Error("Error al cargar el video"));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
xhr.onerror = function () {
|
|
122
|
+
reject(new Error("Error de red al cargar el video"));
|
|
123
|
+
};
|
|
124
|
+
xhr.send();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Para videos ya guardados, `getImage` deja en `src` una URL del
|
|
129
|
+
// redimensionador de imágenes que no sirve para mp4. Descargamos el video
|
|
130
|
+
// desde S3 (srcDB) y generamos un thumbnail JPEG para mostrarlo en la galería.
|
|
131
|
+
async function regenerateVideoThumbnails(values) {
|
|
132
|
+
if (!Array.isArray(values)) return values;
|
|
133
|
+
return Promise.all(
|
|
134
|
+
values.map(async (value) => {
|
|
135
|
+
if (value?.ext !== "mp4" || !value?.srcDB) return value;
|
|
136
|
+
try {
|
|
137
|
+
const base64 = await convertirVideoABase64(
|
|
138
|
+
"https://" +
|
|
139
|
+
process.env.REACT_APP_IMAGES_BUCKET +
|
|
140
|
+
".s3.amazonaws.com/" +
|
|
141
|
+
value.srcDB,
|
|
142
|
+
);
|
|
143
|
+
const videoSrc = "data:video/mp4;base64," + base64.split(",")[1];
|
|
144
|
+
const thumbnail = await generateThumbnail(videoSrc, 2);
|
|
145
|
+
return { ...value, src: thumbnail };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Error al generar thumbnail del video:", error);
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
108
154
|
const RetailerProductEditionView = ({
|
|
109
155
|
tabsSections,
|
|
110
156
|
productSelected = {},
|
|
111
157
|
user = {},
|
|
112
158
|
token,
|
|
113
159
|
location,
|
|
114
|
-
/*
|
|
115
|
-
Activos digitales (360, video, gif, pdf). Apagado por omisión: la interfaz
|
|
116
|
-
ya está, pero el backend no existe todavía, así que la bandeja solo puede
|
|
117
|
-
mostrar lo que el usuario acaba de soltar y nada se guarda. La aplicación
|
|
118
|
-
que consuma la librería lo enciende cuando el backend esté listo.
|
|
119
|
-
*/
|
|
120
|
-
enableDigitalAssets = false,
|
|
121
160
|
}) => {
|
|
122
161
|
const {
|
|
123
162
|
state,
|
|
@@ -126,8 +165,6 @@ const RetailerProductEditionView = ({
|
|
|
126
165
|
saveDescriptions,
|
|
127
166
|
updateImages,
|
|
128
167
|
deleteImages,
|
|
129
|
-
saveAssets,
|
|
130
|
-
registerUploadedAssets,
|
|
131
168
|
handleOnDownloadImages,
|
|
132
169
|
createComment,
|
|
133
170
|
} = useProviderProductEdition();
|
|
@@ -161,6 +198,7 @@ const RetailerProductEditionView = ({
|
|
|
161
198
|
const [variantGroup, setVariantGroup] = useState(null);
|
|
162
199
|
const [showGroup, setShowGroup] = useState(false);
|
|
163
200
|
const [showGroupModal, setShowGroupModal] = useState(false);
|
|
201
|
+
|
|
164
202
|
// Qué vínculo se está por romper, o null si no hay diálogo abierto. Guarda al
|
|
165
203
|
// HIJO, que es quien lleva la relación y lo que pide la acción `unlink`.
|
|
166
204
|
const [porDesvincular, setPorDesvincular] = useState(null);
|
|
@@ -266,54 +304,6 @@ const RetailerProductEditionView = ({
|
|
|
266
304
|
|
|
267
305
|
const [openChangeStatusModal, setOpenChangeStatusModal] = useState(false);
|
|
268
306
|
|
|
269
|
-
/*
|
|
270
|
-
Bandeja de activos digitales. La lista vive en el reducer de la ficha
|
|
271
|
-
(`state.assets_values`), igual que las imágenes, para que el guardado del
|
|
272
|
-
contexto pueda leerla. El hook solo aporta validación y ciclo de vida.
|
|
273
|
-
|
|
274
|
-
Sigue vacía porque todavía no hay de dónde leerla: cuando exista el endpoint
|
|
275
|
-
se llama a `replaceAll` desde `loadData` y el resto de la pantalla no cambia.
|
|
276
|
-
*/
|
|
277
|
-
const [assetNotices, setAssetNotices] = useState([]);
|
|
278
|
-
const digitalAssets = useDigitalAssets({
|
|
279
|
-
assets: state.assets_values,
|
|
280
|
-
dispatch,
|
|
281
|
-
notices: assetNotices,
|
|
282
|
-
setNotices: setAssetNotices,
|
|
283
|
-
// Con los dos, la subida a S3 arranca sola al soltar los archivos. Sin
|
|
284
|
-
// ellos —en Storybook, por ejemplo— la bandeja funciona igual pero no sube.
|
|
285
|
-
articleId: state.product?.id_article ?? null,
|
|
286
|
-
token,
|
|
287
|
-
// Al terminar la subida el activo queda registrado solo, sin esperar al
|
|
288
|
-
// botón de guardar: con la barra llena, nadie supone que falta un paso.
|
|
289
|
-
onUploaded: (uploaded) => registerUploadedAssets(token, uploaded),
|
|
290
|
-
});
|
|
291
|
-
const [activeTray, setActiveTray] = useState(TRAYS.IMAGES);
|
|
292
|
-
const currentTray = enableDigitalAssets ? activeTray : TRAYS.IMAGES;
|
|
293
|
-
|
|
294
|
-
/*
|
|
295
|
-
De qué artículo se cargaron ya los activos.
|
|
296
|
-
|
|
297
|
-
`loadData` se vuelve a ejecutar cada vez que cambia la cadena activa, pero los
|
|
298
|
-
activos son generales: no dependen de la cadena ni de la versión, así que la
|
|
299
|
-
respuesta sería idéntica. Volver a cargarlos borraría los archivos que el
|
|
300
|
-
usuario acaba de soltar y todavía no guarda —sin aviso y sin forma de
|
|
301
|
-
recuperarlos—. Por eso se cargan una sola vez por artículo.
|
|
302
|
-
*/
|
|
303
|
-
const assetsLoadedFor = useRef(null);
|
|
304
|
-
|
|
305
|
-
const handleOnSetSelectedAssets = (assets) =>
|
|
306
|
-
dispatch({
|
|
307
|
-
type: "SET_SELECTED_ASSETS",
|
|
308
|
-
payload: assets,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const handleOnSetAllAssetsChecked = (isChecked) =>
|
|
312
|
-
dispatch({
|
|
313
|
-
type: "TOGGLE_CHECK_ALL_ASSETS",
|
|
314
|
-
payload: isChecked,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
307
|
const thumbs = () => {
|
|
318
308
|
const imageInputs = state.images_values?.inputs?.map((e) => ({
|
|
319
309
|
value: e?.id,
|
|
@@ -356,7 +346,6 @@ const RetailerProductEditionView = ({
|
|
|
356
346
|
updateApprovedInputsImages={(item) =>
|
|
357
347
|
console.log("updateApprovedInputsImages", item)
|
|
358
348
|
}
|
|
359
|
-
onSelect={() => handleOnChangeCurrentImage(index)}
|
|
360
349
|
/>
|
|
361
350
|
));
|
|
362
351
|
};
|
|
@@ -623,6 +612,12 @@ const RetailerProductEditionView = ({
|
|
|
623
612
|
);
|
|
624
613
|
});
|
|
625
614
|
|
|
615
|
+
// Regenerar thumbnails de videos guardados para que se muestren.
|
|
616
|
+
const orderedValuesWithThumbs =
|
|
617
|
+
await regenerateVideoThumbnails(orderedValues);
|
|
618
|
+
const filteredValuesWithThumbs =
|
|
619
|
+
await regenerateVideoThumbnails(filteredValues);
|
|
620
|
+
|
|
626
621
|
// Dispatch consolidado
|
|
627
622
|
dispatch({
|
|
628
623
|
type: "SET_SERVICES",
|
|
@@ -631,7 +626,7 @@ const RetailerProductEditionView = ({
|
|
|
631
626
|
descriptions: services[1],
|
|
632
627
|
images: {
|
|
633
628
|
...services[2],
|
|
634
|
-
values:
|
|
629
|
+
values: orderedValuesWithThumbs,
|
|
635
630
|
},
|
|
636
631
|
},
|
|
637
632
|
});
|
|
@@ -648,12 +643,7 @@ const RetailerProductEditionView = ({
|
|
|
648
643
|
// Setear inputs del retailer activo directamente (evita cascada)
|
|
649
644
|
dispatch({
|
|
650
645
|
type: "SET_DATASHEETS_INPUTS",
|
|
651
|
-
|
|
652
|
-
// fichas puede volver sin datos para esa cadena y categoría. Arriba ya
|
|
653
|
-
// se contemplaba; aquí no, y como este dispatch va ANTES de
|
|
654
|
-
// SET_IMAGES_VALUES, reventar aquí dejaba la ficha cargando para
|
|
655
|
-
// siempre — con las imágenes ya traídas y sin pintar.
|
|
656
|
-
payload: [datasheetsActiveRetailer, services[0]?.inputs ?? {}],
|
|
646
|
+
payload: [datasheetsActiveRetailer, services[0].inputs],
|
|
657
647
|
});
|
|
658
648
|
|
|
659
649
|
dispatch({
|
|
@@ -665,23 +655,10 @@ const RetailerProductEditionView = ({
|
|
|
665
655
|
type: "SET_IMAGES_VALUES",
|
|
666
656
|
payload: {
|
|
667
657
|
...services[2],
|
|
668
|
-
values:
|
|
658
|
+
values: filteredValuesWithThumbs,
|
|
669
659
|
},
|
|
670
660
|
});
|
|
671
661
|
|
|
672
|
-
/*
|
|
673
|
-
Activos digitales. `services[2].assets` todavía no existe en la
|
|
674
|
-
respuesta del backend: hasta que exista, esto no hace nada, y el día que
|
|
675
|
-
llegue la bandeja se llena sola sin tocar nada más.
|
|
676
|
-
*/
|
|
677
|
-
if (
|
|
678
|
-
Array.isArray(services[2].assets) &&
|
|
679
|
-
assetsLoadedFor.current !== id_article
|
|
680
|
-
) {
|
|
681
|
-
assetsLoadedFor.current = id_article;
|
|
682
|
-
digitalAssets.replaceAll(services[2].assets);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
662
|
// Calcular campos requeridos sin valor
|
|
686
663
|
dispatch({
|
|
687
664
|
type: "SET_MISSING_REQUIRED_FIELDS",
|
|
@@ -827,8 +804,7 @@ const RetailerProductEditionView = ({
|
|
|
827
804
|
const canEvaluateAsAuditor =
|
|
828
805
|
state.services_data.every((srv) =>
|
|
829
806
|
["AC", "RP", "RCA"].includes(srv?.status?.replace(/.*\//, "")),
|
|
830
|
-
) &&
|
|
831
|
-
([1, 6].includes(user.id_role) || isAuditorAssigned());
|
|
807
|
+
) && ([1, 6].includes(user.id_role) || isAuditorAssigned());
|
|
832
808
|
|
|
833
809
|
return canEvaluateAsFacilitator || canEvaluateAsAuditor;
|
|
834
810
|
};
|
|
@@ -849,29 +825,7 @@ const RetailerProductEditionView = ({
|
|
|
849
825
|
return currentStatus;
|
|
850
826
|
};
|
|
851
827
|
|
|
852
|
-
/*
|
|
853
|
-
Guarda los activos y recarga si algo se escribió.
|
|
854
|
-
|
|
855
|
-
La recarga no es opcional: un activo recién creado sigue en memoria con su
|
|
856
|
-
id temporal y sin el id de la base. Sin volver a leer, el siguiente guardado
|
|
857
|
-
lo mandaría otra vez como nuevo y se duplicaría. Poner el ref en `null` es
|
|
858
|
-
lo que permite que `loadData` lo vuelva a traer —normalmente los activos se
|
|
859
|
-
cargan una sola vez por artículo—.
|
|
860
|
-
*/
|
|
861
|
-
const handleOnSaveAssets = async () => {
|
|
862
|
-
const guardado = await saveAssets(token);
|
|
863
|
-
if (!guardado) return;
|
|
864
|
-
|
|
865
|
-
assetsLoadedFor.current = null;
|
|
866
|
-
await loadData(false);
|
|
867
|
-
};
|
|
868
|
-
|
|
869
828
|
const handleOnClickSaveImages = async () => {
|
|
870
|
-
if (currentTray === TRAYS.ASSETS) {
|
|
871
|
-
await handleOnSaveAssets();
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
829
|
if (state.product?.services?.images === 1) {
|
|
876
830
|
/**
|
|
877
831
|
* `updateImages` y NO `saveImageAttrs`.
|
|
@@ -909,7 +863,7 @@ const RetailerProductEditionView = ({
|
|
|
909
863
|
const res = await axios.post(
|
|
910
864
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
|
|
911
865
|
{ articleId: state.product?.id_article },
|
|
912
|
-
{ headers: { Authorization: token } }
|
|
866
|
+
{ headers: { Authorization: token } }
|
|
913
867
|
);
|
|
914
868
|
const data = readPayload(res);
|
|
915
869
|
setFamily(data);
|
|
@@ -949,6 +903,20 @@ const RetailerProductEditionView = ({
|
|
|
949
903
|
? !!state.product?.parentArticleId
|
|
950
904
|
: !!family?.parent;
|
|
951
905
|
|
|
906
|
+
/*
|
|
907
|
+
Un hijo no se agrupa como variante (decisión de producto, 2026-09-25).
|
|
908
|
+
|
|
909
|
+
Agrupar declara «estos son el mismo producto en otra presentación», y un
|
|
910
|
+
hijo ya declaró de dónde deriva. El servidor lo rechaza con
|
|
911
|
+
VARIANT_GROUP_NOT_FOR_CHILD, así que ofrecer el botón sería ofrecer algo
|
|
912
|
+
que siempre falla.
|
|
913
|
+
|
|
914
|
+
Se apoya en `esHijoDeOtro` y no en `state.product` porque en esta pantalla
|
|
915
|
+
el producto sale de sessionStorage: la señal buena es la familia recién
|
|
916
|
+
consultada, con el producto solo como respaldo si esa consulta falló.
|
|
917
|
+
*/
|
|
918
|
+
const puedeAgruparse = !esHijoDeOtro;
|
|
919
|
+
|
|
952
920
|
/*
|
|
953
921
|
Abre otro producto de la familia.
|
|
954
922
|
|
|
@@ -985,9 +953,9 @@ const RetailerProductEditionView = ({
|
|
|
985
953
|
result.mode === "self"
|
|
986
954
|
? { ...state.product, parentArticleId: null, variantsCount: 0 }
|
|
987
955
|
: {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
956
|
+
...state.product,
|
|
957
|
+
variantsCount: Math.max((state.product?.variantsCount ?? 1) - 1, 0),
|
|
958
|
+
};
|
|
991
959
|
|
|
992
960
|
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
993
961
|
|
|
@@ -996,7 +964,7 @@ const RetailerProductEditionView = ({
|
|
|
996
964
|
// aplanada de esta pantalla lo dejaría mutilado en el siguiente F5.
|
|
997
965
|
try {
|
|
998
966
|
const guardado = JSON.parse(
|
|
999
|
-
sessionStorage.getItem("productSelected") ?? "null"
|
|
967
|
+
sessionStorage.getItem("productSelected") ?? "null"
|
|
1000
968
|
);
|
|
1001
969
|
|
|
1002
970
|
if (guardado) {
|
|
@@ -1006,7 +974,7 @@ const RetailerProductEditionView = ({
|
|
|
1006
974
|
...guardado,
|
|
1007
975
|
variantsCount: updatedProduct.variantsCount,
|
|
1008
976
|
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
1009
|
-
})
|
|
977
|
+
})
|
|
1010
978
|
);
|
|
1011
979
|
}
|
|
1012
980
|
} catch (error) {
|
|
@@ -1047,7 +1015,7 @@ const RetailerProductEditionView = ({
|
|
|
1047
1015
|
const res = await axios.post(
|
|
1048
1016
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/group`,
|
|
1049
1017
|
{ articleId: state.product?.id_article },
|
|
1050
|
-
{ headers: { Authorization: token } }
|
|
1018
|
+
{ headers: { Authorization: token } }
|
|
1051
1019
|
);
|
|
1052
1020
|
const data = readPayload(res);
|
|
1053
1021
|
setVariantGroup(data);
|
|
@@ -1070,7 +1038,7 @@ const RetailerProductEditionView = ({
|
|
|
1070
1038
|
const res = await axios.post(
|
|
1071
1039
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/leave`,
|
|
1072
1040
|
{ articleId: state.product?.id_article },
|
|
1073
|
-
{ headers: { Authorization: token } }
|
|
1041
|
+
{ headers: { Authorization: token } }
|
|
1074
1042
|
);
|
|
1075
1043
|
setVariantGroup(readPayload(res));
|
|
1076
1044
|
setShowGroup(false);
|
|
@@ -1105,7 +1073,7 @@ const RetailerProductEditionView = ({
|
|
|
1105
1073
|
{
|
|
1106
1074
|
params: { filterByIdArticle: articleId, includeFilters: false },
|
|
1107
1075
|
headers: { Authorization: token },
|
|
1108
|
-
}
|
|
1076
|
+
}
|
|
1109
1077
|
);
|
|
1110
1078
|
|
|
1111
1079
|
// read-order-services responde { total, data: [...] }. La vista trae una
|
|
@@ -1113,12 +1081,12 @@ const RetailerProductEditionView = ({
|
|
|
1113
1081
|
// ordenadas por order_id DESC y la primera es la orden más reciente.
|
|
1114
1082
|
const { data } = readPayload(res) ?? {};
|
|
1115
1083
|
const encontrado = (data ?? []).find(
|
|
1116
|
-
(item) => Number(item?.article?.id_article) === Number(articleId)
|
|
1084
|
+
(item) => Number(item?.article?.id_article) === Number(articleId)
|
|
1117
1085
|
);
|
|
1118
1086
|
|
|
1119
1087
|
if (!encontrado) {
|
|
1120
1088
|
setAvisoFamilia(
|
|
1121
|
-
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí."
|
|
1089
|
+
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí."
|
|
1122
1090
|
);
|
|
1123
1091
|
return;
|
|
1124
1092
|
}
|
|
@@ -1131,7 +1099,7 @@ const RetailerProductEditionView = ({
|
|
|
1131
1099
|
ArticleId: encontrado.article.id_article,
|
|
1132
1100
|
categoryId: encontrado.article.id_article,
|
|
1133
1101
|
product: [encontrado],
|
|
1134
|
-
})
|
|
1102
|
+
})
|
|
1135
1103
|
);
|
|
1136
1104
|
|
|
1137
1105
|
window.location.assign("/EditProducts");
|
|
@@ -1156,8 +1124,7 @@ const RetailerProductEditionView = ({
|
|
|
1156
1124
|
? {
|
|
1157
1125
|
...state.product,
|
|
1158
1126
|
variantsCount:
|
|
1159
|
-
(state.product?.variantsCount ?? 0) +
|
|
1160
|
-
(result.linked ?? []).length,
|
|
1127
|
+
(state.product?.variantsCount ?? 0) + (result.linked ?? []).length,
|
|
1161
1128
|
}
|
|
1162
1129
|
: {
|
|
1163
1130
|
...state.product,
|
|
@@ -1181,7 +1148,7 @@ const RetailerProductEditionView = ({
|
|
|
1181
1148
|
*/
|
|
1182
1149
|
try {
|
|
1183
1150
|
const guardado = JSON.parse(
|
|
1184
|
-
sessionStorage.getItem("productSelected") ?? "null"
|
|
1151
|
+
sessionStorage.getItem("productSelected") ?? "null"
|
|
1185
1152
|
);
|
|
1186
1153
|
|
|
1187
1154
|
if (guardado) {
|
|
@@ -1191,7 +1158,7 @@ const RetailerProductEditionView = ({
|
|
|
1191
1158
|
...guardado,
|
|
1192
1159
|
variantsCount: updatedProduct.variantsCount,
|
|
1193
1160
|
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
1194
|
-
})
|
|
1161
|
+
})
|
|
1195
1162
|
);
|
|
1196
1163
|
}
|
|
1197
1164
|
} catch (error) {
|
|
@@ -1233,10 +1200,7 @@ const RetailerProductEditionView = ({
|
|
|
1233
1200
|
`rel_article_description` y la ficha técnica en `article_data`: el endpoint
|
|
1234
1201
|
recibe una lista o la otra.
|
|
1235
1202
|
*/
|
|
1236
|
-
const handleOnRestoreInheritance = async (
|
|
1237
|
-
attributeId,
|
|
1238
|
-
kind = "attribute",
|
|
1239
|
-
) => {
|
|
1203
|
+
const handleOnRestoreInheritance = async (attributeId, kind = "attribute") => {
|
|
1240
1204
|
if (!attributeId || !state.product?.id_article) return;
|
|
1241
1205
|
|
|
1242
1206
|
const esDescripcion = kind === "description";
|
|
@@ -1250,7 +1214,7 @@ const RetailerProductEditionView = ({
|
|
|
1250
1214
|
? { descriptions: [Number(attributeId)] }
|
|
1251
1215
|
: { attributes: [Number(attributeId)] }),
|
|
1252
1216
|
},
|
|
1253
|
-
{ headers: { Authorization: token } }
|
|
1217
|
+
{ headers: { Authorization: token } }
|
|
1254
1218
|
);
|
|
1255
1219
|
|
|
1256
1220
|
const data = readPayload(res);
|
|
@@ -1306,13 +1270,68 @@ const RetailerProductEditionView = ({
|
|
|
1306
1270
|
return soloPropagables(cambios);
|
|
1307
1271
|
};
|
|
1308
1272
|
|
|
1273
|
+
/*
|
|
1274
|
+
Lo que se acaba de editar en la pestaña de descripciones.
|
|
1275
|
+
|
|
1276
|
+
Va aparte de `cambiosDeFichaTecnica` porque son tablas distintas y el
|
|
1277
|
+
endpoint recibe una lista o la otra. Aquí NO se filtran UPC ni SKU: esos son
|
|
1278
|
+
atributos de ficha técnica y no aparecen en esta pestaña.
|
|
1279
|
+
*/
|
|
1280
|
+
const cambiosDeDescripciones = () => {
|
|
1281
|
+
const retailerId = state.active_retailer?.id_retailer;
|
|
1282
|
+
const inputs = state.services?.descriptions?.inputs ?? {};
|
|
1283
|
+
|
|
1284
|
+
return (state.updated_descriptions_inputs ?? []).map((item) => {
|
|
1285
|
+
const input = inputs[`${item.attributeId}_${retailerId}`];
|
|
1286
|
+
return {
|
|
1287
|
+
id: Number(item.attributeId),
|
|
1288
|
+
name: input?.name ?? `Descripción ${item.attributeId}`,
|
|
1289
|
+
value: item.value,
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1309
1294
|
const handleOnClickSave = async () => {
|
|
1310
1295
|
if (state.saving || preparandoPropagacion) return;
|
|
1311
1296
|
|
|
1312
1297
|
switch (state.active_tab) {
|
|
1313
|
-
case "Descripción":
|
|
1298
|
+
case "Descripción": {
|
|
1299
|
+
/*
|
|
1300
|
+
Guardar una descripción en un padre también abre la decisión de
|
|
1301
|
+
propagar. Sin esto, el hijo heredaba la descripción al ligarse y se
|
|
1302
|
+
quedaba con la versión vieja en cuanto el padre la corregía, sin que
|
|
1303
|
+
nadie lo notara.
|
|
1304
|
+
*/
|
|
1305
|
+
const cambiosDesc = cambiosDeDescripciones();
|
|
1306
|
+
const companerasDesc = (variantGroup?.members ?? []).filter(
|
|
1307
|
+
(m) => !m.isCurrent
|
|
1308
|
+
);
|
|
1309
|
+
|
|
1310
|
+
if (esHijoDeOtro || cambiosDesc.length === 0) {
|
|
1311
|
+
await saveDescriptions(token);
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
setPreparandoPropagacion(true);
|
|
1316
|
+
let familiaDesc = null;
|
|
1317
|
+
try {
|
|
1318
|
+
familiaDesc = await loadFamily(true);
|
|
1319
|
+
} finally {
|
|
1320
|
+
setPreparandoPropagacion(false);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const hijosDesc = (familiaDesc?.variants ?? []).filter(
|
|
1324
|
+
(v) => !v.isCurrent
|
|
1325
|
+
);
|
|
1326
|
+
|
|
1327
|
+
if (hijosDesc.length > 0 || companerasDesc.length > 0) {
|
|
1328
|
+
setPendingPropagation({ cambios: cambiosDesc, kind: "description" });
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1314
1332
|
await saveDescriptions(token);
|
|
1315
1333
|
break;
|
|
1334
|
+
}
|
|
1316
1335
|
case "Ficha técnica": {
|
|
1317
1336
|
const cambios = cambiosDeFichaTecnica();
|
|
1318
1337
|
|
|
@@ -1377,7 +1396,20 @@ const RetailerProductEditionView = ({
|
|
|
1377
1396
|
return;
|
|
1378
1397
|
}
|
|
1379
1398
|
|
|
1380
|
-
|
|
1399
|
+
/*
|
|
1400
|
+
Un HIJO no propaga a nadie, y hay que decirlo explícitamente.
|
|
1401
|
+
|
|
1402
|
+
`familia.variants` mira la familia desde el PADRE —eso es lo correcto
|
|
1403
|
+
para navegar entre hermanos— así que desde la ficha de un hijo esa
|
|
1404
|
+
lista son sus HERMANOS. Leerla como «los destinos de este producto»
|
|
1405
|
+
hacía que el diálogo le ofreciera heredar a sus hermanos. El servidor
|
|
1406
|
+
resolvía los destinos por su cuenta y no escribía nada, de modo que el
|
|
1407
|
+
usuario elegía, guardaba, y no pasaba nada: un no-operativo silencioso.
|
|
1408
|
+
*/
|
|
1409
|
+
const esHijo = !!familia?.parent;
|
|
1410
|
+
const hijos = esHijo
|
|
1411
|
+
? []
|
|
1412
|
+
: (familia.variants ?? []).filter((v) => !v.isCurrent);
|
|
1381
1413
|
|
|
1382
1414
|
/*
|
|
1383
1415
|
También cuentan las compañeras de grupo: un producto puede no tener
|
|
@@ -1385,11 +1417,13 @@ const RetailerProductEditionView = ({
|
|
|
1385
1417
|
puede copiar igual. Si no se miraran, guardar no ofrecería nada.
|
|
1386
1418
|
*/
|
|
1387
1419
|
const companeras = (variantGroup?.members ?? []).filter(
|
|
1388
|
-
(m) => !m.isCurrent
|
|
1420
|
+
(m) => !m.isCurrent
|
|
1389
1421
|
);
|
|
1390
1422
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1423
|
+
// Un hijo tampoco puede estar agrupado desde 2026-09-25, así que para
|
|
1424
|
+
// él no queda ningún destino posible.
|
|
1425
|
+
if (!esHijo && (hijos.length > 0 || companeras.length > 0)) {
|
|
1426
|
+
setPendingPropagation({ cambios, kind: "datasheet" });
|
|
1393
1427
|
return;
|
|
1394
1428
|
}
|
|
1395
1429
|
|
|
@@ -1397,16 +1431,6 @@ const RetailerProductEditionView = ({
|
|
|
1397
1431
|
break;
|
|
1398
1432
|
}
|
|
1399
1433
|
case "Imágenes":
|
|
1400
|
-
/*
|
|
1401
|
-
Se guarda la bandeja que el usuario tiene a la vista. Guardar las dos
|
|
1402
|
-
de golpe haría que, estando en activos, `updateImages` enviara también
|
|
1403
|
-
los cambios de imágenes que el usuario quizá no dio por terminados —y
|
|
1404
|
-
al revés—. El botón guarda lo que se está viendo.
|
|
1405
|
-
*/
|
|
1406
|
-
if (currentTray === TRAYS.ASSETS) {
|
|
1407
|
-
await handleOnSaveAssets();
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
1434
|
await updateImages(token);
|
|
1411
1435
|
break;
|
|
1412
1436
|
default:
|
|
@@ -1430,98 +1454,7 @@ const RetailerProductEditionView = ({
|
|
|
1430
1454
|
});
|
|
1431
1455
|
};
|
|
1432
1456
|
|
|
1433
|
-
/*
|
|
1434
|
-
Descarga de activos: los seleccionados, o todos si no hay selección — el
|
|
1435
|
-
mismo criterio que usa la descarga de imágenes.
|
|
1436
|
-
|
|
1437
|
-
Solo se descarga lo que ya está en S3. Un activo recién soltado vive como
|
|
1438
|
-
referencia al archivo del disco del usuario: "descargarlo" le devolvería el
|
|
1439
|
-
archivo que él mismo acaba de elegir, lo cual no tiene sentido.
|
|
1440
|
-
*/
|
|
1441
|
-
const handleOnDownloadAssets = () => {
|
|
1442
|
-
const seleccion = state.selected_assets.length
|
|
1443
|
-
? state.selected_assets
|
|
1444
|
-
: state.assets_values;
|
|
1445
|
-
|
|
1446
|
-
const descargables = seleccion.filter((asset) => asset.id && asset.url);
|
|
1447
|
-
|
|
1448
|
-
if (descargables.length === 0) {
|
|
1449
|
-
dispatch({
|
|
1450
|
-
type: "SET_MODAL",
|
|
1451
|
-
payload: {
|
|
1452
|
-
show: true,
|
|
1453
|
-
title: "Nada que descargar",
|
|
1454
|
-
message:
|
|
1455
|
-
"Los activos seleccionados todavía no se han guardado. Guárdalos primero.",
|
|
1456
|
-
image: warningIcon,
|
|
1457
|
-
},
|
|
1458
|
-
});
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
descargables.forEach((asset) => saveAs(asset.url, asset.name));
|
|
1463
|
-
};
|
|
1464
|
-
|
|
1465
|
-
/*
|
|
1466
|
-
Borrado en lote de activos.
|
|
1467
|
-
|
|
1468
|
-
A diferencia de las imágenes, que se borran contra el backend en el momento,
|
|
1469
|
-
los activos se quitan de la bandeja y su baja viaja en el mismo PUT del
|
|
1470
|
-
guardado (`deleteAssets`). Es lo que permite al usuario deshacerse de un
|
|
1471
|
-
borrado por error: basta con no guardar. El mensaje lo dice, para que nadie
|
|
1472
|
-
se quede pensando que ya se aplicó.
|
|
1473
|
-
*/
|
|
1474
|
-
const handleOnAskToDeleteAssets = () => {
|
|
1475
|
-
if (state.selected_assets.length === 0) {
|
|
1476
|
-
dispatch({
|
|
1477
|
-
type: "SET_MODAL",
|
|
1478
|
-
payload: {
|
|
1479
|
-
show: true,
|
|
1480
|
-
title: "Eliminar activos digitales",
|
|
1481
|
-
message: "No has seleccionado ningún activo para eliminar.",
|
|
1482
|
-
image: warningIcon,
|
|
1483
|
-
},
|
|
1484
|
-
});
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
const seleccionados = state.selected_assets;
|
|
1489
|
-
|
|
1490
|
-
dispatch({
|
|
1491
|
-
type: "SET_MODAL",
|
|
1492
|
-
payload: {
|
|
1493
|
-
show: true,
|
|
1494
|
-
title: "Eliminar activos digitales",
|
|
1495
|
-
message: `Se quitarán ${seleccionados.length} activo(s) de la bandeja. El borrado se aplica al guardar.`,
|
|
1496
|
-
image: warningIcon,
|
|
1497
|
-
buttons: [
|
|
1498
|
-
{
|
|
1499
|
-
text: "Cancelar",
|
|
1500
|
-
buttonType: "general-white-button",
|
|
1501
|
-
action: () =>
|
|
1502
|
-
dispatch({ type: "SET_MODAL", payload: { show: false } }),
|
|
1503
|
-
},
|
|
1504
|
-
{
|
|
1505
|
-
text: "Eliminar",
|
|
1506
|
-
buttonType: "general-button-default",
|
|
1507
|
-
action: () => {
|
|
1508
|
-
dispatch({ type: "SET_MODAL", payload: { show: false } });
|
|
1509
|
-
seleccionados.forEach((asset) =>
|
|
1510
|
-
digitalAssets.removeAsset(asset),
|
|
1511
|
-
);
|
|
1512
|
-
},
|
|
1513
|
-
},
|
|
1514
|
-
],
|
|
1515
|
-
},
|
|
1516
|
-
});
|
|
1517
|
-
};
|
|
1518
|
-
|
|
1519
1457
|
const handleOnAskToDeleteImages = () => {
|
|
1520
|
-
if (currentTray === TRAYS.ASSETS) {
|
|
1521
|
-
handleOnAskToDeleteAssets();
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
1458
|
if (state.selected_images.length === 0) {
|
|
1526
1459
|
dispatch({
|
|
1527
1460
|
type: "SET_MODAL",
|
|
@@ -2235,25 +2168,7 @@ const RetailerProductEditionView = ({
|
|
|
2235
2168
|
</Container>
|
|
2236
2169
|
);
|
|
2237
2170
|
|
|
2238
|
-
|
|
2239
|
-
`images_values` entra al guard junto a `services` y `product`.
|
|
2240
|
-
|
|
2241
|
-
React 17 no agrupa los dispatch que van después de un `await`: cada uno
|
|
2242
|
-
re-renderiza al instante. En `loadData` el orden es SET_SERVICES →
|
|
2243
|
-
…_INPUTS → SET_IMAGES_VALUES, así que había renders intermedios con
|
|
2244
|
-
`services` ya puesto —el guard los dejaba pasar— y el resto todavía en null.
|
|
2245
|
-
El cuerpo de abajo lee `datasheets_inputs[1]`, `descriptions_inputs[0]` y
|
|
2246
|
-
`images_values.values` sin protección, y tumbaba la pantalla entera.
|
|
2247
|
-
|
|
2248
|
-
Se comprueba `images_values` y no cada uno: es el ÚLTIMO que `loadData`
|
|
2249
|
-
despacha, así que si ya está, los anteriores también.
|
|
2250
|
-
*/
|
|
2251
|
-
if (
|
|
2252
|
-
state.loading ||
|
|
2253
|
-
!state.services ||
|
|
2254
|
-
!state.product ||
|
|
2255
|
-
!state.images_values
|
|
2256
|
-
)
|
|
2171
|
+
if (state.loading || !state.services || !state.product)
|
|
2257
2172
|
return (
|
|
2258
2173
|
<>
|
|
2259
2174
|
<ProductEditionSkeleton />
|
|
@@ -2271,16 +2186,41 @@ const RetailerProductEditionView = ({
|
|
|
2271
2186
|
{pendingPropagation && (
|
|
2272
2187
|
<PropagateChangesModal
|
|
2273
2188
|
changedAttributes={pendingPropagation.cambios}
|
|
2274
|
-
|
|
2189
|
+
kind={pendingPropagation.kind ?? "datasheet"}
|
|
2190
|
+
variants={
|
|
2191
|
+
esHijoDeOtro
|
|
2192
|
+
? []
|
|
2193
|
+
: (family?.variants ?? []).filter((v) => !v.isCurrent)
|
|
2194
|
+
}
|
|
2275
2195
|
groupMembers={(variantGroup?.members ?? []).filter(
|
|
2276
|
-
(m) => !m.isCurrent
|
|
2196
|
+
(m) => !m.isCurrent
|
|
2277
2197
|
)}
|
|
2278
2198
|
groupCode={variantGroup?.group ?? null}
|
|
2279
2199
|
saving={state.saving}
|
|
2280
2200
|
onCancel={() => setPendingPropagation(null)}
|
|
2281
2201
|
onConfirm={(propagation) => {
|
|
2202
|
+
const esDescripcion = pendingPropagation.kind === "description";
|
|
2282
2203
|
setPendingPropagation(null);
|
|
2283
|
-
|
|
2204
|
+
|
|
2205
|
+
if (!propagation) {
|
|
2206
|
+
if (esDescripcion) saveDescriptions(token);
|
|
2207
|
+
else saveDatasheets(token);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/*
|
|
2212
|
+
El diálogo habla de «atributos» porque es el mismo componente
|
|
2213
|
+
para las dos pestañas. Al guardar hay que ponerlo en la lista
|
|
2214
|
+
que corresponde: el servidor recibe `attributes` o
|
|
2215
|
+
`descriptions`, nunca una por la otra.
|
|
2216
|
+
*/
|
|
2217
|
+
const { attributes, ...destinos } = propagation;
|
|
2218
|
+
|
|
2219
|
+
if (esDescripcion) {
|
|
2220
|
+
saveDescriptions(token, { ...destinos, descriptions: attributes });
|
|
2221
|
+
} else {
|
|
2222
|
+
saveDatasheets(token, propagation);
|
|
2223
|
+
}
|
|
2284
2224
|
}}
|
|
2285
2225
|
/>
|
|
2286
2226
|
)}
|
|
@@ -2490,18 +2430,8 @@ const RetailerProductEditionView = ({
|
|
|
2490
2430
|
<div className="data-container">
|
|
2491
2431
|
<div className="image-data-panel">
|
|
2492
2432
|
<ImagePreviewer
|
|
2493
|
-
/*
|
|
2494
|
-
`images_values` puede ser null en pleno render.
|
|
2495
|
-
|
|
2496
|
-
React 17 no agrupa los dispatch que van después de un `await`:
|
|
2497
|
-
cada uno re-renderiza al instante. En `loadData`, `SET_SERVICES`
|
|
2498
|
-
se despacha antes que `SET_IMAGES_VALUES`, así que hay un render
|
|
2499
|
-
en el que `services` ya existe —y por eso el guard de arriba deja
|
|
2500
|
-
pasar— pero las imágenes todavía no. Sin las interrogaciones,
|
|
2501
|
-
ese render intermedio tumba la pantalla entera.
|
|
2502
|
-
*/
|
|
2503
2433
|
activeImage={
|
|
2504
|
-
state.images_values
|
|
2434
|
+
state.images_values.values[state.current_image] ?? {}
|
|
2505
2435
|
}
|
|
2506
2436
|
imagesArray={state.images_values || []}
|
|
2507
2437
|
setActiveImage={handleOnChangeCurrentImage}
|
|
@@ -2688,6 +2618,7 @@ const RetailerProductEditionView = ({
|
|
|
2688
2618
|
herencia y tiene jerarquía; esto es solo un código compartido
|
|
2689
2619
|
que dice "estos son el mismo producto en otra presentación".
|
|
2690
2620
|
Juntarlos haría creer que agrupar hereda algo, que no pasa. */}
|
|
2621
|
+
{puedeAgruparse && (
|
|
2691
2622
|
<div className="variants-actions">
|
|
2692
2623
|
<button
|
|
2693
2624
|
type="button"
|
|
@@ -2709,8 +2640,9 @@ const RetailerProductEditionView = ({
|
|
|
2709
2640
|
</button>
|
|
2710
2641
|
)}
|
|
2711
2642
|
</div>
|
|
2643
|
+
)}
|
|
2712
2644
|
|
|
2713
|
-
{showGroup && variantGroup?.group && (
|
|
2645
|
+
{puedeAgruparse && showGroup && variantGroup?.group && (
|
|
2714
2646
|
<div className="variants-family">
|
|
2715
2647
|
{(variantGroup.members ?? [])
|
|
2716
2648
|
.filter((miembro) => !miembro.isCurrent)
|
|
@@ -2807,13 +2739,7 @@ const RetailerProductEditionView = ({
|
|
|
2807
2739
|
setShowVersionSelector={setShowVersionSelector}
|
|
2808
2740
|
// setSelectedImages={setSelectedImages} prop pasada asi: FullTabsMenu -> TabsMenu Pero en TabsMenu no se espera ninguna prop setSelectedImages
|
|
2809
2741
|
// actions
|
|
2810
|
-
|
|
2811
|
-
// las imágenes del producto, que no es lo que el usuario pidió.
|
|
2812
|
-
downloadImages={
|
|
2813
|
-
currentTray === TRAYS.ASSETS
|
|
2814
|
-
? handleOnDownloadAssets
|
|
2815
|
-
: handleOnDownloadImages
|
|
2816
|
-
}
|
|
2742
|
+
downloadImages={handleOnDownloadImages}
|
|
2817
2743
|
askToDeleteImages={handleOnAskToDeleteImages}
|
|
2818
2744
|
showSaveButton={isAuditorAssigned() || isUserAssignedToService()}
|
|
2819
2745
|
onClickSave={handleOnClickSave}
|
|
@@ -2891,36 +2817,7 @@ const RetailerProductEditionView = ({
|
|
|
2891
2817
|
{state.active_tab === "Imágenes" &&
|
|
2892
2818
|
(state.product?.images_status !== "NS" ? (
|
|
2893
2819
|
<>
|
|
2894
|
-
{
|
|
2895
|
-
<GalleryTrayTabs
|
|
2896
|
-
activeTray={currentTray}
|
|
2897
|
-
setActiveTray={setActiveTray}
|
|
2898
|
-
imagesCount={
|
|
2899
|
-
state.images_values?.values?.length ?? 0
|
|
2900
|
-
}
|
|
2901
|
-
assetsCount={digitalAssets.assets.length}
|
|
2902
|
-
/>
|
|
2903
|
-
)}
|
|
2904
|
-
{currentTray === TRAYS.ASSETS && (
|
|
2905
|
-
<section
|
|
2906
|
-
className="container"
|
|
2907
|
-
style={{ position: "relative" }}
|
|
2908
|
-
>
|
|
2909
|
-
<DigitalAssetsTray
|
|
2910
|
-
{...digitalAssets}
|
|
2911
|
-
unsaved={countUnsavedAssets(state)}
|
|
2912
|
-
gridLayout={imageLayout}
|
|
2913
|
-
readOnly={
|
|
2914
|
-
!isAuditorAssigned() &&
|
|
2915
|
-
!isUserAssignedToService()
|
|
2916
|
-
}
|
|
2917
|
-
selectedAssets={state.selected_assets}
|
|
2918
|
-
setSelectedAssets={handleOnSetSelectedAssets}
|
|
2919
|
-
setCheckAll={handleOnSetAllAssetsChecked}
|
|
2920
|
-
/>
|
|
2921
|
-
</section>
|
|
2922
|
-
)}
|
|
2923
|
-
{currentTray === TRAYS.IMAGES && !imageLayout && (
|
|
2820
|
+
{!imageLayout && (
|
|
2924
2821
|
<GalleryHeader
|
|
2925
2822
|
checkAll={state.all_image_checked} // Determina si el checkbox "Seleccionar todo" está marcado
|
|
2926
2823
|
setCheckAll={handleOnSetCheckAll} // Toggler de true o false para el checkbox "Seleccionar todo"
|
|
@@ -2928,76 +2825,74 @@ const RetailerProductEditionView = ({
|
|
|
2928
2825
|
// shotThd={shotThd} // No se usa?
|
|
2929
2826
|
/>
|
|
2930
2827
|
)}
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2828
|
+
<section
|
|
2829
|
+
className="container"
|
|
2830
|
+
style={{ position: "relative" }}
|
|
2831
|
+
>
|
|
2832
|
+
<div
|
|
2833
|
+
{...getRootProps({
|
|
2834
|
+
className: `dropzone ${
|
|
2835
|
+
isDragActive ? "drag-active" : ""
|
|
2836
|
+
}`,
|
|
2837
|
+
})}
|
|
2935
2838
|
>
|
|
2839
|
+
<input {...getInputProps()} />
|
|
2840
|
+
{isDragActive && (
|
|
2841
|
+
<div className="drag-overlay">
|
|
2842
|
+
<p>Suelta las imágenes aquí</p>
|
|
2843
|
+
</div>
|
|
2844
|
+
)}
|
|
2845
|
+
<aside>{thumbs()}</aside>
|
|
2846
|
+
</div>
|
|
2847
|
+
{state.images_values?.values.length === 0 && (
|
|
2936
2848
|
<div
|
|
2937
|
-
{
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2849
|
+
style={{
|
|
2850
|
+
position: "absolute",
|
|
2851
|
+
top: "50%",
|
|
2852
|
+
left: "50%",
|
|
2853
|
+
transform: "translate(-50%, -50%)",
|
|
2854
|
+
textAlign: "center",
|
|
2855
|
+
padding: "40px",
|
|
2856
|
+
width: "80%",
|
|
2857
|
+
maxWidth: "500px",
|
|
2858
|
+
fontFamily: "Arial, sans-serif",
|
|
2859
|
+
}}
|
|
2942
2860
|
>
|
|
2943
|
-
<
|
|
2944
|
-
{isDragActive && (
|
|
2945
|
-
<div className="drag-overlay">
|
|
2946
|
-
<p>Suelta las imágenes aquí</p>
|
|
2947
|
-
</div>
|
|
2948
|
-
)}
|
|
2949
|
-
<aside>{thumbs()}</aside>
|
|
2950
|
-
</div>
|
|
2951
|
-
{state.images_values?.values?.length === 0 && (
|
|
2952
|
-
<div
|
|
2861
|
+
<p
|
|
2953
2862
|
style={{
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
transform: "translate(-50%, -50%)",
|
|
2958
|
-
textAlign: "center",
|
|
2959
|
-
padding: "40px",
|
|
2960
|
-
width: "80%",
|
|
2961
|
-
maxWidth: "500px",
|
|
2962
|
-
fontFamily: "Arial, sans-serif",
|
|
2863
|
+
fontSize: "18px",
|
|
2864
|
+
color: "#666",
|
|
2865
|
+
marginBottom: "16px",
|
|
2963
2866
|
}}
|
|
2964
2867
|
>
|
|
2868
|
+
Este producto no tiene imágenes
|
|
2869
|
+
</p>
|
|
2870
|
+
{isAuditorAssigned() ||
|
|
2871
|
+
isUserAssignedToService() ? (
|
|
2965
2872
|
<p
|
|
2966
2873
|
style={{
|
|
2967
|
-
fontSize: "
|
|
2968
|
-
color: "#
|
|
2969
|
-
marginBottom: "
|
|
2874
|
+
fontSize: "14px",
|
|
2875
|
+
color: "#999",
|
|
2876
|
+
marginBottom: "20px",
|
|
2970
2877
|
}}
|
|
2971
2878
|
>
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
isUserAssignedToService() ? (
|
|
2976
|
-
<p
|
|
2879
|
+
Arrastra las imágenes aquí o{" "}
|
|
2880
|
+
<span
|
|
2881
|
+
onClick={open}
|
|
2977
2882
|
style={{
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2883
|
+
color: "#007bff",
|
|
2884
|
+
cursor: "pointer",
|
|
2885
|
+
textDecoration: "underline",
|
|
2981
2886
|
}}
|
|
2982
2887
|
>
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
>
|
|
2992
|
-
haz clic para abrir el explorador de
|
|
2993
|
-
archivos
|
|
2994
|
-
</span>
|
|
2995
|
-
</p>
|
|
2996
|
-
) : null}
|
|
2997
|
-
</div>
|
|
2998
|
-
)}
|
|
2999
|
-
</section>
|
|
3000
|
-
)}
|
|
2888
|
+
haz clic para abrir el explorador de
|
|
2889
|
+
archivos
|
|
2890
|
+
</span>
|
|
2891
|
+
</p>
|
|
2892
|
+
) : null}
|
|
2893
|
+
</div>
|
|
2894
|
+
)}
|
|
2895
|
+
</section>
|
|
3001
2896
|
</>
|
|
3002
2897
|
) : (
|
|
3003
2898
|
<ScreenHeader
|