contentoh-components-library 21.6.57 → 21.6.59
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/settings.local.json +7 -0
- package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
- package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
- package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
- package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
- package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
- package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
- package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
- package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
- package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +2 -1
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +325 -10
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -2
- package/dist/components/pages/ProviderProductEdition/index.js +486 -231
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
- package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
- package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
- package/src/components/pages/CreatorsProductEdition/index.js +19 -0
- package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
- package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
- package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
- package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
- package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
- package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
- package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
- package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
- package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
- package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
- package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
- package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
- package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
- package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
- package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
- package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
- package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
- package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
- package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +1 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +201 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -0
- package/src/components/pages/ProviderProductEdition/index.js +228 -7
- package/src/index.js +1 -0
|
@@ -0,0 +1,2238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lógica de la edición de producto de Creators.
|
|
3
|
+
*
|
|
4
|
+
* COPIA LITERAL de `RetailerProductEdition/index.js` (commit ff06fba), líneas
|
|
5
|
+
* 122-317, 365-2182 y 2309-2320: todo lo que no es JSX. Se copió en vez de
|
|
6
|
+
* compartirse porque la vista actual no se puede tocar. Consecuencia: un fix
|
|
7
|
+
* en RetailerProductEdition hay que portarlo aquí a mano. Para ver qué cambió
|
|
8
|
+
* desde la copia:
|
|
9
|
+
*
|
|
10
|
+
* git diff ff06fba -- src/components/pages/RetailerProductEdition/index.js
|
|
11
|
+
*
|
|
12
|
+
* Lo único que no se copió: `thumbs` (JSX de la galería, vive en la vista) y
|
|
13
|
+
* el skeleton. El botón de "comentario revisado" se dejó fuera a propósito:
|
|
14
|
+
* en el original llama a `commentRevised()`, que no existe.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useRef, useState } from "react";
|
|
17
|
+
|
|
18
|
+
import axios from "axios";
|
|
19
|
+
import { useDropzone } from "react-dropzone";
|
|
20
|
+
import { saveAs } from "file-saver";
|
|
21
|
+
|
|
22
|
+
import { useProviderProductEdition } from "../../RetailerProductEdition/context/provider-product-edition.context";
|
|
23
|
+
import { getMinorStatusFromRetailer } from "../../RetailerProductEdition/context/reducers/product";
|
|
24
|
+
import {
|
|
25
|
+
normalizeProduct,
|
|
26
|
+
getConceptByTab,
|
|
27
|
+
getAuditVersion,
|
|
28
|
+
getInputsData,
|
|
29
|
+
createMessage,
|
|
30
|
+
sendMessage,
|
|
31
|
+
translateConcept,
|
|
32
|
+
getStatusArrayForUser,
|
|
33
|
+
getConceptsByRole,
|
|
34
|
+
buildCollaboratorAssignations,
|
|
35
|
+
calculateRequiredNull,
|
|
36
|
+
} from "../../RetailerProductEdition/utils";
|
|
37
|
+
import generateThumbnail from "../../RetailerProductEdition/generateThumnail";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
fetchUsers,
|
|
41
|
+
getPercentage,
|
|
42
|
+
getRetailerServices,
|
|
43
|
+
getServicesData,
|
|
44
|
+
} from "../../../../global-files/data";
|
|
45
|
+
import { useDigitalAssets } from "../../../../global-files/digital-assets/useDigitalAssets";
|
|
46
|
+
|
|
47
|
+
import { TRAYS } from "../../../molecules/GalleryTrayTabs";
|
|
48
|
+
import { soloPropagables } from "../../../organisms/PropagateChangesModal/non-propagable";
|
|
49
|
+
import { readPayload } from "../../../organisms/LinkVariantsModal/api";
|
|
50
|
+
|
|
51
|
+
import successIcon from "../../../../assets/images/genericModal/genericModalCheck.svg";
|
|
52
|
+
import errorIcon from "../../../../assets/images/genericModal/errorModal.svg";
|
|
53
|
+
import warningIcon from "../../../../assets/images/genericModal/genericModalWarning.svg";
|
|
54
|
+
import attributesSent from "../../../../assets/images/modalsSVGs/attributesSent.svg";
|
|
55
|
+
import descriptionSent from "../../../../assets/images/modalsSVGs/descriptionSent.svg";
|
|
56
|
+
import imagesSent from "../../../../assets/images/modalsSVGs/uploadingImages.svg";
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function obtenerDuracionVideoBase64(base64Data) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const video = document.createElement("video");
|
|
62
|
+
video.src = base64Data;
|
|
63
|
+
video.addEventListener("loadedmetadata", () => {
|
|
64
|
+
resolve(video.duration);
|
|
65
|
+
});
|
|
66
|
+
video.addEventListener("error", reject);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const useCreatorsProductEdition = ({
|
|
71
|
+
tabsSections,
|
|
72
|
+
productSelected = {},
|
|
73
|
+
user = {},
|
|
74
|
+
token,
|
|
75
|
+
location,
|
|
76
|
+
enableDigitalAssets = false,
|
|
77
|
+
}) => {
|
|
78
|
+
const {
|
|
79
|
+
state,
|
|
80
|
+
dispatch,
|
|
81
|
+
saveDatasheets,
|
|
82
|
+
saveDescriptions,
|
|
83
|
+
updateImages,
|
|
84
|
+
deleteImages,
|
|
85
|
+
saveAssets,
|
|
86
|
+
registerUploadedAssets,
|
|
87
|
+
handleOnDownloadImages,
|
|
88
|
+
createComment,
|
|
89
|
+
} = useProviderProductEdition();
|
|
90
|
+
|
|
91
|
+
const isRetailer = user?.is_retailer;
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
El original usaba `useCloseModal("version-selector")`: cierra con cualquier
|
|
95
|
+
clic cuyo destino no esté dentro de `#version-selector`. En el diálogo de
|
|
96
|
+
nueva versión, el botón de «Versión personalizada» desaparece al cambiar de
|
|
97
|
+
paso antes de que llegue ese clic al documento, así que parecía un clic
|
|
98
|
+
fuera y se cerraba todo. El Drawer de `VersionDrawer` ya se cierra al tocar
|
|
99
|
+
fuera o con Escape.
|
|
100
|
+
*/
|
|
101
|
+
const [showVersionSelector, setShowVersionSelector] = useState(false);
|
|
102
|
+
const [auditableVersion, setAuditableVersion] = useState(null);
|
|
103
|
+
const [headerTop, setHeaderTop] = useState(0);
|
|
104
|
+
const [shotThd, setShotThd] = useState(false);
|
|
105
|
+
const [observation, setObservation] = useState();
|
|
106
|
+
const [showRejectModal, setShowRejectModal] = useState(false);
|
|
107
|
+
const [isObservationVisible, setObservationVisible] = useState(false);
|
|
108
|
+
const [imageLayout, setImageLayout] = useState(false);
|
|
109
|
+
const [crossComment, setCrossComment] = useState(false);
|
|
110
|
+
const [statusArray, setStatusArray] = useState([]);
|
|
111
|
+
const [rejectAll, setRejectAll] = useState(false);
|
|
112
|
+
|
|
113
|
+
// Padre/hijo: mismo comportamiento que la ficha de la aplicación de clientes.
|
|
114
|
+
const [showLinkVariants, setShowLinkVariants] = useState(false);
|
|
115
|
+
const [family, setFamily] = useState(null);
|
|
116
|
+
const [showFamily, setShowFamily] = useState(false);
|
|
117
|
+
const [loadingVariant, setLoadingVariant] = useState(null);
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
Agrupación de variantes. Aparte del padre/hijo a propósito: son mecanismos
|
|
121
|
+
independientes y un producto puede tener los dos. Padre/hijo es herencia;
|
|
122
|
+
esto es solo un código compartido.
|
|
123
|
+
*/
|
|
124
|
+
const [variantGroup, setVariantGroup] = useState(null);
|
|
125
|
+
const [showGroup, setShowGroup] = useState(false);
|
|
126
|
+
const [showGroupModal, setShowGroupModal] = useState(false);
|
|
127
|
+
|
|
128
|
+
// Qué vínculo se está por romper, o null si no hay diálogo abierto. Guarda al
|
|
129
|
+
// HIJO, que es quien lleva la relación y lo que pide la acción `unlink`.
|
|
130
|
+
const [porDesvincular, setPorDesvincular] = useState(null);
|
|
131
|
+
// Por qué un salto no se pudo hacer. Vive aparte de `family.error` porque no
|
|
132
|
+
// es un fallo al leer la familia, sino al abrir uno de sus miembros.
|
|
133
|
+
const [avisoFamilia, setAvisoFamilia] = useState(null);
|
|
134
|
+
const [pendingPropagation, setPendingPropagation] = useState(null);
|
|
135
|
+
const [preparandoPropagacion, setPreparandoPropagacion] = useState(false);
|
|
136
|
+
|
|
137
|
+
const [auditServices, setAuditServices] = useState([]);
|
|
138
|
+
const [auditDatasheets, setAuditDatasheets] = useState([]);
|
|
139
|
+
const [auditDescriptions, setAuditDescriptions] = useState([]);
|
|
140
|
+
const [showModal, setShowModal] = useState(false);
|
|
141
|
+
|
|
142
|
+
const [compare, setCompare] = useState(false);
|
|
143
|
+
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
|
144
|
+
accept: "image/*, video/mp4",
|
|
145
|
+
noKeyboard: true,
|
|
146
|
+
multiple: true,
|
|
147
|
+
noClick: true,
|
|
148
|
+
onDrop: (acceptedFiles) => {
|
|
149
|
+
acceptedFiles.map((file) => {
|
|
150
|
+
const reader = new FileReader();
|
|
151
|
+
reader.fileName = file.name;
|
|
152
|
+
|
|
153
|
+
reader.onload = async function (e) {
|
|
154
|
+
const fileName = e.target.fileName;
|
|
155
|
+
const fileExtension = fileName.split(".").pop();
|
|
156
|
+
const fileDataURL = e.target.result;
|
|
157
|
+
|
|
158
|
+
const img = new Image();
|
|
159
|
+
|
|
160
|
+
if (fileExtension === "mp4") {
|
|
161
|
+
try {
|
|
162
|
+
const duracion = await obtenerDuracionVideoBase64(fileDataURL);
|
|
163
|
+
if (duracion <= 10 || duracion >= 600) {
|
|
164
|
+
dispatch({
|
|
165
|
+
type: "SET_MODAL",
|
|
166
|
+
payload: {
|
|
167
|
+
show: true,
|
|
168
|
+
title: "Hubo un error al subir el video",
|
|
169
|
+
message:
|
|
170
|
+
"Los videos deben durar entre 15 segundos y 10 minutos",
|
|
171
|
+
image: errorIcon,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
img.src = await generateThumbnail(fileDataURL, 2);
|
|
178
|
+
img.onload = function () {
|
|
179
|
+
const newImg = {
|
|
180
|
+
src: img.src,
|
|
181
|
+
name: fileName,
|
|
182
|
+
ext: fileExtension,
|
|
183
|
+
width: img.width,
|
|
184
|
+
height: img.height,
|
|
185
|
+
video_src: fileDataURL,
|
|
186
|
+
isApproved: true, // VALIDAR QUE SOLO SEA PARA RADIOSHACK
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
dispatch({
|
|
190
|
+
type: "ADD_IMAGE_VALUE",
|
|
191
|
+
payload: newImg,
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error("Error al obtener la duración del video:", error);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
img.src = fileDataURL;
|
|
201
|
+
img.onload = function () {
|
|
202
|
+
const width = img.width;
|
|
203
|
+
const height = img.height;
|
|
204
|
+
const newImg = {
|
|
205
|
+
src: fileDataURL,
|
|
206
|
+
name: fileName,
|
|
207
|
+
ext: fileExtension,
|
|
208
|
+
width: width,
|
|
209
|
+
height: height,
|
|
210
|
+
video_src: "",
|
|
211
|
+
isApproved: true, // VALIDAR QUE SOLO SEA PARA RADIOSHACK
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
dispatch({
|
|
215
|
+
type: "ADD_IMAGE_VALUE",
|
|
216
|
+
payload: newImg,
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
reader.onerror = function (error) {
|
|
222
|
+
console.error("dropzoneError: ", error);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
reader.readAsDataURL(file);
|
|
226
|
+
return file;
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const [openChangeStatusModal, setOpenChangeStatusModal] = useState(false);
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
Bandeja de activos digitales. La lista vive en el reducer de la ficha
|
|
235
|
+
(`state.assets_values`), igual que las imágenes, para que el guardado del
|
|
236
|
+
contexto pueda leerla. El hook solo aporta validación y ciclo de vida.
|
|
237
|
+
|
|
238
|
+
Sigue vacía porque todavía no hay de dónde leerla: cuando exista el endpoint
|
|
239
|
+
se llama a `replaceAll` desde `loadData` y el resto de la pantalla no cambia.
|
|
240
|
+
*/
|
|
241
|
+
const [assetNotices, setAssetNotices] = useState([]);
|
|
242
|
+
const digitalAssets = useDigitalAssets({
|
|
243
|
+
assets: state.assets_values,
|
|
244
|
+
dispatch,
|
|
245
|
+
notices: assetNotices,
|
|
246
|
+
setNotices: setAssetNotices,
|
|
247
|
+
// Con los dos, la subida a S3 arranca sola al soltar los archivos. Sin
|
|
248
|
+
// ellos —en Storybook, por ejemplo— la bandeja funciona igual pero no sube.
|
|
249
|
+
articleId: state.product?.id_article ?? null,
|
|
250
|
+
token,
|
|
251
|
+
// Al terminar la subida el activo queda registrado solo, sin esperar al
|
|
252
|
+
// botón de guardar: con la barra llena, nadie supone que falta un paso.
|
|
253
|
+
onUploaded: (uploaded) => registerUploadedAssets(token, uploaded),
|
|
254
|
+
});
|
|
255
|
+
const [activeTray, setActiveTray] = useState(TRAYS.IMAGES);
|
|
256
|
+
const currentTray = enableDigitalAssets ? activeTray : TRAYS.IMAGES;
|
|
257
|
+
|
|
258
|
+
/*
|
|
259
|
+
De qué artículo se cargaron ya los activos.
|
|
260
|
+
|
|
261
|
+
`loadData` se vuelve a ejecutar cada vez que cambia la cadena activa, pero los
|
|
262
|
+
activos son generales: no dependen de la cadena ni de la versión, así que la
|
|
263
|
+
respuesta sería idéntica. Volver a cargarlos borraría los archivos que el
|
|
264
|
+
usuario acaba de soltar y todavía no guarda —sin aviso y sin forma de
|
|
265
|
+
recuperarlos—. Por eso se cargan una sola vez por artículo.
|
|
266
|
+
*/
|
|
267
|
+
const assetsLoadedFor = useRef(null);
|
|
268
|
+
|
|
269
|
+
const handleOnSetSelectedAssets = (assets) =>
|
|
270
|
+
dispatch({
|
|
271
|
+
type: "SET_SELECTED_ASSETS",
|
|
272
|
+
payload: assets,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const handleOnSetAllAssetsChecked = (isChecked) =>
|
|
276
|
+
dispatch({
|
|
277
|
+
type: "TOGGLE_CHECK_ALL_ASSETS",
|
|
278
|
+
payload: isChecked,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
const initializeProduct = async () => {
|
|
283
|
+
try {
|
|
284
|
+
const product = productSelected
|
|
285
|
+
? productSelected
|
|
286
|
+
: JSON.parse(sessionStorage.getItem("productSelected"));
|
|
287
|
+
|
|
288
|
+
const productNormalized = normalizeProduct(product);
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
dispatch({
|
|
292
|
+
type: "SET_PRODUCT",
|
|
293
|
+
payload: productNormalized,
|
|
294
|
+
});
|
|
295
|
+
dispatch({
|
|
296
|
+
type: "SET_ACTIVE_RETAILER",
|
|
297
|
+
payload: productNormalized.categoryRetailerInOrder[0],
|
|
298
|
+
});
|
|
299
|
+
setShotThd(
|
|
300
|
+
[58, 59, 60, 61].includes(
|
|
301
|
+
productNormalized.categoryRetailerInOrder[0].id_retailer,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Los estatus evaluables salen del rol UNIDO a la asignación. Solo por
|
|
306
|
+
// rol, quien produce y además audita no tenía 'AC' en la lista y el
|
|
307
|
+
// envío se quedaba sin nada que mandar.
|
|
308
|
+
setStatusArray(getStatusArrayForUser(user, productNormalized));
|
|
309
|
+
|
|
310
|
+
// Cargar usuarios y asignaciones
|
|
311
|
+
const users = await fetchUsers(token);
|
|
312
|
+
if (users.length > 0) {
|
|
313
|
+
dispatch({
|
|
314
|
+
type: "SET_COLLABORATOR_ASSIGNATIONS",
|
|
315
|
+
payload: buildCollaboratorAssignations(productNormalized, users),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const { id_article } = productNormalized;
|
|
320
|
+
if (id_article) {
|
|
321
|
+
// Aislado: si version-manager falla (p. ej. CORS) no debe impedir
|
|
322
|
+
// que se cargue la observación de abajo.
|
|
323
|
+
try {
|
|
324
|
+
await getAuditVersion(id_article, setAuditableVersion, token);
|
|
325
|
+
} catch (auditError) {
|
|
326
|
+
console.warn(
|
|
327
|
+
"No se pudo obtener la versión auditable:",
|
|
328
|
+
auditError.message,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (id_article && productNormalized.id_order) {
|
|
334
|
+
const response = await axios.get(
|
|
335
|
+
process.env.REACT_APP_READ_OBSERVATION,
|
|
336
|
+
{
|
|
337
|
+
params: {
|
|
338
|
+
articleId: id_article,
|
|
339
|
+
orderId: productNormalized.id_order,
|
|
340
|
+
},
|
|
341
|
+
headers: {
|
|
342
|
+
Authorization: sessionStorage.getItem("jwt"),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
const parseData = JSON.parse(response.data.body).data[0];
|
|
347
|
+
setObservation(parseData.observations);
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error("Error setting product data: ", error);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Setter del producto y carga inicial (optimizado para evitar cascada)
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
initializeProduct();
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
const loadAuditableData = async () => {
|
|
360
|
+
const auditServices = await getRetailerServices(
|
|
361
|
+
state.product?.id_article,
|
|
362
|
+
state.product?.categoryName,
|
|
363
|
+
parseInt(state.product?.id_category),
|
|
364
|
+
auditableVersion.version,
|
|
365
|
+
token,
|
|
366
|
+
);
|
|
367
|
+
// getRetailerServices puede devolver undefined si falla; no seguimos.
|
|
368
|
+
if (!Array.isArray(auditServices)) return;
|
|
369
|
+
getInputsData(
|
|
370
|
+
auditServices,
|
|
371
|
+
{ id: state.active_retailer.id_retailer },
|
|
372
|
+
setAuditDatasheets,
|
|
373
|
+
setAuditDescriptions,
|
|
374
|
+
);
|
|
375
|
+
setAuditServices(auditServices);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (auditableVersion) {
|
|
380
|
+
loadAuditableData();
|
|
381
|
+
}
|
|
382
|
+
}, [auditableVersion]);
|
|
383
|
+
|
|
384
|
+
const loadData = async (enableLoading = true, versionSelected = null) => {
|
|
385
|
+
try {
|
|
386
|
+
if (enableLoading) dispatch({ type: "SET_LOADING", payload: true });
|
|
387
|
+
dispatch({ type: "SET_ERRORS", payload: [] });
|
|
388
|
+
|
|
389
|
+
const { id_article, version, id_order } = state.product;
|
|
390
|
+
const { category, id_category, id_retailer } = state.active_retailer;
|
|
391
|
+
|
|
392
|
+
// Payloads y headers
|
|
393
|
+
const data = [
|
|
394
|
+
{
|
|
395
|
+
id_article,
|
|
396
|
+
version: versionSelected ? versionSelected : version,
|
|
397
|
+
relations: [
|
|
398
|
+
{
|
|
399
|
+
id_retailer,
|
|
400
|
+
id_category,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
const headers = { Authorization: token };
|
|
407
|
+
|
|
408
|
+
const [services, percentagesRes, servicesDataRes, commentsResponse] =
|
|
409
|
+
await Promise.all([
|
|
410
|
+
getRetailerServices(
|
|
411
|
+
id_article,
|
|
412
|
+
category,
|
|
413
|
+
id_category,
|
|
414
|
+
versionSelected ?? version,
|
|
415
|
+
token,
|
|
416
|
+
id_retailer,
|
|
417
|
+
),
|
|
418
|
+
getPercentage({ data, headers }),
|
|
419
|
+
getServicesData({
|
|
420
|
+
articleId: id_article,
|
|
421
|
+
orderId: id_order,
|
|
422
|
+
end: true,
|
|
423
|
+
token,
|
|
424
|
+
}),
|
|
425
|
+
// Obtener comentarios en paralelo
|
|
426
|
+
Promise.all([
|
|
427
|
+
axios.get(process.env.REACT_APP_COMMENTS_ENDPOINT, {
|
|
428
|
+
params: {
|
|
429
|
+
articleId: id_article,
|
|
430
|
+
concept: "description",
|
|
431
|
+
orderIdColab: id_order,
|
|
432
|
+
version: versionSelected ?? version,
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
axios.get(process.env.REACT_APP_COMMENTS_ENDPOINT, {
|
|
436
|
+
params: {
|
|
437
|
+
articleId: id_article,
|
|
438
|
+
concept: "datasheet",
|
|
439
|
+
orderIdColab: id_order,
|
|
440
|
+
version: versionSelected ?? version,
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
axios.get(process.env.REACT_APP_COMMENTS_ENDPOINT, {
|
|
444
|
+
params: {
|
|
445
|
+
articleId: id_article,
|
|
446
|
+
concept: "images",
|
|
447
|
+
orderIdColab: id_order,
|
|
448
|
+
version: versionSelected ?? version,
|
|
449
|
+
},
|
|
450
|
+
}),
|
|
451
|
+
]),
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
// getRetailerServices se traga sus errores y devuelve undefined cuando
|
|
455
|
+
// alguna de sus llamadas falla. Si eso pasa, validamos aquí para lanzar
|
|
456
|
+
// un error controlado en vez de reventar más abajo en services[2]/services[0]
|
|
457
|
+
// y dejar la pantalla colgada en el skeleton para siempre.
|
|
458
|
+
if (!Array.isArray(services) || services.length < 3 || !services[2]) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"No se pudieron cargar los servicios del producto. Inténtalo de nuevo.",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
dispatch({
|
|
465
|
+
type: "SET_RETAILER_STATUS",
|
|
466
|
+
payload: servicesDataRes.map((service) => ({
|
|
467
|
+
retailer_id: service?.id_retailer,
|
|
468
|
+
service: service?.service,
|
|
469
|
+
status: service?.status,
|
|
470
|
+
task_user_group_id: service?.task_user_group_id,
|
|
471
|
+
})),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Extraer relaciones del response. getPercentage se traga sus errores y
|
|
475
|
+
// devuelve undefined (p. ej. si CORS bloquea el endpoint), así que el
|
|
476
|
+
// JSON.parse va protegido para no tumbar toda la carga.
|
|
477
|
+
let percentages = [];
|
|
478
|
+
try {
|
|
479
|
+
percentages =
|
|
480
|
+
JSON.parse(percentagesRes?.[0]?.body)?.[0]?.relations ?? [];
|
|
481
|
+
} catch (e) {
|
|
482
|
+
console.warn("No se pudieron parsear los porcentajes:", e.message);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Procesar comentarios
|
|
486
|
+
let commentsMap = {};
|
|
487
|
+
commentsResponse.forEach((commentRes) => {
|
|
488
|
+
const commentData = JSON.parse(commentRes?.data?.body).data[0];
|
|
489
|
+
if (commentData) {
|
|
490
|
+
const tab = translateConcept(commentData.concept);
|
|
491
|
+
commentsMap[tab] = commentData;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
dispatch({ type: "SET_COMMENTS", payload: commentsMap });
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
// Ordenamiento de imágenes
|
|
498
|
+
const orderMap = services[2].inputsByRetailer
|
|
499
|
+
.flat()
|
|
500
|
+
.reduce((acc, item) => {
|
|
501
|
+
acc[item.id_image] = item.order;
|
|
502
|
+
return acc;
|
|
503
|
+
}, {});
|
|
504
|
+
|
|
505
|
+
const orderedValues = [...services[2].values].sort((a, b) => {
|
|
506
|
+
const orderA = orderMap[a.image_id] ?? Number.MAX_SAFE_INTEGER;
|
|
507
|
+
const orderB = orderMap[b.image_id] ?? Number.MAX_SAFE_INTEGER;
|
|
508
|
+
return orderA - orderB;
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Procesamiento de inputs del retailer activo (evita cascada con state.services)
|
|
512
|
+
// El retailer activo puede no traer datasheets; usamos data vacía como fallback
|
|
513
|
+
// para no reventar en Object.values(undefined).
|
|
514
|
+
const retailerDatasheets = services[0]?.[id_retailer];
|
|
515
|
+
const datasheetsActiveRetailer = {
|
|
516
|
+
...retailerDatasheets,
|
|
517
|
+
data: Object.values(retailerDatasheets?.data ?? {}),
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const descriptionsActiveRetailer = services[1].filter(
|
|
521
|
+
(item) => item.id === id_retailer,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* El id del retailer activo viene con dos nombres según de dónde salga:
|
|
526
|
+
* `categoryRetailerInOrder` lo trae como `id_retailer`, mientras que los
|
|
527
|
+
* retailers de `read-data-images` lo traen como `id`. Si se compara solo
|
|
528
|
+
* contra uno, al cambiar de cadena el otro queda `undefined`, la
|
|
529
|
+
* comparación es siempre falsa y `filteredValues` sale vacío — la imagen
|
|
530
|
+
* existe, el backend la devuelve, y aun así no se pinta ninguna.
|
|
531
|
+
*/
|
|
532
|
+
const activeRetailerId = id_retailer ?? state.active_retailer?.id;
|
|
533
|
+
|
|
534
|
+
const filteredValues = services[2].values.filter((value) => {
|
|
535
|
+
return services[2].inputsByRetailer.some((retailerInput) =>
|
|
536
|
+
retailerInput.some(
|
|
537
|
+
(input) =>
|
|
538
|
+
Number(input.id_retailer) === Number(activeRetailerId) &&
|
|
539
|
+
Number(input.id_image) === Number(value.image_id),
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Dispatch consolidado
|
|
545
|
+
dispatch({
|
|
546
|
+
type: "SET_SERVICES",
|
|
547
|
+
payload: {
|
|
548
|
+
datasheets: services[0],
|
|
549
|
+
descriptions: services[1],
|
|
550
|
+
images: {
|
|
551
|
+
...services[2],
|
|
552
|
+
values: orderedValues,
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
dispatch({ type: "SET_PERCENTAGES", payload: percentages });
|
|
558
|
+
dispatch({
|
|
559
|
+
type: "SET_ACTIVE_PERCENTAGES",
|
|
560
|
+
payload: percentages.find(
|
|
561
|
+
({ id_retailer: rId }) => rId === id_retailer,
|
|
562
|
+
),
|
|
563
|
+
});
|
|
564
|
+
dispatch({ type: "SET_SERVICES_DATA", payload: servicesDataRes });
|
|
565
|
+
|
|
566
|
+
// Setear inputs del retailer activo directamente (evita cascada)
|
|
567
|
+
dispatch({
|
|
568
|
+
type: "SET_DATASHEETS_INPUTS",
|
|
569
|
+
// `services[0]?` por lo mismo que el fallback de arriba: la lectura de
|
|
570
|
+
// fichas puede volver sin datos para esa cadena y categoría. Arriba ya
|
|
571
|
+
// se contemplaba; aquí no, y como este dispatch va ANTES de
|
|
572
|
+
// SET_IMAGES_VALUES, reventar aquí dejaba la ficha cargando para
|
|
573
|
+
// siempre — con las imágenes ya traídas y sin pintar.
|
|
574
|
+
payload: [datasheetsActiveRetailer, services[0]?.inputs ?? {}],
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
dispatch({
|
|
578
|
+
type: "SET_DESCRIPTIONS_INPUTS",
|
|
579
|
+
payload: descriptionsActiveRetailer,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
dispatch({
|
|
583
|
+
type: "SET_IMAGES_VALUES",
|
|
584
|
+
payload: {
|
|
585
|
+
...services[2],
|
|
586
|
+
values: filteredValues,
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
/*
|
|
591
|
+
Activos digitales. `services[2].assets` todavía no existe en la
|
|
592
|
+
respuesta del backend: hasta que exista, esto no hace nada, y el día que
|
|
593
|
+
llegue la bandeja se llena sola sin tocar nada más.
|
|
594
|
+
*/
|
|
595
|
+
if (
|
|
596
|
+
Array.isArray(services[2].assets) &&
|
|
597
|
+
assetsLoadedFor.current !== id_article
|
|
598
|
+
) {
|
|
599
|
+
assetsLoadedFor.current = id_article;
|
|
600
|
+
digitalAssets.replaceAll(services[2].assets);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Calcular campos requeridos sin valor
|
|
604
|
+
dispatch({
|
|
605
|
+
type: "SET_MISSING_REQUIRED_FIELDS",
|
|
606
|
+
payload: calculateRequiredNull(services, servicesDataRes, id_retailer),
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Auditable
|
|
610
|
+
if (auditableVersion && id_retailer) {
|
|
611
|
+
loadAuditableData();
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
console.error("Error loading data: ", error);
|
|
615
|
+
dispatch({ type: "SET_ERRORS", payload: [error.message] });
|
|
616
|
+
} finally {
|
|
617
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
if (state.active_retailer) {
|
|
623
|
+
loadData();
|
|
624
|
+
}
|
|
625
|
+
}, [state.active_retailer]);
|
|
626
|
+
|
|
627
|
+
const handleOnChangeCurrentImage = (index) => {
|
|
628
|
+
if (typeof index === "number") {
|
|
629
|
+
dispatch({ type: "SET_CURRENT_IMAGE", payload: index });
|
|
630
|
+
} else if (typeof index === "object") {
|
|
631
|
+
dispatch({ type: "CHANGE_ATTR_VALUE", payload: index });
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const handleOnShowModalGallery = () => {
|
|
636
|
+
dispatch({
|
|
637
|
+
type: "SET_ACTIVE_TAB",
|
|
638
|
+
payload: "Imágenes",
|
|
639
|
+
});
|
|
640
|
+
setShowModal(true);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const handleOnChangeActiveRetailer = (retailer) => {
|
|
644
|
+
dispatch({
|
|
645
|
+
type: "SET_ACTIVE_RETAILER",
|
|
646
|
+
payload: retailer,
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const handleOnChangeActiveTab = (tab) => {
|
|
651
|
+
dispatch({
|
|
652
|
+
type: "SET_ACTIVE_TAB",
|
|
653
|
+
payload: tab,
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const handleOnToggleImageLayout = () => {
|
|
658
|
+
setImageLayout(!imageLayout);
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const handleOnToggleObservation = () => {
|
|
662
|
+
setObservationVisible(!isObservationVisible);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const handleOnHideObservation = () => {
|
|
666
|
+
setObservationVisible(false);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const isAuditorAssigned = () => {
|
|
670
|
+
return state.product?.id_auditor === user?.id_user;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* ¿Ocupo este slot EN ESTE producto?
|
|
675
|
+
*
|
|
676
|
+
* La calidad en la que actúas sale de la ASIGNACIÓN, no del rol. Con la
|
|
677
|
+
* estructura nueva de Creators, quien produce un servicio también lo valida:
|
|
678
|
+
* un especialista de textos puede ser el auditor del mismo producto y tiene
|
|
679
|
+
* que recorrer las dos fases, AS -> AC como especialista y AC -> AA como QA.
|
|
680
|
+
*/
|
|
681
|
+
const isAssignedAs = (rol, concept) => {
|
|
682
|
+
const assigned = state.product?.[`id_${concept}_${rol}`];
|
|
683
|
+
return !!assigned && assigned === user?.id_user;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const isUserAssignedToService = (tab, rol) => {
|
|
687
|
+
const concept = getConceptByTab(state.active_tab);
|
|
688
|
+
|
|
689
|
+
const allowedRoles = [1, 4, 5, 6, 7, 8];
|
|
690
|
+
const validUser = allowedRoles.includes(user?.id_role);
|
|
691
|
+
|
|
692
|
+
const isAdmin = user.id_role === 1;
|
|
693
|
+
if (isAdmin) return true;
|
|
694
|
+
|
|
695
|
+
// Sin `rol` explícito se prueba por asignación, no por rol. Antes se
|
|
696
|
+
// infería del `id_role`: un rol 6 caía en `id_<concept>_undefined` —columna
|
|
697
|
+
// inexistente, siempre falso— y un rol 7 solo podía ser especialista, nunca
|
|
698
|
+
// auditor. Es el mismo defecto que se corrigió en el backend.
|
|
699
|
+
const isAssigned = rol
|
|
700
|
+
? isAssignedAs(rol, concept)
|
|
701
|
+
: isAssignedAs("especialist", concept) ||
|
|
702
|
+
isAssignedAs("facilitator", concept);
|
|
703
|
+
|
|
704
|
+
return isAssigned && validUser;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const canShowSingleEvaluationButtons = (action) => {
|
|
708
|
+
let concept = getConceptByTab(action || state.active_tab);
|
|
709
|
+
|
|
710
|
+
const retailerStatus = state.services_data
|
|
711
|
+
?.find(
|
|
712
|
+
(srv) =>
|
|
713
|
+
srv.id_retailer === state.active_retailer?.id_retailer &&
|
|
714
|
+
srv.service === concept,
|
|
715
|
+
)
|
|
716
|
+
?.status?.replace(/.*\//, "");
|
|
717
|
+
|
|
718
|
+
// Se SUMA la vía por asignación a la que ya había por rol; no la
|
|
719
|
+
// reemplaza. Así los coordinadores y el administrador conservan su acceso
|
|
720
|
+
// tal cual, y el especialista que además es auditor alcanza la segunda fase.
|
|
721
|
+
const canEvaluateAsFacilitator =
|
|
722
|
+
retailerStatus === "IE" &&
|
|
723
|
+
([1, 4, 5].includes(user.id_role) ||
|
|
724
|
+
isAssignedAs("facilitator", concept));
|
|
725
|
+
|
|
726
|
+
const canEvaluateAsAuditor =
|
|
727
|
+
["AC", "RP", "RCA", "SAC"].includes(retailerStatus) &&
|
|
728
|
+
([1, 6].includes(user.id_role) || isAuditorAssigned());
|
|
729
|
+
|
|
730
|
+
// Los roles 7 y 8 también pueden validar/rechazar lo que está en AC.
|
|
731
|
+
const canEvaluateAC =
|
|
732
|
+
retailerStatus === "AC" && [7, 8].includes(user.id_role);
|
|
733
|
+
|
|
734
|
+
return canEvaluateAsFacilitator || canEvaluateAsAuditor || canEvaluateAC;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const canShowBulkEvaluationButtons = () => {
|
|
738
|
+
const concepts = getConceptsByRole(user.id_role);
|
|
739
|
+
|
|
740
|
+
const services = state.services_data.filter(({ service }) =>
|
|
741
|
+
concepts.includes(service),
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
const canEvaluateAsFacilitator =
|
|
745
|
+
services.every((srv) => srv?.status?.replace(/.*\//, "") === "IE") &&
|
|
746
|
+
([1, 4, 5].includes(user.id_role) ||
|
|
747
|
+
services.some(({ service }) => isAssignedAs("facilitator", service)));
|
|
748
|
+
|
|
749
|
+
const canEvaluateAsAuditor =
|
|
750
|
+
state.services_data.every((srv) =>
|
|
751
|
+
["AC", "RP", "RCA"].includes(srv?.status?.replace(/.*\//, "")),
|
|
752
|
+
) &&
|
|
753
|
+
([1, 6].includes(user.id_role) || isAuditorAssigned());
|
|
754
|
+
|
|
755
|
+
const canEvaluateAC =
|
|
756
|
+
services.length > 0 &&
|
|
757
|
+
services.every((srv) => srv?.status?.replace(/.*\//, "") === "AC") &&
|
|
758
|
+
[7, 8].includes(user.id_role);
|
|
759
|
+
|
|
760
|
+
return canEvaluateAsFacilitator || canEvaluateAsAuditor || canEvaluateAC;
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const getStatusByCurrentServiceAndRetailer = () => {
|
|
764
|
+
const { statusByRetailer } = state.product;
|
|
765
|
+
if (!statusByRetailer) return "-";
|
|
766
|
+
|
|
767
|
+
const currentService = getConceptByTab(state.active_tab);
|
|
768
|
+
const currentRetailer = state.active_retailer.id_retailer;
|
|
769
|
+
const currentStatus =
|
|
770
|
+
statusByRetailer.filter(
|
|
771
|
+
(item) =>
|
|
772
|
+
item.retailer_id === currentRetailer &&
|
|
773
|
+
item.service === currentService,
|
|
774
|
+
)[0]?.status || "-";
|
|
775
|
+
|
|
776
|
+
return currentStatus;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
/*
|
|
780
|
+
Guarda los activos y recarga si algo se escribió.
|
|
781
|
+
|
|
782
|
+
La recarga no es opcional: un activo recién creado sigue en memoria con su
|
|
783
|
+
id temporal y sin el id de la base. Sin volver a leer, el siguiente guardado
|
|
784
|
+
lo mandaría otra vez como nuevo y se duplicaría. Poner el ref en `null` es
|
|
785
|
+
lo que permite que `loadData` lo vuelva a traer —normalmente los activos se
|
|
786
|
+
cargan una sola vez por artículo—.
|
|
787
|
+
*/
|
|
788
|
+
const handleOnSaveAssets = async () => {
|
|
789
|
+
const guardado = await saveAssets(token);
|
|
790
|
+
if (!guardado) return;
|
|
791
|
+
|
|
792
|
+
assetsLoadedFor.current = null;
|
|
793
|
+
await loadData(false);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
const handleOnClickSaveImages = async () => {
|
|
797
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
798
|
+
await handleOnSaveAssets();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (state.product?.services?.images === 1) {
|
|
803
|
+
/**
|
|
804
|
+
* `updateImages` y NO `saveImageAttrs`.
|
|
805
|
+
*
|
|
806
|
+
* `saveImageAttrs` manda `articleData: []` fijo, así que las imágenes
|
|
807
|
+
* nuevas nunca llegaban al backend: el archivo se subía a S3 —eso lo hace
|
|
808
|
+
* el front por su cuenta— pero `saveImages` no se ejecutaba y la fila de
|
|
809
|
+
* `rel_article_image` nunca se creaba. Como la pantalla lee de esa tabla,
|
|
810
|
+
* la imagen no aparecía después de guardar.
|
|
811
|
+
*
|
|
812
|
+
* `updateImages` cubre las dos cosas: manda `attrReqImgs` con los
|
|
813
|
+
* atributos requeridos y además `articleData` con las imágenes nuevas y
|
|
814
|
+
* `updateImages` con las existentes. Es la misma función que ya usaba el
|
|
815
|
+
* botón de guardar del menú de pestañas (`handleOnClickSave`), así que
|
|
816
|
+
* ambos caminos quedan consistentes.
|
|
817
|
+
*/
|
|
818
|
+
await updateImages(token);
|
|
819
|
+
// Recargar para que la pantalla refleje lo que quedó guardado, igual que
|
|
820
|
+
// `handleOnClickSave`. Sin esto el estado local se queda con la imagen sin
|
|
821
|
+
// su `id` de base de datos: la vista no muestra lo persistido y, al volver
|
|
822
|
+
// a guardar, el front la manda otra vez como nueva y el backend la omite
|
|
823
|
+
// por duplicada — así que el `src` nunca se actualizaba.
|
|
824
|
+
await loadData(false);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
/*
|
|
829
|
+
Trae la familia: los hijos del producto, o sus hermanos si el abierto es
|
|
830
|
+
una de ellas. Se pide al abrir la sección y no al montar la pantalla: la
|
|
831
|
+
mayoría de los productos no son familia.
|
|
832
|
+
*/
|
|
833
|
+
const loadFamily = async (forzar = false) => {
|
|
834
|
+
if (family && !forzar) return family;
|
|
835
|
+
try {
|
|
836
|
+
const res = await axios.post(
|
|
837
|
+
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
|
|
838
|
+
{ articleId: state.product?.id_article },
|
|
839
|
+
{ headers: { Authorization: token } },
|
|
840
|
+
);
|
|
841
|
+
const data = readPayload(res);
|
|
842
|
+
setFamily(data);
|
|
843
|
+
return data;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.error("loadFamily: ", error);
|
|
846
|
+
setFamily({ variants: [], parent: null, error: true });
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
/*
|
|
852
|
+
En Creators el parentesco se pregunta, no viene con el producto.
|
|
853
|
+
|
|
854
|
+
Su listado sale del endpoint de reportes, que no trae `variantsCount` ni
|
|
855
|
+
`parentArticleId`: esas columnas se agregaron a las vistas pero DynamoDB no
|
|
856
|
+
las tiene hasta que se corra el recorrido de datos. Y aunque las tuviera,
|
|
857
|
+
dependería de lo actualizado que esté ese recorrido.
|
|
858
|
+
|
|
859
|
+
Preguntar a `family` cuesta una llamada por ficha y da el dato de la base, no
|
|
860
|
+
de una copia. Además aplica el alcance del perfil (§5.1): al equipo interno le
|
|
861
|
+
devuelve los productos solicitados a ContentOh, que es justo lo que puede
|
|
862
|
+
tocar.
|
|
863
|
+
*/
|
|
864
|
+
useEffect(() => {
|
|
865
|
+
if (!state.product?.id_article) return;
|
|
866
|
+
loadFamily(true);
|
|
867
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
868
|
+
}, [state.product?.id_article]);
|
|
869
|
+
|
|
870
|
+
// El conteo sale de la familia recién consultada, no del producto.
|
|
871
|
+
const hijosDelProducto = family?.error
|
|
872
|
+
? 0
|
|
873
|
+
: (family?.variants ?? []).filter((v) => !v.isCurrent).length;
|
|
874
|
+
|
|
875
|
+
const esHijoDeOtro = family?.error
|
|
876
|
+
? !!state.product?.parentArticleId
|
|
877
|
+
: !!family?.parent;
|
|
878
|
+
|
|
879
|
+
/*
|
|
880
|
+
Un hijo no se agrupa como variante (decisión de producto, 2026-09-25).
|
|
881
|
+
|
|
882
|
+
Agrupar declara «estos son el mismo producto en otra presentación», y un
|
|
883
|
+
hijo ya declaró de dónde deriva. El servidor lo rechaza con
|
|
884
|
+
VARIANT_GROUP_NOT_FOR_CHILD, así que ofrecer el botón sería ofrecer algo
|
|
885
|
+
que siempre falla.
|
|
886
|
+
|
|
887
|
+
Se apoya en `esHijoDeOtro` y no en `state.product` porque en esta pantalla
|
|
888
|
+
el producto sale de sessionStorage: la señal buena es la familia recién
|
|
889
|
+
consultada, con el producto solo como respaldo si esa consulta falló.
|
|
890
|
+
*/
|
|
891
|
+
const puedeAgruparse = !esHijoDeOtro;
|
|
892
|
+
|
|
893
|
+
/*
|
|
894
|
+
Abre otro producto de la familia.
|
|
895
|
+
|
|
896
|
+
Cambiar la URL no basta: esta pantalla no lee el producto de la ruta sino de
|
|
897
|
+
sessionStorage —el router hace JSON.parse(sessionStorage.productSelected) y
|
|
898
|
+
se lo pasa como prop—, así que navegar sin precargar dejaría la URL nueva
|
|
899
|
+
mostrando el producto anterior. Por eso primero se trae el destino y se
|
|
900
|
+
escriben las tres llaves que el listado escribe al hacer clic en una fila
|
|
901
|
+
(productSelected, version, productEdit), y solo entonces se navega.
|
|
902
|
+
|
|
903
|
+
Se pide a /ordered-services y no a read-articles-v2, que es lo que usa la
|
|
904
|
+
aplicación de clientes: aquel filtra por la compañía del usuario y el equipo
|
|
905
|
+
interno es de ContentOh trabajando productos de sus clientes, así que ahí no
|
|
906
|
+
devolvería nada. /ordered-services es el mismo endpoint del listado de esta
|
|
907
|
+
aplicación, así que el objeto llega con la forma exacta que normalizeProduct
|
|
908
|
+
espera —con `article` anidado, `statusByRetailer` y `categoryRetailer`—.
|
|
909
|
+
|
|
910
|
+
Un producto de la familia puede no estar en ninguna orden de trabajo: la
|
|
911
|
+
relación padre/hijo vive en `article` y no exige haber pasado por ContentOh.
|
|
912
|
+
En ese caso no hay nada que abrir aquí, y se dice en vez de navegar a una
|
|
913
|
+
pantalla que mostraría el producto equivocado.
|
|
914
|
+
*/
|
|
915
|
+
/*
|
|
916
|
+
Después de desvincular, esta ficha cambia de forma: si se soltó de su padre
|
|
917
|
+
vuelve a poder ligar, y si soltó a un hijo pierde uno de la cuenta. En esta
|
|
918
|
+
pantalla el bloque de parentesco se decide con `family`, así que hay que
|
|
919
|
+
repreguntarla —vaciarla lo haría desaparecer, que es el error que ya se
|
|
920
|
+
cometió una vez aquí—.
|
|
921
|
+
*/
|
|
922
|
+
const handleOnUnlinked = (result) => {
|
|
923
|
+
if (!result) return;
|
|
924
|
+
|
|
925
|
+
const updatedProduct =
|
|
926
|
+
result.mode === "self"
|
|
927
|
+
? { ...state.product, parentArticleId: null, variantsCount: 0 }
|
|
928
|
+
: {
|
|
929
|
+
...state.product,
|
|
930
|
+
variantsCount: Math.max((state.product?.variantsCount ?? 1) - 1, 0),
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
934
|
+
|
|
935
|
+
// Se PARCHEA lo guardado: ahí vive el producto con la forma del listado
|
|
936
|
+
// —`article` anidado, servicios, cadenas— y escribir encima la versión
|
|
937
|
+
// aplanada de esta pantalla lo dejaría mutilado en el siguiente F5.
|
|
938
|
+
try {
|
|
939
|
+
const guardado = JSON.parse(
|
|
940
|
+
sessionStorage.getItem("productSelected") ?? "null",
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
if (guardado) {
|
|
944
|
+
sessionStorage.setItem(
|
|
945
|
+
"productSelected",
|
|
946
|
+
JSON.stringify({
|
|
947
|
+
...guardado,
|
|
948
|
+
variantsCount: updatedProduct.variantsCount,
|
|
949
|
+
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
950
|
+
}),
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.error("unlinkVariant/sessionStorage: ", error);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/*
|
|
958
|
+
Si ya no queda familia, se cierra el desplegable: su botón desaparece con
|
|
959
|
+
el vínculo, y dejarlo abierto pinta una caja que dice «no tiene hijos» y
|
|
960
|
+
que nadie puede cerrar.
|
|
961
|
+
*/
|
|
962
|
+
if (
|
|
963
|
+
!updatedProduct.parentArticleId &&
|
|
964
|
+
(updatedProduct.variantsCount ?? 0) === 0
|
|
965
|
+
) {
|
|
966
|
+
setShowFamily(false);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
loadFamily(true);
|
|
970
|
+
|
|
971
|
+
// Al desvincular el servidor pone `is_overridden = 0` en todas las
|
|
972
|
+
// versiones, así que los chips de herencia tienen que dejar de dibujarse.
|
|
973
|
+
// Sin `enableLoading` para no parpadear el esqueleto encima de una pantalla
|
|
974
|
+
// que ya está pintada.
|
|
975
|
+
loadData(false);
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
/*
|
|
979
|
+
Trae el grupo de variantes y sus miembros.
|
|
980
|
+
|
|
981
|
+
Al montar, no al abrir el desplegable: `variant_group` no lo devuelve el
|
|
982
|
+
endpoint del listado, así que sin esta llamada la ficha no sabe si el
|
|
983
|
+
producto está agrupado.
|
|
984
|
+
*/
|
|
985
|
+
const loadVariantGroup = async (forzar = false) => {
|
|
986
|
+
if (variantGroup && !forzar) return variantGroup;
|
|
987
|
+
try {
|
|
988
|
+
const res = await axios.post(
|
|
989
|
+
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/group`,
|
|
990
|
+
{ articleId: state.product?.id_article },
|
|
991
|
+
{ headers: { Authorization: token } },
|
|
992
|
+
);
|
|
993
|
+
const data = readPayload(res);
|
|
994
|
+
setVariantGroup(data);
|
|
995
|
+
return data;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
console.error("loadVariantGroup: ", error);
|
|
998
|
+
setVariantGroup({ group: null, members: [], error: true });
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
useEffect(() => {
|
|
1004
|
+
if (!state.product?.id_article) return;
|
|
1005
|
+
loadVariantGroup(true);
|
|
1006
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1007
|
+
}, [state.product?.id_article]);
|
|
1008
|
+
|
|
1009
|
+
const handleOnLeaveGroup = async () => {
|
|
1010
|
+
try {
|
|
1011
|
+
const res = await axios.post(
|
|
1012
|
+
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/leave`,
|
|
1013
|
+
{ articleId: state.product?.id_article },
|
|
1014
|
+
{ headers: { Authorization: token } },
|
|
1015
|
+
);
|
|
1016
|
+
setVariantGroup(readPayload(res));
|
|
1017
|
+
setShowGroup(false);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
console.error("leaveVariantGroup: ", error);
|
|
1020
|
+
dispatch({
|
|
1021
|
+
type: "SET_MODAL",
|
|
1022
|
+
payload: {
|
|
1023
|
+
show: true,
|
|
1024
|
+
title: "Variantes",
|
|
1025
|
+
message: "No se pudo quitar el producto del grupo. Intenta de nuevo.",
|
|
1026
|
+
image: warningIcon,
|
|
1027
|
+
buttons: [
|
|
1028
|
+
{
|
|
1029
|
+
text: "Entendido",
|
|
1030
|
+
buttonType: "general-white-button",
|
|
1031
|
+
action: () =>
|
|
1032
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } }),
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const openFamilyMember = async (articleId) => {
|
|
1041
|
+
setLoadingVariant(articleId);
|
|
1042
|
+
setAvisoFamilia(null);
|
|
1043
|
+
try {
|
|
1044
|
+
const res = await axios.get(
|
|
1045
|
+
`${process.env.REACT_APP_ORDERED_SERVICES_ENDPOINT}`,
|
|
1046
|
+
{
|
|
1047
|
+
params: { filterByIdArticle: articleId, includeFilters: false },
|
|
1048
|
+
headers: { Authorization: token },
|
|
1049
|
+
},
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
// read-order-services responde { total, data: [...] }. La vista trae una
|
|
1053
|
+
// fila por orden, así que puede haber varias del mismo artículo; llegan
|
|
1054
|
+
// ordenadas por order_id DESC y la primera es la orden más reciente.
|
|
1055
|
+
const { data } = readPayload(res) ?? {};
|
|
1056
|
+
const encontrado = (data ?? []).find(
|
|
1057
|
+
(item) => Number(item?.article?.id_article) === Number(articleId),
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
if (!encontrado) {
|
|
1061
|
+
setAvisoFamilia(
|
|
1062
|
+
"Ese producto no está en ninguna orden de trabajo, así que no se puede abrir desde aquí.",
|
|
1063
|
+
);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
sessionStorage.setItem("productSelected", JSON.stringify(encontrado));
|
|
1068
|
+
sessionStorage.setItem("version", JSON.stringify(encontrado.version));
|
|
1069
|
+
sessionStorage.setItem(
|
|
1070
|
+
"productEdit",
|
|
1071
|
+
JSON.stringify({
|
|
1072
|
+
ArticleId: encontrado.article.id_article,
|
|
1073
|
+
categoryId: encontrado.article.id_article,
|
|
1074
|
+
product: [encontrado],
|
|
1075
|
+
}),
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
window.location.assign("/EditProducts");
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.error("openFamilyMember: ", error);
|
|
1081
|
+
setAvisoFamilia("No se pudo abrir ese producto. Intenta de nuevo.");
|
|
1082
|
+
} finally {
|
|
1083
|
+
setLoadingVariant(null);
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
/*
|
|
1088
|
+
Refresca tras ligar, sin recargar la página: el producto sale de
|
|
1089
|
+
sessionStorage, así que un F5 devolvería el mismo objeto y el contador
|
|
1090
|
+
seguiría igual.
|
|
1091
|
+
*/
|
|
1092
|
+
const handleOnLinkedVariants = (result) => {
|
|
1093
|
+
if (!result) return;
|
|
1094
|
+
|
|
1095
|
+
const updatedProduct =
|
|
1096
|
+
result.mode === "children"
|
|
1097
|
+
? {
|
|
1098
|
+
...state.product,
|
|
1099
|
+
variantsCount:
|
|
1100
|
+
(state.product?.variantsCount ?? 0) +
|
|
1101
|
+
(result.linked ?? []).length,
|
|
1102
|
+
}
|
|
1103
|
+
: {
|
|
1104
|
+
...state.product,
|
|
1105
|
+
parentArticleId: result.parentArticleId ?? null,
|
|
1106
|
+
variantsCount: 0,
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
1110
|
+
|
|
1111
|
+
/*
|
|
1112
|
+
En sessionStorage se PARCHEA lo guardado; no se reemplaza.
|
|
1113
|
+
|
|
1114
|
+
Lo que hay ahí es el producto tal como lo devuelve el listado de esta
|
|
1115
|
+
aplicación: con `article` anidado, `statusByRetailer`, `categoryRetailer`
|
|
1116
|
+
y los servicios. `updatedProduct` es la versión aplanada que usa esta
|
|
1117
|
+
pantalla, y escribirla encima tiraba todo eso: al siguiente F5 la ficha se
|
|
1118
|
+
reconstruía desde un objeto mutilado —sin relaciones de cadena, sin
|
|
1119
|
+
estatus por cadena— aunque en pantalla se viera bien hasta entonces.
|
|
1120
|
+
|
|
1121
|
+
De ligar solo cambian dos cosas, y son las dos que se copian aquí.
|
|
1122
|
+
*/
|
|
1123
|
+
try {
|
|
1124
|
+
const guardado = JSON.parse(
|
|
1125
|
+
sessionStorage.getItem("productSelected") ?? "null",
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
if (guardado) {
|
|
1129
|
+
sessionStorage.setItem(
|
|
1130
|
+
"productSelected",
|
|
1131
|
+
JSON.stringify({
|
|
1132
|
+
...guardado,
|
|
1133
|
+
variantsCount: updatedProduct.variantsCount,
|
|
1134
|
+
parentArticleId: updatedProduct.parentArticleId ?? null,
|
|
1135
|
+
}),
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
// Un sessionStorage inaccesible no debe tumbar la pantalla: el estado en
|
|
1140
|
+
// memoria ya quedó bien y es lo que el usuario está viendo.
|
|
1141
|
+
console.error("linkVariants/sessionStorage: ", error);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/*
|
|
1145
|
+
Volver a preguntar, no vaciar.
|
|
1146
|
+
|
|
1147
|
+
Antes esto hacía `setFamily(null)` para invalidar la caché, y en esta
|
|
1148
|
+
pantalla eso tenía el efecto contrario al buscado: el bloque de parentesco
|
|
1149
|
+
se decide con `esHijoDeOtro`, que sale de `family`, no del producto. Con
|
|
1150
|
+
`family` en null el bloque desaparecía, así que después de ligar la
|
|
1151
|
+
pantalla se quedaba SIN ninguna señal de que el producto ya tenía padre
|
|
1152
|
+
—justo lo que el usuario acababa de hacer—.
|
|
1153
|
+
*/
|
|
1154
|
+
loadFamily(true);
|
|
1155
|
+
|
|
1156
|
+
/*
|
|
1157
|
+
Y se recargan los servicios, porque ligar cambia la herencia de cada campo
|
|
1158
|
+
en el servidor: al planchar, lo que era propio pasa a heredado y los chips
|
|
1159
|
+
tienen que decirlo. Sin esto la ficha sigue mostrando el estado anterior
|
|
1160
|
+
hasta que alguien cierra y vuelve a abrir el producto.
|
|
1161
|
+
|
|
1162
|
+
Sin `enableLoading` para no parpadear el esqueleto encima de una pantalla
|
|
1163
|
+
que ya está pintada.
|
|
1164
|
+
*/
|
|
1165
|
+
loadData(false);
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
/*
|
|
1169
|
+
Acción "Volver a heredar" de los chips. Repinta en sitio con lo que devuelve
|
|
1170
|
+
el endpoint: recargar la ficha se llevaría lo que haya sin guardar.
|
|
1171
|
+
*/
|
|
1172
|
+
/*
|
|
1173
|
+
`kind` distingue de qué pestaña salió el chip. Las descripciones viven en
|
|
1174
|
+
`rel_article_description` y la ficha técnica en `article_data`: el endpoint
|
|
1175
|
+
recibe una lista o la otra.
|
|
1176
|
+
*/
|
|
1177
|
+
const handleOnRestoreInheritance = async (
|
|
1178
|
+
attributeId,
|
|
1179
|
+
kind = "attribute",
|
|
1180
|
+
) => {
|
|
1181
|
+
if (!attributeId || !state.product?.id_article) return;
|
|
1182
|
+
|
|
1183
|
+
const esDescripcion = kind === "description";
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
const res = await axios.post(
|
|
1187
|
+
`${process.env.REACT_APP_VARIANTS_ENDPOINT}/restore`,
|
|
1188
|
+
{
|
|
1189
|
+
articleId: state.product.id_article,
|
|
1190
|
+
...(esDescripcion
|
|
1191
|
+
? { descriptions: [Number(attributeId)] }
|
|
1192
|
+
: { attributes: [Number(attributeId)] }),
|
|
1193
|
+
},
|
|
1194
|
+
{ headers: { Authorization: token } },
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
const data = readPayload(res);
|
|
1198
|
+
|
|
1199
|
+
if (esDescripcion) {
|
|
1200
|
+
dispatch({
|
|
1201
|
+
type: "UPDATE_DESCRIPTIONS_INPUTS",
|
|
1202
|
+
payload: (data.descriptionValues || []).map((item) => ({
|
|
1203
|
+
attributeId: item.id_description,
|
|
1204
|
+
value: item.value,
|
|
1205
|
+
isOverridden: 0,
|
|
1206
|
+
})),
|
|
1207
|
+
});
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
dispatch({
|
|
1212
|
+
type: "UPDATE_DATASHEETS_INPUTS",
|
|
1213
|
+
payload: (data.values || []).map((item) => ({
|
|
1214
|
+
attributeId: item.id,
|
|
1215
|
+
value: item.value,
|
|
1216
|
+
isOverridden: 0,
|
|
1217
|
+
})),
|
|
1218
|
+
});
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
console.error("restoreInheritance: ", error);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
/*
|
|
1225
|
+
Los atributos recién tocados, con el nombre que ve el usuario:
|
|
1226
|
+
`updated_datasheets_inputs` solo trae { attributeId, value }.
|
|
1227
|
+
*/
|
|
1228
|
+
const cambiosDeFichaTecnica = () => {
|
|
1229
|
+
const retailerId = state.active_retailer?.id_retailer;
|
|
1230
|
+
const inputs = state.services?.datasheets?.inputs ?? {};
|
|
1231
|
+
|
|
1232
|
+
const cambios = (state.updated_datasheets_inputs ?? []).map((item) => {
|
|
1233
|
+
const input = inputs[`${item.attributeId}_${retailerId}`];
|
|
1234
|
+
return {
|
|
1235
|
+
id: Number(item.attributeId),
|
|
1236
|
+
name: input?.name ?? `Atributo ${item.attributeId}`,
|
|
1237
|
+
value: item.value,
|
|
1238
|
+
};
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
/*
|
|
1242
|
+
UPC y SKU se quitan aquí y no dentro del diálogo porque esta lista también
|
|
1243
|
+
decide SI el diálogo se abre. Editar solo el UPC no tiene nada que
|
|
1244
|
+
propagar, así que preguntarlo sería pedirle al usuario una decisión sobre
|
|
1245
|
+
un conjunto vacío.
|
|
1246
|
+
*/
|
|
1247
|
+
return soloPropagables(cambios);
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
/*
|
|
1251
|
+
Lo que se acaba de editar en la pestaña de descripciones.
|
|
1252
|
+
|
|
1253
|
+
Va aparte de `cambiosDeFichaTecnica` porque son tablas distintas y el
|
|
1254
|
+
endpoint recibe una lista o la otra. Aquí NO se filtran UPC ni SKU: esos son
|
|
1255
|
+
atributos de ficha técnica y no aparecen en esta pestaña.
|
|
1256
|
+
*/
|
|
1257
|
+
const cambiosDeDescripciones = () => {
|
|
1258
|
+
const retailerId = state.active_retailer?.id_retailer;
|
|
1259
|
+
const inputs = state.services?.descriptions?.inputs ?? {};
|
|
1260
|
+
|
|
1261
|
+
return (state.updated_descriptions_inputs ?? []).map((item) => {
|
|
1262
|
+
const input = inputs[`${item.attributeId}_${retailerId}`];
|
|
1263
|
+
return {
|
|
1264
|
+
id: Number(item.attributeId),
|
|
1265
|
+
name: input?.name ?? `Descripción ${item.attributeId}`,
|
|
1266
|
+
value: item.value,
|
|
1267
|
+
};
|
|
1268
|
+
});
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const handleOnClickSave = async () => {
|
|
1272
|
+
if (state.saving || preparandoPropagacion) return;
|
|
1273
|
+
|
|
1274
|
+
switch (state.active_tab) {
|
|
1275
|
+
case "Descripción": {
|
|
1276
|
+
/*
|
|
1277
|
+
Guardar una descripción en un padre también abre la decisión de
|
|
1278
|
+
propagar. Sin esto, el hijo heredaba la descripción al ligarse y se
|
|
1279
|
+
quedaba con la versión vieja en cuanto el padre la corregía, sin que
|
|
1280
|
+
nadie lo notara.
|
|
1281
|
+
*/
|
|
1282
|
+
const cambiosDesc = cambiosDeDescripciones();
|
|
1283
|
+
const companerasDesc = (variantGroup?.members ?? []).filter(
|
|
1284
|
+
(m) => !m.isCurrent
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
if (esHijoDeOtro || cambiosDesc.length === 0) {
|
|
1288
|
+
await saveDescriptions(token);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
setPreparandoPropagacion(true);
|
|
1293
|
+
let familiaDesc = null;
|
|
1294
|
+
try {
|
|
1295
|
+
familiaDesc = await loadFamily(true);
|
|
1296
|
+
} finally {
|
|
1297
|
+
setPreparandoPropagacion(false);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const hijosDesc = (familiaDesc?.variants ?? []).filter(
|
|
1301
|
+
(v) => !v.isCurrent
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
if (hijosDesc.length > 0 || companerasDesc.length > 0) {
|
|
1305
|
+
setPendingPropagation({ cambios: cambiosDesc, kind: "description" });
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
await saveDescriptions(token);
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
case "Ficha técnica": {
|
|
1313
|
+
const cambios = cambiosDeFichaTecnica();
|
|
1314
|
+
|
|
1315
|
+
if (cambios.length === 0) {
|
|
1316
|
+
await saveDatasheets(token);
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/*
|
|
1321
|
+
Guardar un padre con hijos abre la decisión de propagar. Sin este paso
|
|
1322
|
+
el cambio se queda solo en el padre y los hijos se van separando de él
|
|
1323
|
+
sin que nadie lo note.
|
|
1324
|
+
|
|
1325
|
+
Si tiene hijos se resuelve AQUÍ, esperando la respuesta, en vez de
|
|
1326
|
+
mirar la copia en memoria. `family` es una consulta asíncrona: si
|
|
1327
|
+
todavía no llegó, o si falló, `hijosDelProducto` vale 0 — o sea que
|
|
1328
|
+
«todavía no sé» se leía igual que «no tiene hijos», y el padre se
|
|
1329
|
+
guardaba sin ofrecer la propagación. En silencio, que es lo peor: el
|
|
1330
|
+
usuario creía haber bajado el cambio a los hijos.
|
|
1331
|
+
|
|
1332
|
+
La ficha de la aplicación de clientes no tiene el problema porque ahí
|
|
1333
|
+
`state.product.variantsCount` viene del listado; aquí el producto sale
|
|
1334
|
+
del endpoint de reportes, que no trae el parentesco.
|
|
1335
|
+
*/
|
|
1336
|
+
setPreparandoPropagacion(true);
|
|
1337
|
+
let familia = null;
|
|
1338
|
+
try {
|
|
1339
|
+
familia = await loadFamily(true);
|
|
1340
|
+
} finally {
|
|
1341
|
+
setPreparandoPropagacion(false);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// No se pudo saber si tiene hijos. Guardar de todas formas repetiría el
|
|
1345
|
+
// defecto que se está corrigiendo, así que se dice y se deja la decisión.
|
|
1346
|
+
if (!familia || familia.error) {
|
|
1347
|
+
dispatch({
|
|
1348
|
+
type: "SET_MODAL",
|
|
1349
|
+
payload: {
|
|
1350
|
+
show: true,
|
|
1351
|
+
title: "No se pudo consultar la familia",
|
|
1352
|
+
message:
|
|
1353
|
+
"No pudimos saber si este producto tiene hijos, así que no sabemos si hay que bajarles el cambio. Vuelve a intentar el guardado; si sigue fallando, guarda y revisa los hijos a mano.",
|
|
1354
|
+
image: warningIcon,
|
|
1355
|
+
buttons: [
|
|
1356
|
+
{
|
|
1357
|
+
text: "Entendido",
|
|
1358
|
+
buttonType: "general-white-button",
|
|
1359
|
+
action: () =>
|
|
1360
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } }),
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
text: "Guardar solo este producto",
|
|
1364
|
+
buttonType: "general-button-default",
|
|
1365
|
+
action: () => {
|
|
1366
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } });
|
|
1367
|
+
saveDatasheets(token);
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
],
|
|
1371
|
+
},
|
|
1372
|
+
});
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/*
|
|
1377
|
+
Un HIJO no propaga a nadie, y hay que decirlo explícitamente.
|
|
1378
|
+
|
|
1379
|
+
`familia.variants` mira la familia desde el PADRE —eso es lo correcto
|
|
1380
|
+
para navegar entre hermanos— así que desde la ficha de un hijo esa
|
|
1381
|
+
lista son sus HERMANOS. Leerla como «los destinos de este producto»
|
|
1382
|
+
hacía que el diálogo le ofreciera heredar a sus hermanos. El servidor
|
|
1383
|
+
resolvía los destinos por su cuenta y no escribía nada, de modo que el
|
|
1384
|
+
usuario elegía, guardaba, y no pasaba nada: un no-operativo silencioso.
|
|
1385
|
+
*/
|
|
1386
|
+
const esHijo = !!familia?.parent;
|
|
1387
|
+
const hijos = esHijo
|
|
1388
|
+
? []
|
|
1389
|
+
: (familia.variants ?? []).filter((v) => !v.isCurrent);
|
|
1390
|
+
|
|
1391
|
+
/*
|
|
1392
|
+
También cuentan las compañeras de grupo: un producto puede no tener
|
|
1393
|
+
hijos y aun así pertenecer a un grupo de variantes, y el cambio se les
|
|
1394
|
+
puede copiar igual. Si no se miraran, guardar no ofrecería nada.
|
|
1395
|
+
*/
|
|
1396
|
+
const companeras = (variantGroup?.members ?? []).filter(
|
|
1397
|
+
(m) => !m.isCurrent,
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// Un hijo tampoco puede estar agrupado desde 2026-09-25, así que para
|
|
1401
|
+
// él no queda ningún destino posible.
|
|
1402
|
+
if (!esHijo && (hijos.length > 0 || companeras.length > 0)) {
|
|
1403
|
+
setPendingPropagation({ cambios, kind: "datasheet" });
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
await saveDatasheets(token);
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
case "Imágenes":
|
|
1411
|
+
/*
|
|
1412
|
+
Se guarda la bandeja que el usuario tiene a la vista. Guardar las dos
|
|
1413
|
+
de golpe haría que, estando en activos, `updateImages` enviara también
|
|
1414
|
+
los cambios de imágenes que el usuario quizá no dio por terminados —y
|
|
1415
|
+
al revés—. El botón guarda lo que se está viendo.
|
|
1416
|
+
*/
|
|
1417
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
1418
|
+
await handleOnSaveAssets();
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
await updateImages(token);
|
|
1422
|
+
break;
|
|
1423
|
+
default:
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
await loadData(false);
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
const handleOnSetUpdatedDescriptions = (items) => {
|
|
1431
|
+
dispatch({
|
|
1432
|
+
type: "SET_UPDATED_DESCRIPTIONS_INPUTS",
|
|
1433
|
+
payload: items,
|
|
1434
|
+
});
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
const handleOnSetUpdatedDatasheets = (items) => {
|
|
1438
|
+
dispatch({
|
|
1439
|
+
type: "SET_UPDATED_DATASHEETS_INPUTS",
|
|
1440
|
+
payload: items,
|
|
1441
|
+
});
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
/*
|
|
1445
|
+
Descarga de activos: los seleccionados, o todos si no hay selección — el
|
|
1446
|
+
mismo criterio que usa la descarga de imágenes.
|
|
1447
|
+
|
|
1448
|
+
Solo se descarga lo que ya está en S3. Un activo recién soltado vive como
|
|
1449
|
+
referencia al archivo del disco del usuario: "descargarlo" le devolvería el
|
|
1450
|
+
archivo que él mismo acaba de elegir, lo cual no tiene sentido.
|
|
1451
|
+
*/
|
|
1452
|
+
const handleOnDownloadAssets = () => {
|
|
1453
|
+
const seleccion = state.selected_assets.length
|
|
1454
|
+
? state.selected_assets
|
|
1455
|
+
: state.assets_values;
|
|
1456
|
+
|
|
1457
|
+
const descargables = seleccion.filter((asset) => asset.id && asset.url);
|
|
1458
|
+
|
|
1459
|
+
if (descargables.length === 0) {
|
|
1460
|
+
dispatch({
|
|
1461
|
+
type: "SET_MODAL",
|
|
1462
|
+
payload: {
|
|
1463
|
+
show: true,
|
|
1464
|
+
title: "Nada que descargar",
|
|
1465
|
+
message:
|
|
1466
|
+
"Los activos seleccionados todavía no se han guardado. Guárdalos primero.",
|
|
1467
|
+
image: warningIcon,
|
|
1468
|
+
},
|
|
1469
|
+
});
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
descargables.forEach((asset) => saveAs(asset.url, asset.name));
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
/*
|
|
1477
|
+
Borrado en lote de activos.
|
|
1478
|
+
|
|
1479
|
+
A diferencia de las imágenes, que se borran contra el backend en el momento,
|
|
1480
|
+
los activos se quitan de la bandeja y su baja viaja en el mismo PUT del
|
|
1481
|
+
guardado (`deleteAssets`). Es lo que permite al usuario deshacerse de un
|
|
1482
|
+
borrado por error: basta con no guardar. El mensaje lo dice, para que nadie
|
|
1483
|
+
se quede pensando que ya se aplicó.
|
|
1484
|
+
*/
|
|
1485
|
+
const handleOnAskToDeleteAssets = () => {
|
|
1486
|
+
if (state.selected_assets.length === 0) {
|
|
1487
|
+
dispatch({
|
|
1488
|
+
type: "SET_MODAL",
|
|
1489
|
+
payload: {
|
|
1490
|
+
show: true,
|
|
1491
|
+
title: "Eliminar activos digitales",
|
|
1492
|
+
message: "No has seleccionado ningún activo para eliminar.",
|
|
1493
|
+
image: warningIcon,
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const seleccionados = state.selected_assets;
|
|
1500
|
+
|
|
1501
|
+
dispatch({
|
|
1502
|
+
type: "SET_MODAL",
|
|
1503
|
+
payload: {
|
|
1504
|
+
show: true,
|
|
1505
|
+
title: "Eliminar activos digitales",
|
|
1506
|
+
message: `Se quitarán ${seleccionados.length} activo(s) de la bandeja. El borrado se aplica al guardar.`,
|
|
1507
|
+
image: warningIcon,
|
|
1508
|
+
buttons: [
|
|
1509
|
+
{
|
|
1510
|
+
text: "Cancelar",
|
|
1511
|
+
buttonType: "general-white-button",
|
|
1512
|
+
action: () =>
|
|
1513
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } }),
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
text: "Eliminar",
|
|
1517
|
+
buttonType: "general-button-default",
|
|
1518
|
+
action: () => {
|
|
1519
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } });
|
|
1520
|
+
seleccionados.forEach((asset) =>
|
|
1521
|
+
digitalAssets.removeAsset(asset),
|
|
1522
|
+
);
|
|
1523
|
+
},
|
|
1524
|
+
},
|
|
1525
|
+
],
|
|
1526
|
+
},
|
|
1527
|
+
});
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
const handleOnAskToDeleteImages = () => {
|
|
1531
|
+
if (currentTray === TRAYS.ASSETS) {
|
|
1532
|
+
handleOnAskToDeleteAssets();
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (state.selected_images.length === 0) {
|
|
1537
|
+
dispatch({
|
|
1538
|
+
type: "SET_MODAL",
|
|
1539
|
+
payload: {
|
|
1540
|
+
show: true,
|
|
1541
|
+
title: "Eliminar imágenes",
|
|
1542
|
+
message: "No has seleccionado ninguna imagen para eliminar.",
|
|
1543
|
+
image: warningIcon,
|
|
1544
|
+
},
|
|
1545
|
+
});
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
dispatch({
|
|
1550
|
+
type: "SET_MODAL",
|
|
1551
|
+
payload: {
|
|
1552
|
+
show: true,
|
|
1553
|
+
title: "Eliminar imágenes",
|
|
1554
|
+
message:
|
|
1555
|
+
"¿Estás seguro de que deseas eliminar las imágenes seleccionadas?",
|
|
1556
|
+
image: warningIcon,
|
|
1557
|
+
buttons: [
|
|
1558
|
+
{
|
|
1559
|
+
text: "Cancelar",
|
|
1560
|
+
buttonType: "general-white-button",
|
|
1561
|
+
action: () =>
|
|
1562
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } }),
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
text: "Eliminar",
|
|
1566
|
+
buttonType: "general-button-default",
|
|
1567
|
+
action: () => {
|
|
1568
|
+
dispatch({ type: "SET_MODAL", payload: { show: false } });
|
|
1569
|
+
deleteImages(token);
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
],
|
|
1573
|
+
},
|
|
1574
|
+
});
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
const handleOnApproveSingleService = async () => {
|
|
1578
|
+
await sendSingleEvaluation("A");
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
const handleOnRejectSingleService = () => {
|
|
1582
|
+
setShowRejectModal(true);
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
const sendSingleEvaluation = async (result) => {
|
|
1586
|
+
if (state.saving) return;
|
|
1587
|
+
|
|
1588
|
+
dispatch({ type: "SET_SAVING", payload: true });
|
|
1589
|
+
|
|
1590
|
+
const concept = getConceptByTab(state.active_tab);
|
|
1591
|
+
|
|
1592
|
+
const articleId = state.product.id_article;
|
|
1593
|
+
const orderId = state.product.id_order ?? state.product.orderId;
|
|
1594
|
+
const sectionStatusKey = `${concept}_status`;
|
|
1595
|
+
const evalStatus = state.product[sectionStatusKey];
|
|
1596
|
+
const retailerId = state.active_retailer?.id_retailer;
|
|
1597
|
+
|
|
1598
|
+
let data = { articleId, orderId, concept, evalStatus, retailerId };
|
|
1599
|
+
let res;
|
|
1600
|
+
let message;
|
|
1601
|
+
let icon;
|
|
1602
|
+
|
|
1603
|
+
const activeTab = state.active_tab;
|
|
1604
|
+
|
|
1605
|
+
try {
|
|
1606
|
+
//Esto solo se ejecutará cuando se aprueba o rechaza desde el modal superior derecho
|
|
1607
|
+
if (result) {
|
|
1608
|
+
data.result = result;
|
|
1609
|
+
data.retailerId = retailerId;
|
|
1610
|
+
|
|
1611
|
+
res = await axios.put(
|
|
1612
|
+
`${process.env.REACT_APP_EVALUATION_ENDPOINT}`,
|
|
1613
|
+
data,
|
|
1614
|
+
{
|
|
1615
|
+
headers: {
|
|
1616
|
+
Authorization: token,
|
|
1617
|
+
},
|
|
1618
|
+
},
|
|
1619
|
+
);
|
|
1620
|
+
|
|
1621
|
+
const newStatuses = JSON.parse(res.data.body);
|
|
1622
|
+
const serviceStatus =
|
|
1623
|
+
newStatuses.newServiceStatus[articleId][`${concept}Status`];
|
|
1624
|
+
const articleStatus = newStatuses.newArticleStatus[articleId];
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
// Actualizar el producto con los nuevos estados
|
|
1628
|
+
const updatedStatusByRetailer = state.product.statusByRetailer.map(
|
|
1629
|
+
(item) =>
|
|
1630
|
+
item.retailer_id === retailerId && item.service === concept
|
|
1631
|
+
? { ...item, status: serviceStatus }
|
|
1632
|
+
: item,
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
const updatedProduct = {
|
|
1636
|
+
...state.product,
|
|
1637
|
+
statusByRetailer: updatedStatusByRetailer,
|
|
1638
|
+
status: getMinorStatusFromRetailer(updatedStatusByRetailer),
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
1642
|
+
|
|
1643
|
+
// Mostrar modal de éxito
|
|
1644
|
+
dispatch({
|
|
1645
|
+
type: "SET_MODAL",
|
|
1646
|
+
payload: {
|
|
1647
|
+
show: true,
|
|
1648
|
+
title: "",
|
|
1649
|
+
message:
|
|
1650
|
+
result === "A" ? "Aprobado con éxito" : "Rechazado con éxito",
|
|
1651
|
+
image: successIcon,
|
|
1652
|
+
},
|
|
1653
|
+
});
|
|
1654
|
+
} else {
|
|
1655
|
+
//Caso del botón "Enviar evaluación"
|
|
1656
|
+
|
|
1657
|
+
//Se construye el mensaje y se actualiza el estatus
|
|
1658
|
+
|
|
1659
|
+
const specialistDone = ["RC", "RA", "CA"].includes(evalStatus);
|
|
1660
|
+
|
|
1661
|
+
if (specialistDone) {
|
|
1662
|
+
message = `${activeTab} enviada a facilitador`;
|
|
1663
|
+
icon =
|
|
1664
|
+
activeTab === "Descripción"
|
|
1665
|
+
? descriptionSent
|
|
1666
|
+
: activeTab === "Ficha técnica"
|
|
1667
|
+
? attributesSent
|
|
1668
|
+
: imagesSent;
|
|
1669
|
+
} else if (["IE", "AC", "RP", "RCA"].includes(evalStatus)) {
|
|
1670
|
+
message = "Evaluación enviada";
|
|
1671
|
+
icon = successIcon;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
res = await axios.put(`${process.env.REACT_APP_SEND_EVAL}`, data, {
|
|
1675
|
+
headers: {
|
|
1676
|
+
Authorization: token,
|
|
1677
|
+
},
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (res.data.statusCode === 200) {
|
|
1682
|
+
const { newStatus, newOrderStatus, newArticleStatus } = JSON.parse(
|
|
1683
|
+
res.data.body,
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1686
|
+
const articleStatus = newArticleStatus[articleId];
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
// const retailers = state.product.categoryRetailer.map((r) => ({
|
|
1690
|
+
// id: r.id_retailer,
|
|
1691
|
+
// name: r.retailerName,
|
|
1692
|
+
// }));
|
|
1693
|
+
// const messageToChat = createMessage(
|
|
1694
|
+
// retailers,
|
|
1695
|
+
// retailerId,
|
|
1696
|
+
// evalStatus,
|
|
1697
|
+
// newStatus,
|
|
1698
|
+
// activeTab,
|
|
1699
|
+
// );
|
|
1700
|
+
|
|
1701
|
+
// const messageData = {
|
|
1702
|
+
// paramsBody: {
|
|
1703
|
+
// id: articleId,
|
|
1704
|
+
// version: state.product.version,
|
|
1705
|
+
// items: [{ type: "status", value: messageToChat }],
|
|
1706
|
+
// retailerId: retailerId,
|
|
1707
|
+
// status: state.product.status,
|
|
1708
|
+
// },
|
|
1709
|
+
// paramsHeader: { Authorization: token },
|
|
1710
|
+
// };
|
|
1711
|
+
// await sendMessage(messageData);
|
|
1712
|
+
|
|
1713
|
+
const updatedStatusByRetailerForSend =
|
|
1714
|
+
state.product.statusByRetailer.map((item) => {
|
|
1715
|
+
if (item.retailer_id === retailerId && item.service === concept) {
|
|
1716
|
+
return { ...item, status: newStatus };
|
|
1717
|
+
}
|
|
1718
|
+
return item;
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
const updatedProduct = {
|
|
1722
|
+
...state.product,
|
|
1723
|
+
[`${concept}_status`]: newStatus,
|
|
1724
|
+
statusByRetailer: updatedStatusByRetailerForSend,
|
|
1725
|
+
status: getMinorStatusFromRetailer(updatedStatusByRetailerForSend),
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
//Actualizamos el services_data en el servicio específico del retailer actual
|
|
1729
|
+
|
|
1730
|
+
const updatedServicesData = state.services_data?.map((service) => ({
|
|
1731
|
+
...service,
|
|
1732
|
+
status:
|
|
1733
|
+
service?.id_retailer === retailerId && service?.service === concept
|
|
1734
|
+
? newStatus
|
|
1735
|
+
: service?.status,
|
|
1736
|
+
}));
|
|
1737
|
+
|
|
1738
|
+
const sessionProduct = sessionStorage.getItem("productSelected");
|
|
1739
|
+
|
|
1740
|
+
if (sessionProduct) {
|
|
1741
|
+
const productInSession = JSON.parse(sessionProduct);
|
|
1742
|
+
const updatedProductInSession = {
|
|
1743
|
+
...productInSession,
|
|
1744
|
+
[`${concept}_status`]: newStatus,
|
|
1745
|
+
statusByRetailer: state.product.statusByRetailer.map((item) => {
|
|
1746
|
+
if (item.retailer_id === retailerId && item.service === concept) {
|
|
1747
|
+
return { ...item, status: newStatus };
|
|
1748
|
+
}
|
|
1749
|
+
return item;
|
|
1750
|
+
}),
|
|
1751
|
+
};
|
|
1752
|
+
sessionStorage.setItem(
|
|
1753
|
+
"productSelected",
|
|
1754
|
+
JSON.stringify(updatedProductInSession),
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (message) {
|
|
1759
|
+
dispatch({
|
|
1760
|
+
type: "SET_MODAL",
|
|
1761
|
+
payload: {
|
|
1762
|
+
show: true,
|
|
1763
|
+
title: "",
|
|
1764
|
+
message: message,
|
|
1765
|
+
image: successIcon,
|
|
1766
|
+
},
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
1771
|
+
dispatch({ type: "SET_SERVICES_DATA", payload: updatedServicesData });
|
|
1772
|
+
}
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
console.error("Error sending evaluation:", error);
|
|
1775
|
+
dispatch({
|
|
1776
|
+
type: "SET_MODAL",
|
|
1777
|
+
payload: {
|
|
1778
|
+
show: true,
|
|
1779
|
+
title: "Error",
|
|
1780
|
+
message: "Hubo un error al procesar la evaluación",
|
|
1781
|
+
image: errorIcon,
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
} finally {
|
|
1785
|
+
dispatch({ type: "SET_SAVING", payload: false });
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const handleOnApproveAllServices = async () => {
|
|
1790
|
+
await sendBulkEvaluation("A");
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
const handleOnRejectAllServices = () => {
|
|
1794
|
+
setRejectAll(true);
|
|
1795
|
+
setShowRejectModal(true);
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
const sendBulkEvaluation = async (result) => {
|
|
1799
|
+
dispatch({ type: "SET_SAVING", payload: true });
|
|
1800
|
+
try {
|
|
1801
|
+
const evaluationArray = [];
|
|
1802
|
+
|
|
1803
|
+
const conceptArray = getConceptsByRole(user.id_role);
|
|
1804
|
+
|
|
1805
|
+
state.services_data?.forEach((ret, idx) => {
|
|
1806
|
+
const {
|
|
1807
|
+
service: retailer_service,
|
|
1808
|
+
id_retailer,
|
|
1809
|
+
status: status_retailer_service,
|
|
1810
|
+
} = ret;
|
|
1811
|
+
|
|
1812
|
+
const lastStatusLevel = status_retailer_service?.replace(/.*\//, "");
|
|
1813
|
+
|
|
1814
|
+
if (
|
|
1815
|
+
conceptArray.includes(retailer_service) &&
|
|
1816
|
+
statusArray?.includes(lastStatusLevel)
|
|
1817
|
+
) {
|
|
1818
|
+
const data = {
|
|
1819
|
+
articleId: state.product.id_article,
|
|
1820
|
+
orderId: state.product.id_order,
|
|
1821
|
+
concept: retailer_service,
|
|
1822
|
+
result,
|
|
1823
|
+
evalStatus: status_retailer_service,
|
|
1824
|
+
retailerId: id_retailer,
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
evaluationArray.push(
|
|
1828
|
+
axios.put(`${process.env.REACT_APP_EVALUATION_ENDPOINT}`, data, {
|
|
1829
|
+
headers: {
|
|
1830
|
+
Authorization: token,
|
|
1831
|
+
},
|
|
1832
|
+
}),
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
await Promise.all(evaluationArray);
|
|
1838
|
+
|
|
1839
|
+
// Sin una sola petición no hubo evaluación. Seguir de largo es lo que
|
|
1840
|
+
// pintaba el estatus nuevo y anunciaba éxito mientras la base no se
|
|
1841
|
+
// enteraba: el cambio se veía en pantalla y no existía. Un no-op tiene
|
|
1842
|
+
// que decirlo, no fingir que funcionó.
|
|
1843
|
+
if (evaluationArray.length === 0) {
|
|
1844
|
+
dispatch({
|
|
1845
|
+
type: "SET_MODAL",
|
|
1846
|
+
payload: {
|
|
1847
|
+
show: true,
|
|
1848
|
+
title: "Error",
|
|
1849
|
+
message:
|
|
1850
|
+
"No hay servicios que puedas evaluar en el estatus actual.",
|
|
1851
|
+
image: errorIcon,
|
|
1852
|
+
},
|
|
1853
|
+
});
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const status = `${result}A`;
|
|
1858
|
+
|
|
1859
|
+
// Actualizar el statusByRetailer para todos los retailers y conceptos
|
|
1860
|
+
const updatedStatusByRetailer = state.product.statusByRetailer.map(
|
|
1861
|
+
(item) =>
|
|
1862
|
+
conceptArray.includes(item.service) ? { ...item, status } : item,
|
|
1863
|
+
);
|
|
1864
|
+
|
|
1865
|
+
// Actualizar el producto con los nuevos estados
|
|
1866
|
+
const updatedProduct = {
|
|
1867
|
+
...state.product,
|
|
1868
|
+
article_status: status,
|
|
1869
|
+
status: getMinorStatusFromRetailer(updatedStatusByRetailer),
|
|
1870
|
+
datasheet_status:
|
|
1871
|
+
state.product.datasheet_status === "NA" ? "NA" : status,
|
|
1872
|
+
description_status:
|
|
1873
|
+
state.product.description_status === "NA" ? "NA" : status,
|
|
1874
|
+
images_status: state.product.images_status === "NA" ? "NA" : status,
|
|
1875
|
+
statusByRetailer: updatedStatusByRetailer,
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
dispatch({
|
|
1879
|
+
type: "SET_PRODUCT",
|
|
1880
|
+
payload: updatedProduct,
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
const updatedServicesData = state.services_data.map((service) =>
|
|
1884
|
+
conceptArray.includes(service.service)
|
|
1885
|
+
? { ...service, status: status }
|
|
1886
|
+
: service,
|
|
1887
|
+
);
|
|
1888
|
+
|
|
1889
|
+
dispatch({
|
|
1890
|
+
type: "SET_SERVICES_DATA",
|
|
1891
|
+
payload: updatedServicesData,
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
// sessionStorage.setItem(
|
|
1895
|
+
// "productSelected",
|
|
1896
|
+
// JSON.stringify(updatedProduct),
|
|
1897
|
+
// );
|
|
1898
|
+
// sessionStorage.setItem(
|
|
1899
|
+
// "productEdit",
|
|
1900
|
+
// JSON.stringify({
|
|
1901
|
+
// ArticleId: updatedProduct.id_article,
|
|
1902
|
+
// idCategory: updatedProduct.id_category,
|
|
1903
|
+
// product: updatedProduct,
|
|
1904
|
+
// }),
|
|
1905
|
+
// );
|
|
1906
|
+
|
|
1907
|
+
// Mostrar modal de éxito
|
|
1908
|
+
dispatch({
|
|
1909
|
+
type: "SET_MODAL",
|
|
1910
|
+
payload: {
|
|
1911
|
+
show: true,
|
|
1912
|
+
title: "",
|
|
1913
|
+
message:
|
|
1914
|
+
result === "A"
|
|
1915
|
+
? "Todos los servicios aprobados con éxito"
|
|
1916
|
+
: "Todos los servicios rechazados con éxito",
|
|
1917
|
+
image: successIcon,
|
|
1918
|
+
},
|
|
1919
|
+
});
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
console.error("Error in bulk evaluation:", error);
|
|
1922
|
+
} finally {
|
|
1923
|
+
dispatch({ type: "SET_SAVING", payload: false });
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
const handleOnChangeAssignations = async (assignationType, assignationId) => {
|
|
1928
|
+
const concept = getConceptByTab(state.active_tab);
|
|
1929
|
+
const data = {
|
|
1930
|
+
articleList: [
|
|
1931
|
+
{
|
|
1932
|
+
orderId: state.product.id_order,
|
|
1933
|
+
articleId: state.product.id_article,
|
|
1934
|
+
},
|
|
1935
|
+
],
|
|
1936
|
+
concept: concept,
|
|
1937
|
+
userId: assignationId,
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
await axios({
|
|
1941
|
+
method: "post",
|
|
1942
|
+
url: process.env.REACT_APP_ASSIGNATIONS_ENDPOINT,
|
|
1943
|
+
data,
|
|
1944
|
+
headers: {
|
|
1945
|
+
Authorization: token,
|
|
1946
|
+
},
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
const updatedProduct = {
|
|
1950
|
+
...state.product,
|
|
1951
|
+
[`id_${concept}_${assignationType}`]: assignationId,
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
const handleOnSetAssignation = () => {
|
|
1958
|
+
console.log("handleOnSetAssignation - pendiente de implementar");
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
const handleOnSetImages = () => {
|
|
1962
|
+
console.log("handleOnSetImages - pendiente de implementar");
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
const handleOnSetCheckAll = (isChecked) => {
|
|
1966
|
+
dispatch({ type: "TOGGLE_CHECK_ALL_IMAGES", payload: isChecked });
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1969
|
+
const handleOnSetSelectedImages = () => {
|
|
1970
|
+
/*
|
|
1971
|
+
Esta función no se utiliza actualmente.
|
|
1972
|
+
|
|
1973
|
+
En la versión anterior se usaban dos funciones distintas para el checkbox
|
|
1974
|
+
"Seleccionar todo":
|
|
1975
|
+
- Una para cambiar el valor booleano del checkbox.
|
|
1976
|
+
- Otra para actualizar el array de imágenes seleccionadas.
|
|
1977
|
+
|
|
1978
|
+
En la versión actual se unificó la lógica en una sola función,
|
|
1979
|
+
`handleOnSetCheckAll`, que recibe el valor `true` o `false` del checkbox
|
|
1980
|
+
"Seleccionar todo" y, a partir de este, agrega o elimina todas las imágenes
|
|
1981
|
+
del array de imágenes seleccionadas.
|
|
1982
|
+
*/
|
|
1983
|
+
// Sin uso: la selección se maneja con handleOnSetCheckAll.
|
|
1984
|
+
};
|
|
1985
|
+
const handleOnSubmitComment = async (e) => {
|
|
1986
|
+
e.preventDefault();
|
|
1987
|
+
const commentText = document.querySelector(
|
|
1988
|
+
"#commentary-box .ql-container .ql-editor > p",
|
|
1989
|
+
).innerHTML;
|
|
1990
|
+
try {
|
|
1991
|
+
await createComment(commentText, token);
|
|
1992
|
+
|
|
1993
|
+
dispatch({
|
|
1994
|
+
type: "SET_MODAL",
|
|
1995
|
+
payload: {
|
|
1996
|
+
show: true,
|
|
1997
|
+
title: "",
|
|
1998
|
+
message: "Comentario guardado con éxito",
|
|
1999
|
+
image: successIcon,
|
|
2000
|
+
},
|
|
2001
|
+
});
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
console.error("Error saving comment:", error);
|
|
2004
|
+
dispatch({
|
|
2005
|
+
type: "SET_MODAL",
|
|
2006
|
+
payload: {
|
|
2007
|
+
show: true,
|
|
2008
|
+
title: "Error",
|
|
2009
|
+
message: "Hubo un error al guardar el comentario",
|
|
2010
|
+
image: errorIcon,
|
|
2011
|
+
},
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
const handleOnSendEvaluationToFacilitator = () => {
|
|
2017
|
+
sendSingleEvaluation();
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
const isEvaluationFinished = (id_rol, tab, statusArray) => {
|
|
2021
|
+
const servicesData = state.services_data;
|
|
2022
|
+
const activeRetailer = state.active_retailer;
|
|
2023
|
+
const product = state.product;
|
|
2024
|
+
const concept = getConceptByTab(tab);
|
|
2025
|
+
|
|
2026
|
+
const servicesByTab = servicesData?.filter(
|
|
2027
|
+
(serv) => serv.service === concept,
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
const statusBySelectedRetailerService = servicesByTab
|
|
2031
|
+
?.find((serv) => serv.id_retailer === activeRetailer?.id_retailer)
|
|
2032
|
+
?.status?.replace(/.*\//, "");
|
|
2033
|
+
|
|
2034
|
+
const currentProducStatus = product?.[`${concept}_status`]?.replace(
|
|
2035
|
+
/.*\//,
|
|
2036
|
+
"",
|
|
2037
|
+
);
|
|
2038
|
+
|
|
2039
|
+
const unvalidated = ["IE", "CA"].includes(currentProducStatus);
|
|
2040
|
+
const auditorUnvalidated = !["RA", "AA", "ACA", "AP"].includes(
|
|
2041
|
+
currentProducStatus,
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
let result;
|
|
2045
|
+
switch (id_rol) {
|
|
2046
|
+
case 7:
|
|
2047
|
+
case 8:
|
|
2048
|
+
// AC se valida/rechaza desde el panel, no se reenvía con "Enviar evaluación".
|
|
2049
|
+
const canSendEvaluation =
|
|
2050
|
+
statusBySelectedRetailerService !== "AC" &&
|
|
2051
|
+
statusArray.includes(statusBySelectedRetailerService);
|
|
2052
|
+
|
|
2053
|
+
// const conditionTwo = statusArray.includes(product?.status);
|
|
2054
|
+
|
|
2055
|
+
// const conditionThree =
|
|
2056
|
+
// servicesByTab.filter((service) =>
|
|
2057
|
+
// statusArray.includes(service.status?.replace(/.*\//, "")),
|
|
2058
|
+
// ).length === servicesByTab.length;
|
|
2059
|
+
|
|
2060
|
+
// return (canSendEvaluation && conditionTwo) || conditionThree;
|
|
2061
|
+
|
|
2062
|
+
return canSendEvaluation;
|
|
2063
|
+
|
|
2064
|
+
case 4:
|
|
2065
|
+
case 5:
|
|
2066
|
+
const filteredSrv45 = servicesByTab?.filter((serv) =>
|
|
2067
|
+
statusArray.includes(serv?.status),
|
|
2068
|
+
);
|
|
2069
|
+
const condition45 =
|
|
2070
|
+
unvalidated &&
|
|
2071
|
+
["CA", "IE"].includes(product?.status) &&
|
|
2072
|
+
filteredSrv45?.length === servicesByTab?.length;
|
|
2073
|
+
return condition45;
|
|
2074
|
+
case 6:
|
|
2075
|
+
const statusInArray = statusArray.includes(product?.status);
|
|
2076
|
+
const allSrvValid = srv?.every((serv) =>
|
|
2077
|
+
["RA", "AA", "AP", "ACA"].includes(serv?.status),
|
|
2078
|
+
);
|
|
2079
|
+
const condition6 = statusInArray && allSrvValid && auditorUnvalidated;
|
|
2080
|
+
return condition6;
|
|
2081
|
+
default:
|
|
2082
|
+
return false;
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
const handleOnChangeProductVersion = (version) => {
|
|
2087
|
+
dispatch({
|
|
2088
|
+
type: "SET_PRODUCT_VERSION",
|
|
2089
|
+
payload: version,
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
loadData(true, version);
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
const handleRetryLoad = () => {
|
|
2096
|
+
dispatch({ type: "SET_ERRORS", payload: [] });
|
|
2097
|
+
if (state.active_retailer) {
|
|
2098
|
+
loadData();
|
|
2099
|
+
} else {
|
|
2100
|
+
initializeProduct();
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
return {
|
|
2105
|
+
isRetailer,
|
|
2106
|
+
digitalAssets,
|
|
2107
|
+
currentTray,
|
|
2108
|
+
assetsLoadedFor,
|
|
2109
|
+
handleOnSetSelectedAssets,
|
|
2110
|
+
handleOnSetAllAssetsChecked,
|
|
2111
|
+
initializeProduct,
|
|
2112
|
+
loadAuditableData,
|
|
2113
|
+
loadData,
|
|
2114
|
+
handleOnChangeCurrentImage,
|
|
2115
|
+
handleOnShowModalGallery,
|
|
2116
|
+
handleOnChangeActiveRetailer,
|
|
2117
|
+
handleOnChangeActiveTab,
|
|
2118
|
+
handleOnToggleImageLayout,
|
|
2119
|
+
handleOnToggleObservation,
|
|
2120
|
+
handleOnHideObservation,
|
|
2121
|
+
isAuditorAssigned,
|
|
2122
|
+
isAssignedAs,
|
|
2123
|
+
isUserAssignedToService,
|
|
2124
|
+
canShowSingleEvaluationButtons,
|
|
2125
|
+
canShowBulkEvaluationButtons,
|
|
2126
|
+
getStatusByCurrentServiceAndRetailer,
|
|
2127
|
+
handleOnSaveAssets,
|
|
2128
|
+
handleOnClickSaveImages,
|
|
2129
|
+
loadFamily,
|
|
2130
|
+
hijosDelProducto,
|
|
2131
|
+
esHijoDeOtro,
|
|
2132
|
+
puedeAgruparse,
|
|
2133
|
+
handleOnUnlinked,
|
|
2134
|
+
loadVariantGroup,
|
|
2135
|
+
handleOnLeaveGroup,
|
|
2136
|
+
openFamilyMember,
|
|
2137
|
+
handleOnLinkedVariants,
|
|
2138
|
+
handleOnRestoreInheritance,
|
|
2139
|
+
cambiosDeFichaTecnica,
|
|
2140
|
+
cambiosDeDescripciones,
|
|
2141
|
+
handleOnClickSave,
|
|
2142
|
+
handleOnSetUpdatedDescriptions,
|
|
2143
|
+
handleOnSetUpdatedDatasheets,
|
|
2144
|
+
handleOnDownloadAssets,
|
|
2145
|
+
handleOnAskToDeleteAssets,
|
|
2146
|
+
handleOnAskToDeleteImages,
|
|
2147
|
+
handleOnApproveSingleService,
|
|
2148
|
+
handleOnRejectSingleService,
|
|
2149
|
+
sendSingleEvaluation,
|
|
2150
|
+
handleOnApproveAllServices,
|
|
2151
|
+
handleOnRejectAllServices,
|
|
2152
|
+
sendBulkEvaluation,
|
|
2153
|
+
handleOnChangeAssignations,
|
|
2154
|
+
handleOnSetAssignation,
|
|
2155
|
+
handleOnSetImages,
|
|
2156
|
+
handleOnSetCheckAll,
|
|
2157
|
+
handleOnSetSelectedImages,
|
|
2158
|
+
handleOnSubmitComment,
|
|
2159
|
+
handleOnSendEvaluationToFacilitator,
|
|
2160
|
+
isEvaluationFinished,
|
|
2161
|
+
handleOnChangeProductVersion,
|
|
2162
|
+
handleRetryLoad,
|
|
2163
|
+
showVersionSelector,
|
|
2164
|
+
setShowVersionSelector,
|
|
2165
|
+
auditableVersion,
|
|
2166
|
+
setAuditableVersion,
|
|
2167
|
+
headerTop,
|
|
2168
|
+
setHeaderTop,
|
|
2169
|
+
shotThd,
|
|
2170
|
+
setShotThd,
|
|
2171
|
+
observation,
|
|
2172
|
+
setObservation,
|
|
2173
|
+
showRejectModal,
|
|
2174
|
+
setShowRejectModal,
|
|
2175
|
+
isObservationVisible,
|
|
2176
|
+
setObservationVisible,
|
|
2177
|
+
imageLayout,
|
|
2178
|
+
setImageLayout,
|
|
2179
|
+
crossComment,
|
|
2180
|
+
setCrossComment,
|
|
2181
|
+
statusArray,
|
|
2182
|
+
setStatusArray,
|
|
2183
|
+
rejectAll,
|
|
2184
|
+
setRejectAll,
|
|
2185
|
+
showLinkVariants,
|
|
2186
|
+
setShowLinkVariants,
|
|
2187
|
+
family,
|
|
2188
|
+
setFamily,
|
|
2189
|
+
showFamily,
|
|
2190
|
+
setShowFamily,
|
|
2191
|
+
loadingVariant,
|
|
2192
|
+
setLoadingVariant,
|
|
2193
|
+
variantGroup,
|
|
2194
|
+
setVariantGroup,
|
|
2195
|
+
showGroup,
|
|
2196
|
+
setShowGroup,
|
|
2197
|
+
showGroupModal,
|
|
2198
|
+
setShowGroupModal,
|
|
2199
|
+
porDesvincular,
|
|
2200
|
+
setPorDesvincular,
|
|
2201
|
+
avisoFamilia,
|
|
2202
|
+
setAvisoFamilia,
|
|
2203
|
+
pendingPropagation,
|
|
2204
|
+
setPendingPropagation,
|
|
2205
|
+
preparandoPropagacion,
|
|
2206
|
+
setPreparandoPropagacion,
|
|
2207
|
+
auditServices,
|
|
2208
|
+
setAuditServices,
|
|
2209
|
+
auditDatasheets,
|
|
2210
|
+
setAuditDatasheets,
|
|
2211
|
+
auditDescriptions,
|
|
2212
|
+
setAuditDescriptions,
|
|
2213
|
+
showModal,
|
|
2214
|
+
setShowModal,
|
|
2215
|
+
compare,
|
|
2216
|
+
setCompare,
|
|
2217
|
+
openChangeStatusModal,
|
|
2218
|
+
setOpenChangeStatusModal,
|
|
2219
|
+
assetNotices,
|
|
2220
|
+
setAssetNotices,
|
|
2221
|
+
activeTray,
|
|
2222
|
+
setActiveTray,
|
|
2223
|
+
state,
|
|
2224
|
+
dispatch,
|
|
2225
|
+
saveDatasheets,
|
|
2226
|
+
saveDescriptions,
|
|
2227
|
+
updateImages,
|
|
2228
|
+
deleteImages,
|
|
2229
|
+
saveAssets,
|
|
2230
|
+
registerUploadedAssets,
|
|
2231
|
+
handleOnDownloadImages,
|
|
2232
|
+
createComment,
|
|
2233
|
+
getRootProps,
|
|
2234
|
+
getInputProps,
|
|
2235
|
+
isDragActive,
|
|
2236
|
+
open,
|
|
2237
|
+
};
|
|
2238
|
+
};
|