contentoh-components-library 21.6.55 → 21.6.57
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/AsignationOption/index.js +1 -0
- package/dist/components/atoms/AsignationOption/styles.js +1 -1
- 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/molecules/RetailerSelector/RetailerSelector.stories.js +23 -2
- package/dist/components/molecules/RetailerSelector/index.js +7 -2
- package/dist/components/molecules/RetailerSelectorv2/index.js +6 -2
- package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +235 -0
- package/dist/components/organisms/DigitalAssetsTray/index.js +219 -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/pages/CreatorsProductEdition/CreatorsProductEditionView.js +905 -0
- package/dist/components/pages/CreatorsProductEdition/constants.js +19 -0
- package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2967 -0
- package/dist/components/pages/CreatorsProductEdition/index.js +33 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +537 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +251 -0
- package/dist/components/pages/CreatorsProductEdition/sections/CommentBox.js +311 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ImagePanel.js +421 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ImagesSection.js +473 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ProductHeader.js +379 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ProductMedia.js +138 -0
- package/dist/components/pages/CreatorsProductEdition/sections/RetailerMenu.js +162 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ServiceActions.js +542 -0
- package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +50 -0
- package/dist/components/pages/CreatorsProductEdition/sections/TabsBar.js +247 -0
- package/dist/components/pages/CreatorsProductEdition/sections/VariantsPanel.js +389 -0
- package/dist/components/pages/CreatorsProductEdition/sections/tokens.js +90 -0
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +168 -0
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +74 -0
- package/dist/components/pages/CreatorsProductEdition/styles.js +46 -0
- 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 +606 -425
- package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/dist/global-files/data.js +16 -3
- package/dist/global-files/digital-assets/assetTypes.js +517 -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 +202 -0
- package/dist/global-files/digital-assets/useDigitalAssets.js +535 -0
- package/dist/index.js +142 -90
- package/docs/activos-digitales.md +273 -0
- package/package.json +1 -1
- package/src/components/atoms/AsignationOption/index.js +1 -1
- package/src/components/atoms/AsignationOption/styles.js +9 -0
- 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 +163 -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/molecules/RetailerSelector/RetailerSelector.stories.js +15 -1
- package/src/components/molecules/RetailerSelector/index.js +8 -1
- package/src/components/molecules/RetailerSelectorv2/index.js +7 -1
- package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +164 -0
- package/src/components/organisms/DigitalAssetsTray/index.js +193 -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/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 +356 -139
- package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/src/global-files/data.js +17 -2
- package/src/global-files/digital-assets/assetTypes.js +451 -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 +124 -0
- package/src/global-files/digital-assets/useDigitalAssets.js +340 -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";
|
|
@@ -101,62 +105,19 @@ function obtenerDuracionVideoBase64(base64Data) {
|
|
|
101
105
|
});
|
|
102
106
|
}
|
|
103
107
|
|
|
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
|
-
|
|
154
108
|
const RetailerProductEditionView = ({
|
|
155
109
|
tabsSections,
|
|
156
110
|
productSelected = {},
|
|
157
111
|
user = {},
|
|
158
112
|
token,
|
|
159
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,
|
|
160
121
|
}) => {
|
|
161
122
|
const {
|
|
162
123
|
state,
|
|
@@ -165,6 +126,8 @@ const RetailerProductEditionView = ({
|
|
|
165
126
|
saveDescriptions,
|
|
166
127
|
updateImages,
|
|
167
128
|
deleteImages,
|
|
129
|
+
saveAssets,
|
|
130
|
+
registerUploadedAssets,
|
|
168
131
|
handleOnDownloadImages,
|
|
169
132
|
createComment,
|
|
170
133
|
} = useProviderProductEdition();
|
|
@@ -304,6 +267,54 @@ const RetailerProductEditionView = ({
|
|
|
304
267
|
|
|
305
268
|
const [openChangeStatusModal, setOpenChangeStatusModal] = useState(false);
|
|
306
269
|
|
|
270
|
+
/*
|
|
271
|
+
Bandeja de activos digitales. La lista vive en el reducer de la ficha
|
|
272
|
+
(`state.assets_values`), igual que las imágenes, para que el guardado del
|
|
273
|
+
contexto pueda leerla. El hook solo aporta validación y ciclo de vida.
|
|
274
|
+
|
|
275
|
+
Sigue vacía porque todavía no hay de dónde leerla: cuando exista el endpoint
|
|
276
|
+
se llama a `replaceAll` desde `loadData` y el resto de la pantalla no cambia.
|
|
277
|
+
*/
|
|
278
|
+
const [assetNotices, setAssetNotices] = useState([]);
|
|
279
|
+
const digitalAssets = useDigitalAssets({
|
|
280
|
+
assets: state.assets_values,
|
|
281
|
+
dispatch,
|
|
282
|
+
notices: assetNotices,
|
|
283
|
+
setNotices: setAssetNotices,
|
|
284
|
+
// Con los dos, la subida a S3 arranca sola al soltar los archivos. Sin
|
|
285
|
+
// ellos —en Storybook, por ejemplo— la bandeja funciona igual pero no sube.
|
|
286
|
+
articleId: state.product?.id_article ?? null,
|
|
287
|
+
token,
|
|
288
|
+
// Al terminar la subida el activo queda registrado solo, sin esperar al
|
|
289
|
+
// botón de guardar: con la barra llena, nadie supone que falta un paso.
|
|
290
|
+
onUploaded: (uploaded) => registerUploadedAssets(token, uploaded),
|
|
291
|
+
});
|
|
292
|
+
const [activeTray, setActiveTray] = useState(TRAYS.IMAGES);
|
|
293
|
+
const currentTray = enableDigitalAssets ? activeTray : TRAYS.IMAGES;
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
De qué artículo se cargaron ya los activos.
|
|
297
|
+
|
|
298
|
+
`loadData` se vuelve a ejecutar cada vez que cambia la cadena activa, pero los
|
|
299
|
+
activos son generales: no dependen de la cadena ni de la versión, así que la
|
|
300
|
+
respuesta sería idéntica. Volver a cargarlos borraría los archivos que el
|
|
301
|
+
usuario acaba de soltar y todavía no guarda —sin aviso y sin forma de
|
|
302
|
+
recuperarlos—. Por eso se cargan una sola vez por artículo.
|
|
303
|
+
*/
|
|
304
|
+
const assetsLoadedFor = useRef(null);
|
|
305
|
+
|
|
306
|
+
const handleOnSetSelectedAssets = (assets) =>
|
|
307
|
+
dispatch({
|
|
308
|
+
type: "SET_SELECTED_ASSETS",
|
|
309
|
+
payload: assets,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const handleOnSetAllAssetsChecked = (isChecked) =>
|
|
313
|
+
dispatch({
|
|
314
|
+
type: "TOGGLE_CHECK_ALL_ASSETS",
|
|
315
|
+
payload: isChecked,
|
|
316
|
+
});
|
|
317
|
+
|
|
307
318
|
const thumbs = () => {
|
|
308
319
|
const imageInputs = state.images_values?.inputs?.map((e) => ({
|
|
309
320
|
value: e?.id,
|
|
@@ -346,6 +357,7 @@ const RetailerProductEditionView = ({
|
|
|
346
357
|
updateApprovedInputsImages={(item) =>
|
|
347
358
|
console.log("updateApprovedInputsImages", item)
|
|
348
359
|
}
|
|
360
|
+
onSelect={() => handleOnChangeCurrentImage(index)}
|
|
349
361
|
/>
|
|
350
362
|
));
|
|
351
363
|
};
|
|
@@ -614,12 +626,6 @@ const RetailerProductEditionView = ({
|
|
|
614
626
|
);
|
|
615
627
|
});
|
|
616
628
|
|
|
617
|
-
// Regenerar thumbnails de videos guardados para que se muestren.
|
|
618
|
-
const orderedValuesWithThumbs =
|
|
619
|
-
await regenerateVideoThumbnails(orderedValues);
|
|
620
|
-
const filteredValuesWithThumbs =
|
|
621
|
-
await regenerateVideoThumbnails(filteredValues);
|
|
622
|
-
|
|
623
629
|
// Dispatch consolidado
|
|
624
630
|
dispatch({
|
|
625
631
|
type: "SET_SERVICES",
|
|
@@ -628,7 +634,7 @@ const RetailerProductEditionView = ({
|
|
|
628
634
|
descriptions: services[1],
|
|
629
635
|
images: {
|
|
630
636
|
...services[2],
|
|
631
|
-
values:
|
|
637
|
+
values: orderedValues,
|
|
632
638
|
},
|
|
633
639
|
},
|
|
634
640
|
});
|
|
@@ -645,7 +651,12 @@ const RetailerProductEditionView = ({
|
|
|
645
651
|
// Setear inputs del retailer activo directamente (evita cascada)
|
|
646
652
|
dispatch({
|
|
647
653
|
type: "SET_DATASHEETS_INPUTS",
|
|
648
|
-
|
|
654
|
+
// `services[0]?` por lo mismo que el fallback de arriba: la lectura de
|
|
655
|
+
// fichas puede volver sin datos para esa cadena y categoría. Arriba ya
|
|
656
|
+
// se contemplaba; aquí no, y como este dispatch va ANTES de
|
|
657
|
+
// SET_IMAGES_VALUES, reventar aquí dejaba la ficha cargando para
|
|
658
|
+
// siempre — con las imágenes ya traídas y sin pintar.
|
|
659
|
+
payload: [datasheetsActiveRetailer, services[0]?.inputs ?? {}],
|
|
649
660
|
});
|
|
650
661
|
|
|
651
662
|
dispatch({
|
|
@@ -657,10 +668,23 @@ const RetailerProductEditionView = ({
|
|
|
657
668
|
type: "SET_IMAGES_VALUES",
|
|
658
669
|
payload: {
|
|
659
670
|
...services[2],
|
|
660
|
-
values:
|
|
671
|
+
values: filteredValues,
|
|
661
672
|
},
|
|
662
673
|
});
|
|
663
674
|
|
|
675
|
+
/*
|
|
676
|
+
Activos digitales. `services[2].assets` todavía no existe en la
|
|
677
|
+
respuesta del backend: hasta que exista, esto no hace nada, y el día que
|
|
678
|
+
llegue la bandeja se llena sola sin tocar nada más.
|
|
679
|
+
*/
|
|
680
|
+
if (
|
|
681
|
+
Array.isArray(services[2].assets) &&
|
|
682
|
+
assetsLoadedFor.current !== id_article
|
|
683
|
+
) {
|
|
684
|
+
assetsLoadedFor.current = id_article;
|
|
685
|
+
digitalAssets.replaceAll(services[2].assets);
|
|
686
|
+
}
|
|
687
|
+
|
|
664
688
|
// Calcular campos requeridos sin valor
|
|
665
689
|
dispatch({
|
|
666
690
|
type: "SET_MISSING_REQUIRED_FIELDS",
|
|
@@ -810,7 +834,8 @@ const RetailerProductEditionView = ({
|
|
|
810
834
|
const canEvaluateAsAuditor =
|
|
811
835
|
state.services_data.every((srv) =>
|
|
812
836
|
["AC", "RP", "RCA"].includes(srv?.status?.replace(/.*\//, "")),
|
|
813
|
-
) &&
|
|
837
|
+
) &&
|
|
838
|
+
([1, 6].includes(user.id_role) || isAuditorAssigned());
|
|
814
839
|
|
|
815
840
|
const canEvaluateAC =
|
|
816
841
|
services.length > 0 &&
|
|
@@ -836,7 +861,29 @@ const RetailerProductEditionView = ({
|
|
|
836
861
|
return currentStatus;
|
|
837
862
|
};
|
|
838
863
|
|
|
864
|
+
/*
|
|
865
|
+
Guarda los activos y recarga si algo se escribió.
|
|
866
|
+
|
|
867
|
+
La recarga no es opcional: un activo recién creado sigue en memoria con su
|
|
868
|
+
id temporal y sin el id de la base. Sin volver a leer, el siguiente guardado
|
|
869
|
+
lo mandaría otra vez como nuevo y se duplicaría. Poner el ref en `null` es
|
|
870
|
+
lo que permite que `loadData` lo vuelva a traer —normalmente los activos se
|
|
871
|
+
cargan una sola vez por artículo—.
|
|
872
|
+
*/
|
|
873
|
+
const handleOnSaveAssets = async () => {
|
|
874
|
+
const guardado = await saveAssets(token);
|
|
875
|
+
if (!guardado) return;
|
|
876
|
+
|
|
877
|
+
assetsLoadedFor.current = null;
|
|
878
|
+
await loadData(false);
|
|
879
|
+
};
|
|
880
|
+
|
|
839
881
|
const handleOnClickSaveImages = async () => {
|
|
882
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
883
|
+
await handleOnSaveAssets();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
840
887
|
if (state.product?.services?.images === 1) {
|
|
841
888
|
/**
|
|
842
889
|
* `updateImages` y NO `saveImageAttrs`.
|
|
@@ -874,7 +921,7 @@ const RetailerProductEditionView = ({
|
|
|
874
921
|
const res = await axios.post(
|
|
875
922
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
|
|
876
923
|
{ articleId: state.product?.id_article },
|
|
877
|
-
{ headers: { Authorization: token } }
|
|
924
|
+
{ headers: { Authorization: token } },
|
|
878
925
|
);
|
|
879
926
|
const data = readPayload(res);
|
|
880
927
|
setFamily(data);
|
|
@@ -964,9 +1011,9 @@ const RetailerProductEditionView = ({
|
|
|
964
1011
|
result.mode === "self"
|
|
965
1012
|
? { ...state.product, parentArticleId: null, variantsCount: 0 }
|
|
966
1013
|
: {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1014
|
+
...state.product,
|
|
1015
|
+
variantsCount: Math.max((state.product?.variantsCount ?? 1) - 1, 0),
|
|
1016
|
+
};
|
|
970
1017
|
|
|
971
1018
|
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
972
1019
|
|
|
@@ -975,7 +1022,7 @@ const RetailerProductEditionView = ({
|
|
|
975
1022
|
// aplanada de esta pantalla lo dejaría mutilado en el siguiente F5.
|
|
976
1023
|
try {
|
|
977
1024
|
const guardado = JSON.parse(
|
|
978
|
-
sessionStorage.getItem("productSelected") ?? "null"
|
|
1025
|
+
sessionStorage.getItem("productSelected") ?? "null",
|
|
979
1026
|
);
|
|
980
1027
|
|
|
981
1028
|
if (guardado) {
|
|
@@ -985,7 +1032,7 @@ const RetailerProductEditionView = ({
|
|
|
985
1032
|
...guardado,
|
|
986
1033
|
variantsCount: updatedProduct.variantsCount,
|
|
987
1034
|
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
988
|
-
})
|
|
1035
|
+
}),
|
|
989
1036
|
);
|
|
990
1037
|
}
|
|
991
1038
|
} catch (error) {
|
|
@@ -1026,7 +1073,7 @@ const RetailerProductEditionView = ({
|
|
|
1026
1073
|
const res = await axios.post(
|
|
1027
1074
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/group`,
|
|
1028
1075
|
{ articleId: state.product?.id_article },
|
|
1029
|
-
{ headers: { Authorization: token } }
|
|
1076
|
+
{ headers: { Authorization: token } },
|
|
1030
1077
|
);
|
|
1031
1078
|
const data = readPayload(res);
|
|
1032
1079
|
setVariantGroup(data);
|
|
@@ -1049,7 +1096,7 @@ const RetailerProductEditionView = ({
|
|
|
1049
1096
|
const res = await axios.post(
|
|
1050
1097
|
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/leave`,
|
|
1051
1098
|
{ articleId: state.product?.id_article },
|
|
1052
|
-
{ headers: { Authorization: token } }
|
|
1099
|
+
{ headers: { Authorization: token } },
|
|
1053
1100
|
);
|
|
1054
1101
|
setVariantGroup(readPayload(res));
|
|
1055
1102
|
setShowGroup(false);
|
|
@@ -1084,7 +1131,7 @@ const RetailerProductEditionView = ({
|
|
|
1084
1131
|
{
|
|
1085
1132
|
params: { filterByIdArticle: articleId, includeFilters: false },
|
|
1086
1133
|
headers: { Authorization: token },
|
|
1087
|
-
}
|
|
1134
|
+
},
|
|
1088
1135
|
);
|
|
1089
1136
|
|
|
1090
1137
|
// read-order-services responde { total, data: [...] }. La vista trae una
|
|
@@ -1092,12 +1139,12 @@ const RetailerProductEditionView = ({
|
|
|
1092
1139
|
// ordenadas por order_id DESC y la primera es la orden más reciente.
|
|
1093
1140
|
const { data } = readPayload(res) ?? {};
|
|
1094
1141
|
const encontrado = (data ?? []).find(
|
|
1095
|
-
(item) => Number(item?.article?.id_article) === Number(articleId)
|
|
1142
|
+
(item) => Number(item?.article?.id_article) === Number(articleId),
|
|
1096
1143
|
);
|
|
1097
1144
|
|
|
1098
1145
|
if (!encontrado) {
|
|
1099
1146
|
setAvisoFamilia(
|
|
1100
|
-
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí."
|
|
1147
|
+
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí.",
|
|
1101
1148
|
);
|
|
1102
1149
|
return;
|
|
1103
1150
|
}
|
|
@@ -1110,7 +1157,7 @@ const RetailerProductEditionView = ({
|
|
|
1110
1157
|
ArticleId: encontrado.article.id_article,
|
|
1111
1158
|
categoryId: encontrado.article.id_article,
|
|
1112
1159
|
product: [encontrado],
|
|
1113
|
-
})
|
|
1160
|
+
}),
|
|
1114
1161
|
);
|
|
1115
1162
|
|
|
1116
1163
|
window.location.assign("/EditProducts");
|
|
@@ -1135,7 +1182,8 @@ const RetailerProductEditionView = ({
|
|
|
1135
1182
|
? {
|
|
1136
1183
|
...state.product,
|
|
1137
1184
|
variantsCount:
|
|
1138
|
-
(state.product?.variantsCount ?? 0) +
|
|
1185
|
+
(state.product?.variantsCount ?? 0) +
|
|
1186
|
+
(result.linked ?? []).length,
|
|
1139
1187
|
}
|
|
1140
1188
|
: {
|
|
1141
1189
|
...state.product,
|
|
@@ -1159,7 +1207,7 @@ const RetailerProductEditionView = ({
|
|
|
1159
1207
|
*/
|
|
1160
1208
|
try {
|
|
1161
1209
|
const guardado = JSON.parse(
|
|
1162
|
-
sessionStorage.getItem("productSelected") ?? "null"
|
|
1210
|
+
sessionStorage.getItem("productSelected") ?? "null",
|
|
1163
1211
|
);
|
|
1164
1212
|
|
|
1165
1213
|
if (guardado) {
|
|
@@ -1169,7 +1217,7 @@ const RetailerProductEditionView = ({
|
|
|
1169
1217
|
...guardado,
|
|
1170
1218
|
variantsCount: updatedProduct.variantsCount,
|
|
1171
1219
|
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
1172
|
-
})
|
|
1220
|
+
}),
|
|
1173
1221
|
);
|
|
1174
1222
|
}
|
|
1175
1223
|
} catch (error) {
|
|
@@ -1211,7 +1259,10 @@ const RetailerProductEditionView = ({
|
|
|
1211
1259
|
`rel_article_description` y la ficha técnica en `article_data`: el endpoint
|
|
1212
1260
|
recibe una lista o la otra.
|
|
1213
1261
|
*/
|
|
1214
|
-
const handleOnRestoreInheritance = async (
|
|
1262
|
+
const handleOnRestoreInheritance = async (
|
|
1263
|
+
attributeId,
|
|
1264
|
+
kind = "attribute",
|
|
1265
|
+
) => {
|
|
1215
1266
|
if (!attributeId || !state.product?.id_article) return;
|
|
1216
1267
|
|
|
1217
1268
|
const esDescripcion = kind === "description";
|
|
@@ -1225,7 +1276,7 @@ const RetailerProductEditionView = ({
|
|
|
1225
1276
|
? { descriptions: [Number(attributeId)] }
|
|
1226
1277
|
: { attributes: [Number(attributeId)] }),
|
|
1227
1278
|
},
|
|
1228
|
-
{ headers: { Authorization: token } }
|
|
1279
|
+
{ headers: { Authorization: token } },
|
|
1229
1280
|
);
|
|
1230
1281
|
|
|
1231
1282
|
const data = readPayload(res);
|
|
@@ -1428,7 +1479,7 @@ const RetailerProductEditionView = ({
|
|
|
1428
1479
|
puede copiar igual. Si no se miraran, guardar no ofrecería nada.
|
|
1429
1480
|
*/
|
|
1430
1481
|
const companeras = (variantGroup?.members ?? []).filter(
|
|
1431
|
-
(m) => !m.isCurrent
|
|
1482
|
+
(m) => !m.isCurrent,
|
|
1432
1483
|
);
|
|
1433
1484
|
|
|
1434
1485
|
// Un hijo tampoco puede estar agrupado desde 2026-09-25, así que para
|
|
@@ -1442,6 +1493,16 @@ const RetailerProductEditionView = ({
|
|
|
1442
1493
|
break;
|
|
1443
1494
|
}
|
|
1444
1495
|
case "Imágenes":
|
|
1496
|
+
/*
|
|
1497
|
+
Se guarda la bandeja que el usuario tiene a la vista. Guardar las dos
|
|
1498
|
+
de golpe haría que, estando en activos, `updateImages` enviara también
|
|
1499
|
+
los cambios de imágenes que el usuario quizá no dio por terminados —y
|
|
1500
|
+
al revés—. El botón guarda lo que se está viendo.
|
|
1501
|
+
*/
|
|
1502
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
1503
|
+
await handleOnSaveAssets();
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1445
1506
|
await updateImages(token);
|
|
1446
1507
|
break;
|
|
1447
1508
|
default:
|
|
@@ -1465,7 +1526,98 @@ const RetailerProductEditionView = ({
|
|
|
1465
1526
|
});
|
|
1466
1527
|
};
|
|
1467
1528
|
|
|
1529
|
+
/*
|
|
1530
|
+
Descarga de activos: los seleccionados, o todos si no hay selección — el
|
|
1531
|
+
mismo criterio que usa la descarga de imágenes.
|
|
1532
|
+
|
|
1533
|
+
Solo se descarga lo que ya está en S3. Un activo recién soltado vive como
|
|
1534
|
+
referencia al archivo del disco del usuario: "descargarlo" le devolvería el
|
|
1535
|
+
archivo que él mismo acaba de elegir, lo cual no tiene sentido.
|
|
1536
|
+
*/
|
|
1537
|
+
const handleOnDownloadAssets = () => {
|
|
1538
|
+
const seleccion = state.selected_assets.length
|
|
1539
|
+
? state.selected_assets
|
|
1540
|
+
: state.assets_values;
|
|
1541
|
+
|
|
1542
|
+
const descargables = seleccion.filter((asset) => asset.id && asset.url);
|
|
1543
|
+
|
|
1544
|
+
if (descargables.length === 0) {
|
|
1545
|
+
dispatch({
|
|
1546
|
+
type: "SET_MODAL",
|
|
1547
|
+
payload: {
|
|
1548
|
+
show: true,
|
|
1549
|
+
title: "Nada que descargar",
|
|
1550
|
+
message:
|
|
1551
|
+
"Los activos seleccionados todavía no se han guardado. Guárdalos primero.",
|
|
1552
|
+
image: warningIcon,
|
|
1553
|
+
},
|
|
1554
|
+
});
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
descargables.forEach((asset) => saveAs(asset.url, asset.name));
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
/*
|
|
1562
|
+
Borrado en lote de activos.
|
|
1563
|
+
|
|
1564
|
+
A diferencia de las imágenes, que se borran contra el backend en el momento,
|
|
1565
|
+
los activos se quitan de la bandeja y su baja viaja en el mismo PUT del
|
|
1566
|
+
guardado (`deleteAssets`). Es lo que permite al usuario deshacerse de un
|
|
1567
|
+
borrado por error: basta con no guardar. El mensaje lo dice, para que nadie
|
|
1568
|
+
se quede pensando que ya se aplicó.
|
|
1569
|
+
*/
|
|
1570
|
+
const handleOnAskToDeleteAssets = () => {
|
|
1571
|
+
if (state.selected_assets.length === 0) {
|
|
1572
|
+
dispatch({
|
|
1573
|
+
type: "SET_MODAL",
|
|
1574
|
+
payload: {
|
|
1575
|
+
show: true,
|
|
1576
|
+
title: "Eliminar activos digitales",
|
|
1577
|
+
message: "No has seleccionado ningún activo para eliminar.",
|
|
1578
|
+
image: warningIcon,
|
|
1579
|
+
},
|
|
1580
|
+
});
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const seleccionados = state.selected_assets;
|
|
1585
|
+
|
|
1586
|
+
dispatch({
|
|
1587
|
+
type: "SET_MODAL",
|
|
1588
|
+
payload: {
|
|
1589
|
+
show: true,
|
|
1590
|
+
title: "Eliminar activos digitales",
|
|
1591
|
+
message: `Se quitarán ${seleccionados.length} activo(s) de la bandeja. El borrado se aplica al guardar.`,
|
|
1592
|
+
image: warningIcon,
|
|
1593
|
+
buttons: [
|
|
1594
|
+
{
|
|
1595
|
+
text: "Cancelar",
|
|
1596
|
+
buttonType: "general-white-button",
|
|
1597
|
+
action: () =>
|
|
1598
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } }),
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
text: "Eliminar",
|
|
1602
|
+
buttonType: "general-button-default",
|
|
1603
|
+
action: () => {
|
|
1604
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } });
|
|
1605
|
+
seleccionados.forEach((asset) =>
|
|
1606
|
+
digitalAssets.removeAsset(asset),
|
|
1607
|
+
);
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1610
|
+
],
|
|
1611
|
+
},
|
|
1612
|
+
});
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1468
1615
|
const handleOnAskToDeleteImages = () => {
|
|
1616
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
1617
|
+
handleOnAskToDeleteAssets();
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1469
1621
|
if (state.selected_images.length === 0) {
|
|
1470
1622
|
dispatch({
|
|
1471
1623
|
type: "SET_MODAL",
|
|
@@ -2198,7 +2350,25 @@ const RetailerProductEditionView = ({
|
|
|
2198
2350
|
</Container>
|
|
2199
2351
|
);
|
|
2200
2352
|
|
|
2201
|
-
|
|
2353
|
+
/*
|
|
2354
|
+
`images_values` entra al guard junto a `services` y `product`.
|
|
2355
|
+
|
|
2356
|
+
React 17 no agrupa los dispatch que van después de un `await`: cada uno
|
|
2357
|
+
re-renderiza al instante. En `loadData` el orden es SET_SERVICES →
|
|
2358
|
+
…_INPUTS → SET_IMAGES_VALUES, así que había renders intermedios con
|
|
2359
|
+
`services` ya puesto —el guard los dejaba pasar— y el resto todavía en null.
|
|
2360
|
+
El cuerpo de abajo lee `datasheets_inputs[1]`, `descriptions_inputs[0]` y
|
|
2361
|
+
`images_values.values` sin protección, y tumbaba la pantalla entera.
|
|
2362
|
+
|
|
2363
|
+
Se comprueba `images_values` y no cada uno: es el ÚLTIMO que `loadData`
|
|
2364
|
+
despacha, así que si ya está, los anteriores también.
|
|
2365
|
+
*/
|
|
2366
|
+
if (
|
|
2367
|
+
state.loading ||
|
|
2368
|
+
!state.services ||
|
|
2369
|
+
!state.product ||
|
|
2370
|
+
!state.images_values
|
|
2371
|
+
)
|
|
2202
2372
|
return (
|
|
2203
2373
|
<>
|
|
2204
2374
|
<ProductEditionSkeleton />
|
|
@@ -2223,7 +2393,7 @@ const RetailerProductEditionView = ({
|
|
|
2223
2393
|
: (family?.variants ?? []).filter((v) => !v.isCurrent)
|
|
2224
2394
|
}
|
|
2225
2395
|
groupMembers={(variantGroup?.members ?? []).filter(
|
|
2226
|
-
(m) => !m.isCurrent
|
|
2396
|
+
(m) => !m.isCurrent,
|
|
2227
2397
|
)}
|
|
2228
2398
|
groupCode={variantGroup?.group ?? null}
|
|
2229
2399
|
saving={state.saving}
|
|
@@ -2460,8 +2630,18 @@ const RetailerProductEditionView = ({
|
|
|
2460
2630
|
<div className="data-container">
|
|
2461
2631
|
<div className="image-data-panel">
|
|
2462
2632
|
<ImagePreviewer
|
|
2633
|
+
/*
|
|
2634
|
+
`images_values` puede ser null en pleno render.
|
|
2635
|
+
|
|
2636
|
+
React 17 no agrupa los dispatch que van después de un `await`:
|
|
2637
|
+
cada uno re-renderiza al instante. En `loadData`, `SET_SERVICES`
|
|
2638
|
+
se despacha antes que `SET_IMAGES_VALUES`, así que hay un render
|
|
2639
|
+
en el que `services` ya existe —y por eso el guard de arriba deja
|
|
2640
|
+
pasar— pero las imágenes todavía no. Sin las interrogaciones,
|
|
2641
|
+
ese render intermedio tumba la pantalla entera.
|
|
2642
|
+
*/
|
|
2463
2643
|
activeImage={
|
|
2464
|
-
state.images_values
|
|
2644
|
+
state.images_values?.values?.[state.current_image] ?? {}
|
|
2465
2645
|
}
|
|
2466
2646
|
imagesArray={state.images_values || []}
|
|
2467
2647
|
setActiveImage={handleOnChangeCurrentImage}
|
|
@@ -2769,7 +2949,13 @@ const RetailerProductEditionView = ({
|
|
|
2769
2949
|
setShowVersionSelector={setShowVersionSelector}
|
|
2770
2950
|
// setSelectedImages={setSelectedImages} prop pasada asi: FullTabsMenu -> TabsMenu Pero en TabsMenu no se espera ninguna prop setSelectedImages
|
|
2771
2951
|
// actions
|
|
2772
|
-
|
|
2952
|
+
// El botón actúa sobre la bandeja visible: en activos descargaba
|
|
2953
|
+
// las imágenes del producto, que no es lo que el usuario pidió.
|
|
2954
|
+
downloadImages={
|
|
2955
|
+
currentTray === TRAYS.ASSETS
|
|
2956
|
+
? handleOnDownloadAssets
|
|
2957
|
+
: handleOnDownloadImages
|
|
2958
|
+
}
|
|
2773
2959
|
askToDeleteImages={handleOnAskToDeleteImages}
|
|
2774
2960
|
showSaveButton={isAuditorAssigned() || isUserAssignedToService()}
|
|
2775
2961
|
onClickSave={handleOnClickSave}
|
|
@@ -2847,7 +3033,36 @@ const RetailerProductEditionView = ({
|
|
|
2847
3033
|
{state.active_tab === "Imágenes" &&
|
|
2848
3034
|
(state.product?.images_status !== "NS" ? (
|
|
2849
3035
|
<>
|
|
2850
|
-
{
|
|
3036
|
+
{enableDigitalAssets && (
|
|
3037
|
+
<GalleryTrayTabs
|
|
3038
|
+
activeTray={currentTray}
|
|
3039
|
+
setActiveTray={setActiveTray}
|
|
3040
|
+
imagesCount={
|
|
3041
|
+
state.images_values?.values?.length ?? 0
|
|
3042
|
+
}
|
|
3043
|
+
assetsCount={digitalAssets.assets.length}
|
|
3044
|
+
/>
|
|
3045
|
+
)}
|
|
3046
|
+
{currentTray === TRAYS.ASSETS && (
|
|
3047
|
+
<section
|
|
3048
|
+
className="container"
|
|
3049
|
+
style={{ position: "relative" }}
|
|
3050
|
+
>
|
|
3051
|
+
<DigitalAssetsTray
|
|
3052
|
+
{...digitalAssets}
|
|
3053
|
+
unsaved={countUnsavedAssets(state)}
|
|
3054
|
+
gridLayout={imageLayout}
|
|
3055
|
+
readOnly={
|
|
3056
|
+
!isAuditorAssigned() &&
|
|
3057
|
+
!isUserAssignedToService()
|
|
3058
|
+
}
|
|
3059
|
+
selectedAssets={state.selected_assets}
|
|
3060
|
+
setSelectedAssets={handleOnSetSelectedAssets}
|
|
3061
|
+
setCheckAll={handleOnSetAllAssetsChecked}
|
|
3062
|
+
/>
|
|
3063
|
+
</section>
|
|
3064
|
+
)}
|
|
3065
|
+
{currentTray === TRAYS.IMAGES && !imageLayout && (
|
|
2851
3066
|
<GalleryHeader
|
|
2852
3067
|
checkAll={state.all_image_checked} // Determina si el checkbox "Seleccionar todo" está marcado
|
|
2853
3068
|
setCheckAll={handleOnSetCheckAll} // Toggler de true o false para el checkbox "Seleccionar todo"
|
|
@@ -2855,74 +3070,76 @@ const RetailerProductEditionView = ({
|
|
|
2855
3070
|
// shotThd={shotThd} // No se usa?
|
|
2856
3071
|
/>
|
|
2857
3072
|
)}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
<div
|
|
2863
|
-
{...getRootProps({
|
|
2864
|
-
className: `dropzone ${
|
|
2865
|
-
isDragActive ? "drag-active" : ""
|
|
2866
|
-
}`,
|
|
2867
|
-
})}
|
|
3073
|
+
{currentTray === TRAYS.IMAGES && (
|
|
3074
|
+
<section
|
|
3075
|
+
className="container"
|
|
3076
|
+
style={{ position: "relative" }}
|
|
2868
3077
|
>
|
|
2869
|
-
<input {...getInputProps()} />
|
|
2870
|
-
{isDragActive && (
|
|
2871
|
-
<div className="drag-overlay">
|
|
2872
|
-
<p>Suelta las imágenes aquí</p>
|
|
2873
|
-
</div>
|
|
2874
|
-
)}
|
|
2875
|
-
<aside>{thumbs()}</aside>
|
|
2876
|
-
</div>
|
|
2877
|
-
{state.images_values?.values.length === 0 && (
|
|
2878
3078
|
<div
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
textAlign: "center",
|
|
2885
|
-
padding: "40px",
|
|
2886
|
-
width: "80%",
|
|
2887
|
-
maxWidth: "500px",
|
|
2888
|
-
fontFamily: "Arial, sans-serif",
|
|
2889
|
-
}}
|
|
3079
|
+
{...getRootProps({
|
|
3080
|
+
className: `dropzone ${
|
|
3081
|
+
isDragActive ? "drag-active" : ""
|
|
3082
|
+
}`,
|
|
3083
|
+
})}
|
|
2890
3084
|
>
|
|
2891
|
-
<
|
|
3085
|
+
<input {...getInputProps()} />
|
|
3086
|
+
{isDragActive && (
|
|
3087
|
+
<div className="drag-overlay">
|
|
3088
|
+
<p>Suelta las imágenes aquí</p>
|
|
3089
|
+
</div>
|
|
3090
|
+
)}
|
|
3091
|
+
<aside>{thumbs()}</aside>
|
|
3092
|
+
</div>
|
|
3093
|
+
{state.images_values?.values?.length === 0 && (
|
|
3094
|
+
<div
|
|
2892
3095
|
style={{
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
3096
|
+
position: "absolute",
|
|
3097
|
+
top: "50%",
|
|
3098
|
+
left: "50%",
|
|
3099
|
+
transform: "translate(-50%, -50%)",
|
|
3100
|
+
textAlign: "center",
|
|
3101
|
+
padding: "40px",
|
|
3102
|
+
width: "80%",
|
|
3103
|
+
maxWidth: "500px",
|
|
3104
|
+
fontFamily: "Arial, sans-serif",
|
|
2896
3105
|
}}
|
|
2897
3106
|
>
|
|
2898
|
-
Este producto no tiene imágenes
|
|
2899
|
-
</p>
|
|
2900
|
-
{isAuditorAssigned() ||
|
|
2901
|
-
isUserAssignedToService() ? (
|
|
2902
3107
|
<p
|
|
2903
3108
|
style={{
|
|
2904
|
-
fontSize: "
|
|
2905
|
-
color: "#
|
|
2906
|
-
marginBottom: "
|
|
3109
|
+
fontSize: "18px",
|
|
3110
|
+
color: "#666",
|
|
3111
|
+
marginBottom: "16px",
|
|
2907
3112
|
}}
|
|
2908
3113
|
>
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3114
|
+
Este producto no tiene imágenes
|
|
3115
|
+
</p>
|
|
3116
|
+
{isAuditorAssigned() ||
|
|
3117
|
+
isUserAssignedToService() ? (
|
|
3118
|
+
<p
|
|
2912
3119
|
style={{
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
3120
|
+
fontSize: "14px",
|
|
3121
|
+
color: "#999",
|
|
3122
|
+
marginBottom: "20px",
|
|
2916
3123
|
}}
|
|
2917
3124
|
>
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3125
|
+
Arrastra las imágenes aquí o{" "}
|
|
3126
|
+
<span
|
|
3127
|
+
onClick={open}
|
|
3128
|
+
style={{
|
|
3129
|
+
color: "#007bff",
|
|
3130
|
+
cursor: "pointer",
|
|
3131
|
+
textDecoration: "underline",
|
|
3132
|
+
}}
|
|
3133
|
+
>
|
|
3134
|
+
haz clic para abrir el explorador de
|
|
3135
|
+
archivos
|
|
3136
|
+
</span>
|
|
3137
|
+
</p>
|
|
3138
|
+
) : null}
|
|
3139
|
+
</div>
|
|
3140
|
+
)}
|
|
3141
|
+
</section>
|
|
3142
|
+
)}
|
|
2926
3143
|
</>
|
|
2927
3144
|
) : (
|
|
2928
3145
|
<ScreenHeader
|