contentoh-components-library 21.6.49 → 21.6.50
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 +14 -0
- package/.env.test +1 -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/AssetPreview/AssetPreview.stories.js +118 -0
- package/dist/components/atoms/AssetPreview/index.js +150 -0
- package/dist/components/atoms/AssetPreview/styles.js +28 -0
- package/dist/components/atoms/IconFile/index.js +85 -2
- package/dist/components/atoms/IconFile/styles.js +1 -1
- package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
- package/dist/components/atoms/ProductImage/index.js +100 -27
- package/dist/components/atoms/ProductImage/styles.js +3 -1
- package/dist/components/molecules/EditionActiveImage/index.js +4 -2
- package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
- package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
- package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
- package/dist/components/molecules/GalleryElement/index.js +13 -4
- package/dist/components/molecules/GalleryHeader/index.js +96 -24
- package/dist/components/molecules/GalleryHeader/styles.js +4 -1
- package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
- package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
- package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
- package/dist/components/molecules/ImageSelector/index.js +1 -0
- package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +171 -0
- package/dist/components/organisms/DigitalAssetsTray/index.js +211 -0
- package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
- package/dist/components/organisms/ImagePreviewer/index.js +1 -0
- package/dist/components/organisms/ProductImageModal/index.js +3 -2
- package/dist/components/organisms/PropagateChangesModal/index.js +12 -1
- package/dist/components/organisms/PropagateChangesModal/non-propagable.js +45 -0
- package/dist/components/pages/ProviderProductEdition/index.js +11 -1
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
- package/dist/components/pages/RetailerProductEdition/index.js +605 -414
- package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +45 -145
- package/dist/global-files/data.js +16 -3
- package/dist/global-files/digital-assets/assetTypes.js +480 -0
- package/dist/global-files/digital-assets/assets.actions.js +64 -0
- package/dist/global-files/digital-assets/assets.adapter.js +304 -0
- package/dist/global-files/digital-assets/assets.mock.js +136 -0
- package/dist/global-files/digital-assets/assets.reducer.js +220 -0
- package/dist/global-files/digital-assets/uploadAssets.js +179 -0
- package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
- package/dist/index.js +142 -90
- package/docs/activos-digitales.md +273 -0
- package/package.json +1 -1
- package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
- package/src/components/atoms/AssetPreview/index.js +119 -0
- package/src/components/atoms/AssetPreview/styles.js +190 -0
- package/src/components/atoms/IconFile/index.js +28 -1
- package/src/components/atoms/IconFile/styles.js +8 -0
- package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
- package/src/components/atoms/ProductImage/index.js +95 -20
- package/src/components/atoms/ProductImage/styles.js +92 -7
- package/src/components/molecules/EditionActiveImage/index.js +2 -2
- package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
- package/src/components/molecules/GalleryAssetElement/index.js +162 -0
- package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
- package/src/components/molecules/GalleryElement/index.js +9 -0
- package/src/components/molecules/GalleryHeader/index.js +65 -13
- package/src/components/molecules/GalleryHeader/styles.js +2 -1
- package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
- package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
- package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
- package/src/components/molecules/ImageSelector/index.js +1 -0
- package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
- package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
- package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
- package/src/components/organisms/ImagePreviewer/index.js +1 -0
- package/src/components/organisms/ProductImageModal/index.js +1 -0
- package/src/components/organisms/PropagateChangesModal/index.js +11 -1
- package/src/components/organisms/PropagateChangesModal/non-propagable.js +29 -0
- package/src/components/pages/ProviderProductEdition/index.js +10 -1
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
- package/src/components/pages/RetailerProductEdition/index.js +366 -140
- package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +51 -104
- package/src/global-files/data.js +17 -2
- package/src/global-files/digital-assets/assetTypes.js +419 -0
- package/src/global-files/digital-assets/assets.actions.js +50 -0
- package/src/global-files/digital-assets/assets.adapter.js +229 -0
- package/src/global-files/digital-assets/assets.mock.js +122 -0
- package/src/global-files/digital-assets/assets.reducer.js +197 -0
- package/src/global-files/digital-assets/uploadAssets.js +109 -0
- package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
- package/src/index.js +4 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import axios from "axios";
|
|
4
4
|
import { useDropzone } from "react-dropzone";
|
|
@@ -30,6 +30,8 @@ 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";
|
|
33
35
|
|
|
34
36
|
import { Button } from "../../atoms/GeneralButton";
|
|
35
37
|
import { GenericModal } from "../../atoms/GenericModal";
|
|
@@ -39,12 +41,14 @@ import { ButtonV2 } from "../../atoms/ButtonV2";
|
|
|
39
41
|
|
|
40
42
|
import { GalleryElement } from "../../molecules/GalleryElement";
|
|
41
43
|
import { GalleryHeader } from "../../molecules/GalleryHeader";
|
|
44
|
+
import { GalleryTrayTabs, TRAYS } from "../../molecules/GalleryTrayTabs";
|
|
42
45
|
import { HeaderTop } from "../../molecules/HeaderTop";
|
|
43
46
|
import { TagAndInput } from "../../molecules/TagAndInput/index";
|
|
44
47
|
|
|
45
48
|
import { FullProductNameHeader } from "../../organisms/FullProductNameHeader";
|
|
46
49
|
import { FullTabsMenu } from "../../organisms/FullTabsMenu";
|
|
47
50
|
import { ImageDataTable } from "../../organisms/ImageDataTable";
|
|
51
|
+
import { DigitalAssetsTray } from "../../organisms/DigitalAssetsTray";
|
|
48
52
|
import { ImagePreviewer } from "../../organisms/ImagePreviewer";
|
|
49
53
|
import { InputGroup } from "../../organisms/InputGroup";
|
|
50
54
|
import { ProductImageModal } from "../../organisms/ProductImageModal";
|
|
@@ -54,6 +58,7 @@ import { LinkVariantsModal } from "../../organisms/LinkVariantsModal";
|
|
|
54
58
|
import { UnlinkVariantModal } from "../../organisms/UnlinkVariantModal";
|
|
55
59
|
import { VariantGroupModal } from "../../organisms/VariantGroupModal";
|
|
56
60
|
import { PropagateChangesModal } from "../../organisms/PropagateChangesModal";
|
|
61
|
+
import { soloPropagables } from "../../organisms/PropagateChangesModal/non-propagable";
|
|
57
62
|
import { readPayload } from "../../organisms/LinkVariantsModal/api";
|
|
58
63
|
|
|
59
64
|
import successIcon from "../../../assets/images/genericModal/genericModalCheck.svg";
|
|
@@ -100,62 +105,19 @@ function obtenerDuracionVideoBase64(base64Data) {
|
|
|
100
105
|
});
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
function convertirVideoABase64(url) {
|
|
104
|
-
return new Promise((resolve, reject) => {
|
|
105
|
-
const xhr = new XMLHttpRequest();
|
|
106
|
-
xhr.open("GET", url, true);
|
|
107
|
-
xhr.responseType = "blob";
|
|
108
|
-
xhr.onload = function () {
|
|
109
|
-
if (xhr.status === 200) {
|
|
110
|
-
const blob = xhr.response;
|
|
111
|
-
const reader = new FileReader();
|
|
112
|
-
reader.onloadend = function () {
|
|
113
|
-
resolve(reader.result);
|
|
114
|
-
};
|
|
115
|
-
reader.readAsDataURL(blob);
|
|
116
|
-
} else {
|
|
117
|
-
reject(new Error("Error al cargar el video"));
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
xhr.onerror = function () {
|
|
121
|
-
reject(new Error("Error de red al cargar el video"));
|
|
122
|
-
};
|
|
123
|
-
xhr.send();
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Para videos ya guardados, `getImage` deja en `src` una URL del
|
|
128
|
-
// redimensionador de imágenes que no sirve para mp4. Descargamos el video
|
|
129
|
-
// desde S3 (srcDB) y generamos un thumbnail JPEG para mostrarlo en la galería.
|
|
130
|
-
async function regenerateVideoThumbnails(values) {
|
|
131
|
-
if (!Array.isArray(values)) return values;
|
|
132
|
-
return Promise.all(
|
|
133
|
-
values.map(async (value) => {
|
|
134
|
-
if (value?.ext !== "mp4" || !value?.srcDB) return value;
|
|
135
|
-
try {
|
|
136
|
-
const base64 = await convertirVideoABase64(
|
|
137
|
-
"https://" +
|
|
138
|
-
process.env.REACT_APP_IMAGES_BUCKET +
|
|
139
|
-
".s3.amazonaws.com/" +
|
|
140
|
-
value.srcDB,
|
|
141
|
-
);
|
|
142
|
-
const videoSrc = "data:video/mp4;base64," + base64.split(",")[1];
|
|
143
|
-
const thumbnail = await generateThumbnail(videoSrc, 2);
|
|
144
|
-
return { ...value, src: thumbnail };
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error("Error al generar thumbnail del video:", error);
|
|
147
|
-
return value;
|
|
148
|
-
}
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
108
|
const RetailerProductEditionView = ({
|
|
154
109
|
tabsSections,
|
|
155
110
|
productSelected = {},
|
|
156
111
|
user = {},
|
|
157
112
|
token,
|
|
158
113
|
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,
|
|
159
121
|
}) => {
|
|
160
122
|
const {
|
|
161
123
|
state,
|
|
@@ -164,6 +126,8 @@ const RetailerProductEditionView = ({
|
|
|
164
126
|
saveDescriptions,
|
|
165
127
|
updateImages,
|
|
166
128
|
deleteImages,
|
|
129
|
+
saveAssets,
|
|
130
|
+
registerUploadedAssets,
|
|
167
131
|
handleOnDownloadImages,
|
|
168
132
|
createComment,
|
|
169
133
|
} = useProviderProductEdition();
|
|
@@ -302,6 +266,54 @@ const RetailerProductEditionView = ({
|
|
|
302
266
|
|
|
303
267
|
const [openChangeStatusModal, setOpenChangeStatusModal] = useState(false);
|
|
304
268
|
|
|
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
|
+
|
|
305
317
|
const thumbs = () => {
|
|
306
318
|
const imageInputs = state.images_values?.inputs?.map((e) => ({
|
|
307
319
|
value: e?.id,
|
|
@@ -344,6 +356,7 @@ const RetailerProductEditionView = ({
|
|
|
344
356
|
updateApprovedInputsImages={(item) =>
|
|
345
357
|
console.log("updateApprovedInputsImages", item)
|
|
346
358
|
}
|
|
359
|
+
onSelect={() => handleOnChangeCurrentImage(index)}
|
|
347
360
|
/>
|
|
348
361
|
));
|
|
349
362
|
};
|
|
@@ -610,12 +623,6 @@ const RetailerProductEditionView = ({
|
|
|
610
623
|
);
|
|
611
624
|
});
|
|
612
625
|
|
|
613
|
-
// Regenerar thumbnails de videos guardados para que se muestren.
|
|
614
|
-
const orderedValuesWithThumbs =
|
|
615
|
-
await regenerateVideoThumbnails(orderedValues);
|
|
616
|
-
const filteredValuesWithThumbs =
|
|
617
|
-
await regenerateVideoThumbnails(filteredValues);
|
|
618
|
-
|
|
619
626
|
// Dispatch consolidado
|
|
620
627
|
dispatch({
|
|
621
628
|
type: "SET_SERVICES",
|
|
@@ -624,7 +631,7 @@ const RetailerProductEditionView = ({
|
|
|
624
631
|
descriptions: services[1],
|
|
625
632
|
images: {
|
|
626
633
|
...services[2],
|
|
627
|
-
values:
|
|
634
|
+
values: orderedValues,
|
|
628
635
|
},
|
|
629
636
|
},
|
|
630
637
|
});
|
|
@@ -641,7 +648,12 @@ const RetailerProductEditionView = ({
|
|
|
641
648
|
// Setear inputs del retailer activo directamente (evita cascada)
|
|
642
649
|
dispatch({
|
|
643
650
|
type: "SET_DATASHEETS_INPUTS",
|
|
644
|
-
|
|
651
|
+
// `services[0]?` por lo mismo que el fallback de arriba: la lectura de
|
|
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 ?? {}],
|
|
645
657
|
});
|
|
646
658
|
|
|
647
659
|
dispatch({
|
|
@@ -653,10 +665,23 @@ const RetailerProductEditionView = ({
|
|
|
653
665
|
type: "SET_IMAGES_VALUES",
|
|
654
666
|
payload: {
|
|
655
667
|
...services[2],
|
|
656
|
-
values:
|
|
668
|
+
values: filteredValues,
|
|
657
669
|
},
|
|
658
670
|
});
|
|
659
671
|
|
|
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
|
+
|
|
660
685
|
// Calcular campos requeridos sin valor
|
|
661
686
|
dispatch({
|
|
662
687
|
type: "SET_MISSING_REQUIRED_FIELDS",
|
|
@@ -802,7 +827,8 @@ const RetailerProductEditionView = ({
|
|
|
802
827
|
const canEvaluateAsAuditor =
|
|
803
828
|
state.services_data.every((srv) =>
|
|
804
829
|
["AC", "RP", "RCA"].includes(srv?.status?.replace(/.*\//, "")),
|
|
805
|
-
) &&
|
|
830
|
+
) &&
|
|
831
|
+
([1, 6].includes(user.id_role) || isAuditorAssigned());
|
|
806
832
|
|
|
807
833
|
return canEvaluateAsFacilitator || canEvaluateAsAuditor;
|
|
808
834
|
};
|
|
@@ -823,7 +849,29 @@ const RetailerProductEditionView = ({
|
|
|
823
849
|
return currentStatus;
|
|
824
850
|
};
|
|
825
851
|
|
|
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
|
+
|
|
826
869
|
const handleOnClickSaveImages = async () => {
|
|
870
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
871
|
+
await handleOnSaveAssets();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
827
875
|
if (state.product?.services?.images === 1) {
|
|
828
876
|
/**
|
|
829
877
|
* `updateImages` y NO `saveImageAttrs`.
|
|
@@ -861,7 +909,7 @@ const RetailerProductEditionView = ({
|
|
|
861
909
|
const res = await axios.post(
|
|
862
910
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
|
|
863
911
|
{ articleId: state.product?.id_article },
|
|
864
|
-
{ headers: { Authorization: token } }
|
|
912
|
+
{ headers: { Authorization: token } },
|
|
865
913
|
);
|
|
866
914
|
const data = readPayload(res);
|
|
867
915
|
setFamily(data);
|
|
@@ -937,9 +985,9 @@ const RetailerProductEditionView = ({
|
|
|
937
985
|
result.mode === "self"
|
|
938
986
|
? { ...state.product, parentArticleId: null, variantsCount: 0 }
|
|
939
987
|
: {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
988
|
+
...state.product,
|
|
989
|
+
variantsCount: Math.max((state.product?.variantsCount ?? 1) - 1, 0),
|
|
990
|
+
};
|
|
943
991
|
|
|
944
992
|
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
945
993
|
|
|
@@ -948,7 +996,7 @@ const RetailerProductEditionView = ({
|
|
|
948
996
|
// aplanada de esta pantalla lo dejaría mutilado en el siguiente F5.
|
|
949
997
|
try {
|
|
950
998
|
const guardado = JSON.parse(
|
|
951
|
-
sessionStorage.getItem("productSelected") ?? "null"
|
|
999
|
+
sessionStorage.getItem("productSelected") ?? "null",
|
|
952
1000
|
);
|
|
953
1001
|
|
|
954
1002
|
if (guardado) {
|
|
@@ -958,7 +1006,7 @@ const RetailerProductEditionView = ({
|
|
|
958
1006
|
...guardado,
|
|
959
1007
|
variantsCount: updatedProduct.variantsCount,
|
|
960
1008
|
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
961
|
-
})
|
|
1009
|
+
}),
|
|
962
1010
|
);
|
|
963
1011
|
}
|
|
964
1012
|
} catch (error) {
|
|
@@ -999,7 +1047,7 @@ const RetailerProductEditionView = ({
|
|
|
999
1047
|
const res = await axios.post(
|
|
1000
1048
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/group`,
|
|
1001
1049
|
{ articleId: state.product?.id_article },
|
|
1002
|
-
{ headers: { Authorization: token } }
|
|
1050
|
+
{ headers: { Authorization: token } },
|
|
1003
1051
|
);
|
|
1004
1052
|
const data = readPayload(res);
|
|
1005
1053
|
setVariantGroup(data);
|
|
@@ -1022,7 +1070,7 @@ const RetailerProductEditionView = ({
|
|
|
1022
1070
|
const res = await axios.post(
|
|
1023
1071
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/leave`,
|
|
1024
1072
|
{ articleId: state.product?.id_article },
|
|
1025
|
-
{ headers: { Authorization: token } }
|
|
1073
|
+
{ headers: { Authorization: token } },
|
|
1026
1074
|
);
|
|
1027
1075
|
setVariantGroup(readPayload(res));
|
|
1028
1076
|
setShowGroup(false);
|
|
@@ -1057,7 +1105,7 @@ const RetailerProductEditionView = ({
|
|
|
1057
1105
|
{
|
|
1058
1106
|
params: { filterByIdArticle: articleId, includeFilters: false },
|
|
1059
1107
|
headers: { Authorization: token },
|
|
1060
|
-
}
|
|
1108
|
+
},
|
|
1061
1109
|
);
|
|
1062
1110
|
|
|
1063
1111
|
// read-order-services responde { total, data: [...] }. La vista trae una
|
|
@@ -1065,12 +1113,12 @@ const RetailerProductEditionView = ({
|
|
|
1065
1113
|
// ordenadas por order_id DESC y la primera es la orden más reciente.
|
|
1066
1114
|
const { data } = readPayload(res) ?? {};
|
|
1067
1115
|
const encontrado = (data ?? []).find(
|
|
1068
|
-
(item) => Number(item?.article?.id_article) === Number(articleId)
|
|
1116
|
+
(item) => Number(item?.article?.id_article) === Number(articleId),
|
|
1069
1117
|
);
|
|
1070
1118
|
|
|
1071
1119
|
if (!encontrado) {
|
|
1072
1120
|
setAvisoFamilia(
|
|
1073
|
-
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí."
|
|
1121
|
+
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí.",
|
|
1074
1122
|
);
|
|
1075
1123
|
return;
|
|
1076
1124
|
}
|
|
@@ -1083,7 +1131,7 @@ const RetailerProductEditionView = ({
|
|
|
1083
1131
|
ArticleId: encontrado.article.id_article,
|
|
1084
1132
|
categoryId: encontrado.article.id_article,
|
|
1085
1133
|
product: [encontrado],
|
|
1086
|
-
})
|
|
1134
|
+
}),
|
|
1087
1135
|
);
|
|
1088
1136
|
|
|
1089
1137
|
window.location.assign("/EditProducts");
|
|
@@ -1108,7 +1156,8 @@ const RetailerProductEditionView = ({
|
|
|
1108
1156
|
? {
|
|
1109
1157
|
...state.product,
|
|
1110
1158
|
variantsCount:
|
|
1111
|
-
(state.product?.variantsCount ?? 0) +
|
|
1159
|
+
(state.product?.variantsCount ?? 0) +
|
|
1160
|
+
(result.linked ?? []).length,
|
|
1112
1161
|
}
|
|
1113
1162
|
: {
|
|
1114
1163
|
...state.product,
|
|
@@ -1132,7 +1181,7 @@ const RetailerProductEditionView = ({
|
|
|
1132
1181
|
*/
|
|
1133
1182
|
try {
|
|
1134
1183
|
const guardado = JSON.parse(
|
|
1135
|
-
sessionStorage.getItem("productSelected") ?? "null"
|
|
1184
|
+
sessionStorage.getItem("productSelected") ?? "null",
|
|
1136
1185
|
);
|
|
1137
1186
|
|
|
1138
1187
|
if (guardado) {
|
|
@@ -1142,7 +1191,7 @@ const RetailerProductEditionView = ({
|
|
|
1142
1191
|
...guardado,
|
|
1143
1192
|
variantsCount: updatedProduct.variantsCount,
|
|
1144
1193
|
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
1145
|
-
})
|
|
1194
|
+
}),
|
|
1146
1195
|
);
|
|
1147
1196
|
}
|
|
1148
1197
|
} catch (error) {
|
|
@@ -1184,7 +1233,10 @@ const RetailerProductEditionView = ({
|
|
|
1184
1233
|
`rel_article_description` y la ficha técnica en `article_data`: el endpoint
|
|
1185
1234
|
recibe una lista o la otra.
|
|
1186
1235
|
*/
|
|
1187
|
-
const handleOnRestoreInheritance = async (
|
|
1236
|
+
const handleOnRestoreInheritance = async (
|
|
1237
|
+
attributeId,
|
|
1238
|
+
kind = "attribute",
|
|
1239
|
+
) => {
|
|
1188
1240
|
if (!attributeId || !state.product?.id_article) return;
|
|
1189
1241
|
|
|
1190
1242
|
const esDescripcion = kind === "description";
|
|
@@ -1198,7 +1250,7 @@ const RetailerProductEditionView = ({
|
|
|
1198
1250
|
? { descriptions: [Number(attributeId)] }
|
|
1199
1251
|
: { attributes: [Number(attributeId)] }),
|
|
1200
1252
|
},
|
|
1201
|
-
{ headers: { Authorization: token } }
|
|
1253
|
+
{ headers: { Authorization: token } },
|
|
1202
1254
|
);
|
|
1203
1255
|
|
|
1204
1256
|
const data = readPayload(res);
|
|
@@ -1236,7 +1288,7 @@ const RetailerProductEditionView = ({
|
|
|
1236
1288
|
const retailerId = state.active_retailer?.id_retailer;
|
|
1237
1289
|
const inputs = state.services?.datasheets?.inputs ?? {};
|
|
1238
1290
|
|
|
1239
|
-
|
|
1291
|
+
const cambios = (state.updated_datasheets_inputs ?? []).map((item) => {
|
|
1240
1292
|
const input = inputs[`${item.attributeId}_${retailerId}`];
|
|
1241
1293
|
return {
|
|
1242
1294
|
id: Number(item.attributeId),
|
|
@@ -1244,6 +1296,14 @@ const RetailerProductEditionView = ({
|
|
|
1244
1296
|
value: item.value,
|
|
1245
1297
|
};
|
|
1246
1298
|
});
|
|
1299
|
+
|
|
1300
|
+
/*
|
|
1301
|
+
UPC y SKU se quitan aquí y no dentro del diálogo porque esta lista también
|
|
1302
|
+
decide SI el diálogo se abre. Editar solo el UPC no tiene nada que
|
|
1303
|
+
propagar, así que preguntarlo sería pedirle al usuario una decisión sobre
|
|
1304
|
+
un conjunto vacío.
|
|
1305
|
+
*/
|
|
1306
|
+
return soloPropagables(cambios);
|
|
1247
1307
|
};
|
|
1248
1308
|
|
|
1249
1309
|
const handleOnClickSave = async () => {
|
|
@@ -1325,7 +1385,7 @@ const RetailerProductEditionView = ({
|
|
|
1325
1385
|
puede copiar igual. Si no se miraran, guardar no ofrecería nada.
|
|
1326
1386
|
*/
|
|
1327
1387
|
const companeras = (variantGroup?.members ?? []).filter(
|
|
1328
|
-
(m) => !m.isCurrent
|
|
1388
|
+
(m) => !m.isCurrent,
|
|
1329
1389
|
);
|
|
1330
1390
|
|
|
1331
1391
|
if (hijos.length > 0 || companeras.length > 0) {
|
|
@@ -1337,6 +1397,16 @@ const RetailerProductEditionView = ({
|
|
|
1337
1397
|
break;
|
|
1338
1398
|
}
|
|
1339
1399
|
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
|
+
}
|
|
1340
1410
|
await updateImages(token);
|
|
1341
1411
|
break;
|
|
1342
1412
|
default:
|
|
@@ -1360,7 +1430,98 @@ const RetailerProductEditionView = ({
|
|
|
1360
1430
|
});
|
|
1361
1431
|
};
|
|
1362
1432
|
|
|
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
|
+
|
|
1363
1519
|
const handleOnAskToDeleteImages = () => {
|
|
1520
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
1521
|
+
handleOnAskToDeleteAssets();
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1364
1525
|
if (state.selected_images.length === 0) {
|
|
1365
1526
|
dispatch({
|
|
1366
1527
|
type: "SET_MODAL",
|
|
@@ -2074,7 +2235,25 @@ const RetailerProductEditionView = ({
|
|
|
2074
2235
|
</Container>
|
|
2075
2236
|
);
|
|
2076
2237
|
|
|
2077
|
-
|
|
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
|
+
)
|
|
2078
2257
|
return (
|
|
2079
2258
|
<>
|
|
2080
2259
|
<ProductEditionSkeleton />
|
|
@@ -2094,7 +2273,7 @@ const RetailerProductEditionView = ({
|
|
|
2094
2273
|
changedAttributes={pendingPropagation.cambios}
|
|
2095
2274
|
variants={(family?.variants ?? []).filter((v) => !v.isCurrent)}
|
|
2096
2275
|
groupMembers={(variantGroup?.members ?? []).filter(
|
|
2097
|
-
(m) => !m.isCurrent
|
|
2276
|
+
(m) => !m.isCurrent,
|
|
2098
2277
|
)}
|
|
2099
2278
|
groupCode={variantGroup?.group ?? null}
|
|
2100
2279
|
saving={state.saving}
|
|
@@ -2311,8 +2490,18 @@ const RetailerProductEditionView = ({
|
|
|
2311
2490
|
<div className="data-container">
|
|
2312
2491
|
<div className="image-data-panel">
|
|
2313
2492
|
<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
|
+
*/
|
|
2314
2503
|
activeImage={
|
|
2315
|
-
state.images_values
|
|
2504
|
+
state.images_values?.values?.[state.current_image] ?? {}
|
|
2316
2505
|
}
|
|
2317
2506
|
imagesArray={state.images_values || []}
|
|
2318
2507
|
setActiveImage={handleOnChangeCurrentImage}
|
|
@@ -2618,7 +2807,13 @@ const RetailerProductEditionView = ({
|
|
|
2618
2807
|
setShowVersionSelector={setShowVersionSelector}
|
|
2619
2808
|
// setSelectedImages={setSelectedImages} prop pasada asi: FullTabsMenu -> TabsMenu Pero en TabsMenu no se espera ninguna prop setSelectedImages
|
|
2620
2809
|
// actions
|
|
2621
|
-
|
|
2810
|
+
// El botón actúa sobre la bandeja visible: en activos descargaba
|
|
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
|
+
}
|
|
2622
2817
|
askToDeleteImages={handleOnAskToDeleteImages}
|
|
2623
2818
|
showSaveButton={isAuditorAssigned() || isUserAssignedToService()}
|
|
2624
2819
|
onClickSave={handleOnClickSave}
|
|
@@ -2696,7 +2891,36 @@ const RetailerProductEditionView = ({
|
|
|
2696
2891
|
{state.active_tab === "Imágenes" &&
|
|
2697
2892
|
(state.product?.images_status !== "NS" ? (
|
|
2698
2893
|
<>
|
|
2699
|
-
{
|
|
2894
|
+
{enableDigitalAssets && (
|
|
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 && (
|
|
2700
2924
|
<GalleryHeader
|
|
2701
2925
|
checkAll={state.all_image_checked} // Determina si el checkbox "Seleccionar todo" está marcado
|
|
2702
2926
|
setCheckAll={handleOnSetCheckAll} // Toggler de true o false para el checkbox "Seleccionar todo"
|
|
@@ -2704,74 +2928,76 @@ const RetailerProductEditionView = ({
|
|
|
2704
2928
|
// shotThd={shotThd} // No se usa?
|
|
2705
2929
|
/>
|
|
2706
2930
|
)}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
<div
|
|
2712
|
-
{...getRootProps({
|
|
2713
|
-
className: `dropzone ${
|
|
2714
|
-
isDragActive ? "drag-active" : ""
|
|
2715
|
-
}`,
|
|
2716
|
-
})}
|
|
2931
|
+
{currentTray === TRAYS.IMAGES && (
|
|
2932
|
+
<section
|
|
2933
|
+
className="container"
|
|
2934
|
+
style={{ position: "relative" }}
|
|
2717
2935
|
>
|
|
2718
|
-
<input {...getInputProps()} />
|
|
2719
|
-
{isDragActive && (
|
|
2720
|
-
<div className="drag-overlay">
|
|
2721
|
-
<p>Suelta las imágenes aquí</p>
|
|
2722
|
-
</div>
|
|
2723
|
-
)}
|
|
2724
|
-
<aside>{thumbs()}</aside>
|
|
2725
|
-
</div>
|
|
2726
|
-
{state.images_values?.values.length === 0 && (
|
|
2727
2936
|
<div
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
textAlign: "center",
|
|
2734
|
-
padding: "40px",
|
|
2735
|
-
width: "80%",
|
|
2736
|
-
maxWidth: "500px",
|
|
2737
|
-
fontFamily: "Arial, sans-serif",
|
|
2738
|
-
}}
|
|
2937
|
+
{...getRootProps({
|
|
2938
|
+
className: `dropzone ${
|
|
2939
|
+
isDragActive ? "drag-active" : ""
|
|
2940
|
+
}`,
|
|
2941
|
+
})}
|
|
2739
2942
|
>
|
|
2740
|
-
<
|
|
2943
|
+
<input {...getInputProps()} />
|
|
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
|
|
2741
2953
|
style={{
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2954
|
+
position: "absolute",
|
|
2955
|
+
top: "50%",
|
|
2956
|
+
left: "50%",
|
|
2957
|
+
transform: "translate(-50%, -50%)",
|
|
2958
|
+
textAlign: "center",
|
|
2959
|
+
padding: "40px",
|
|
2960
|
+
width: "80%",
|
|
2961
|
+
maxWidth: "500px",
|
|
2962
|
+
fontFamily: "Arial, sans-serif",
|
|
2745
2963
|
}}
|
|
2746
2964
|
>
|
|
2747
|
-
Este producto no tiene imágenes
|
|
2748
|
-
</p>
|
|
2749
|
-
{isAuditorAssigned() ||
|
|
2750
|
-
isUserAssignedToService() ? (
|
|
2751
2965
|
<p
|
|
2752
2966
|
style={{
|
|
2753
|
-
fontSize: "
|
|
2754
|
-
color: "#
|
|
2755
|
-
marginBottom: "
|
|
2967
|
+
fontSize: "18px",
|
|
2968
|
+
color: "#666",
|
|
2969
|
+
marginBottom: "16px",
|
|
2756
2970
|
}}
|
|
2757
2971
|
>
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2972
|
+
Este producto no tiene imágenes
|
|
2973
|
+
</p>
|
|
2974
|
+
{isAuditorAssigned() ||
|
|
2975
|
+
isUserAssignedToService() ? (
|
|
2976
|
+
<p
|
|
2761
2977
|
style={{
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2978
|
+
fontSize: "14px",
|
|
2979
|
+
color: "#999",
|
|
2980
|
+
marginBottom: "20px",
|
|
2765
2981
|
}}
|
|
2766
2982
|
>
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2983
|
+
Arrastra las imágenes aquí o{" "}
|
|
2984
|
+
<span
|
|
2985
|
+
onClick={open}
|
|
2986
|
+
style={{
|
|
2987
|
+
color: "#007bff",
|
|
2988
|
+
cursor: "pointer",
|
|
2989
|
+
textDecoration: "underline",
|
|
2990
|
+
}}
|
|
2991
|
+
>
|
|
2992
|
+
haz clic para abrir el explorador de
|
|
2993
|
+
archivos
|
|
2994
|
+
</span>
|
|
2995
|
+
</p>
|
|
2996
|
+
) : null}
|
|
2997
|
+
</div>
|
|
2998
|
+
)}
|
|
2999
|
+
</section>
|
|
3000
|
+
)}
|
|
2775
3001
|
</>
|
|
2776
3002
|
) : (
|
|
2777
3003
|
<ScreenHeader
|