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.
Files changed (49) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
  3. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
  4. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
  5. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
  6. package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
  7. package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
  8. package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
  9. package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
  10. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
  11. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
  12. package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
  13. package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
  14. package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +2 -1
  15. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +325 -10
  16. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -2
  17. package/dist/components/pages/ProviderProductEdition/index.js +486 -231
  18. package/dist/index.js +13 -0
  19. package/package.json +1 -1
  20. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
  21. package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
  22. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
  23. package/src/components/pages/CreatorsProductEdition/index.js +19 -0
  24. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
  25. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
  26. package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
  27. package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
  28. package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
  29. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
  30. package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
  31. package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
  32. package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
  33. package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
  34. package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
  35. package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
  36. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
  37. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
  38. package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
  39. package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
  40. package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
  41. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
  42. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
  43. package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
  44. package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
  45. package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +1 -0
  46. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +201 -0
  47. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -0
  48. package/src/components/pages/ProviderProductEdition/index.js +228 -7
  49. package/src/index.js +1 -0
@@ -0,0 +1,382 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ Avatar,
4
+ AvatarGroup,
5
+ Box,
6
+ Button,
7
+ IconButton,
8
+ List,
9
+ ListItemAvatar,
10
+ ListItemButton,
11
+ ListItemText,
12
+ MobileStepper,
13
+ Popover,
14
+ Stack,
15
+ Tooltip,
16
+ Typography,
17
+ } from "@mui/material";
18
+
19
+ import { getProfilePicture } from "../../../../global-files/data";
20
+ import { useAiProductEdition } from "../../../../contexts/AiProductEdition";
21
+
22
+ import defaultProfile from "../../../../assets/images/defaultImages/defaultProfileImage.svg";
23
+ import InfoIcon from "../../../../assets/images/sliderToolTip/infoIcon.svg";
24
+ import Slide1 from "../../../../assets/images/sliderToolTip/slide21.svg";
25
+ import Slide2 from "../../../../assets/images/sliderToolTip/slide22.svg";
26
+ import Slide3 from "../../../../assets/images/sliderToolTip/slide23.svg";
27
+ import saveIcon from "../../../../assets/images/generalButton/saveIcon.svg";
28
+ import saveIconHover from "../../../../assets/images/generalButton/saveIconHover.svg";
29
+ import deleteIcon from "../../../../assets/images/generalButton/deleteIcon.svg";
30
+ import deleteIconHover from "../../../../assets/images/generalButton/deleteIconHover.svg";
31
+ import downloadIcon from "../../../../assets/images/generalButton/downloadIcon.svg";
32
+ import rowsLayoutIcon from "../../../../assets/images/generalButton/rowLayout.svg";
33
+ import gridLayoutIcon from "../../../../assets/images/generalButton/gridLayout.svg";
34
+
35
+ import { StatusChip } from "./StatusChip";
36
+ import { COLORS, FONTS } from "./tokens";
37
+
38
+ /*
39
+ Acciones de un servicio: ayuda, versión, estatus, guardar, (en imágenes)
40
+ borrar/descargar/cambiar vista, y asignaciones con su panel.
41
+
42
+ Es lo que hacía `molecules/StatusAsignationInfo`, que la vista original usa en
43
+ DOS lugares: junto a las pestañas y en la cabecera de los datos de la imagen.
44
+ Aquí también se comparte: TabsBar e ImagePanel usan este mismo componente.
45
+ */
46
+
47
+ const SLIDES = [
48
+ {
49
+ slide: Slide1,
50
+ title: "Valida tu producto en cada activo digital",
51
+ description:
52
+ "Al solicitar a Content-oh! podrás validar la información, imágenes y documentación de tu producto, esto hará más sencillo que puedas aprobar o rechazar el contenido.",
53
+ },
54
+ {
55
+ slide: Slide2,
56
+ title: "Aprueba o rechaza con un clic",
57
+ description:
58
+ "Cuando termines de revisar tu información, elige si aproebas o rechazas, esto enviara una notificiación a nuestro equipo, para continuar o corregir el producto.",
59
+ },
60
+ {
61
+ slide: Slide3,
62
+ title: "Estatus de tu producto en el proceso",
63
+ description:
64
+ "La mayor parte del proceso podrás identificar que estatus tiene tu producto, esto dara visibilidad a todo tu equipo para saber la etapa se encuentra.",
65
+ },
66
+ ];
67
+
68
+ const HelpSlides = () => {
69
+ const [anchor, setAnchor] = useState(null);
70
+ const [step, setStep] = useState(0);
71
+ const last = step === SLIDES.length - 1;
72
+ const close = () => {
73
+ setAnchor(null);
74
+ setStep(0);
75
+ };
76
+
77
+ return (
78
+ <>
79
+ <IconButton
80
+ aria-label="Ayuda"
81
+ size="small"
82
+ onClick={(e) => setAnchor(e.currentTarget)}
83
+ sx={{ p: 0 }}
84
+ >
85
+ <img src={InfoIcon} alt="" width={30} height={30} style={{ filter: "brightness(.35)" }} />
86
+ </IconButton>
87
+ <Popover
88
+ open={Boolean(anchor)}
89
+ anchorEl={anchor}
90
+ onClose={close}
91
+ anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
92
+ transformOrigin={{ vertical: "top", horizontal: "center" }}
93
+ PaperProps={{ sx: { width: 300, maxWidth: "calc(100vw - 24px)", p: 2, mt: 1 } }}
94
+ >
95
+ <Box
96
+ component="img"
97
+ src={SLIDES[step].slide}
98
+ alt=""
99
+ sx={{ width: "100%", height: 150, objectFit: "contain" }}
100
+ />
101
+ <Typography sx={{ fontFamily: FONTS.Raleway_700, fontSize: 14, color: COLORS.s5, mt: 1 }}>
102
+ {SLIDES[step].title}
103
+ </Typography>
104
+ <Typography sx={{ fontFamily: FONTS.AvenirNext, fontSize: 12, color: COLORS.s4, mt: 0.5 }}>
105
+ {SLIDES[step].description}
106
+ </Typography>
107
+ <MobileStepper
108
+ variant="dots"
109
+ steps={SLIDES.length}
110
+ position="static"
111
+ activeStep={step}
112
+ sx={{
113
+ bgcolor: "transparent",
114
+ px: 0,
115
+ mt: 1,
116
+ "& .MuiMobileStepper-dotActive": { bgcolor: COLORS.magenta_s2 },
117
+ }}
118
+ backButton={
119
+ <Button
120
+ size="small"
121
+ disabled={step === 0}
122
+ onClick={() => setStep((s) => s - 1)}
123
+ sx={{ color: COLORS.s5 }}
124
+ >
125
+ Anterior
126
+ </Button>
127
+ }
128
+ nextButton={
129
+ <Button
130
+ size="small"
131
+ onClick={() => (last ? close() : setStep((s) => s + 1))}
132
+ sx={{ color: COLORS.magenta_s2 }}
133
+ >
134
+ {last ? "Cerrar" : "Siguiente"}
135
+ </Button>
136
+ }
137
+ />
138
+ </Popover>
139
+ </>
140
+ );
141
+ };
142
+
143
+ /** Botón redondo de 25px con el ícono de siempre y su versión "hover". */
144
+ const RoundIcon = ({
145
+ label,
146
+ icon,
147
+ hoverIcon,
148
+ onClick,
149
+ bordered = COLORS.s3,
150
+ hoverBg = COLORS.magenta_s2,
151
+ size = 15,
152
+ }) => (
153
+ <Tooltip title={label} arrow>
154
+ <IconButton
155
+ aria-label={label}
156
+ onClick={onClick}
157
+ sx={{
158
+ width: 25,
159
+ height: 25,
160
+ p: 0,
161
+ border: bordered ? `1px solid ${bordered}` : "none",
162
+ backgroundImage: `url(${icon})`,
163
+ backgroundRepeat: "no-repeat",
164
+ backgroundPosition: "center",
165
+ backgroundSize: size,
166
+ "&:hover": {
167
+ backgroundColor: hoverBg,
168
+ borderColor: hoverBg,
169
+ ...(hoverIcon ? { backgroundImage: `url(${hoverIcon})` } : {}),
170
+ },
171
+ }}
172
+ />
173
+ </Tooltip>
174
+ );
175
+
176
+ export const ServiceActions = ({
177
+ status = "",
178
+ imagesSection = false,
179
+ imageLayout,
180
+ setImageLayout,
181
+ assig,
182
+ setAssignation,
183
+ isRetailer,
184
+ downloadImages,
185
+ askToDeleteImages,
186
+ onClickSave,
187
+ showSaveButton,
188
+ canAssign,
189
+ version,
190
+ setShowVersionSelector,
191
+ // Cambia cuando el panel de asignación debe cerrarse (p. ej., otra pestaña).
192
+ resetKey,
193
+ }) => {
194
+ const isImagesTab = imagesSection;
195
+ const { inputsUsingAi } = useAiProductEdition();
196
+
197
+ const [assignAnchor, setAssignAnchor] = useState(null);
198
+ const [assignationType, setAssignationType] = useState("facilitator");
199
+ const collaborators = assig?.collaborators?.[assignationType];
200
+
201
+ useEffect(() => setAssignAnchor(null), [resetKey]);
202
+
203
+ const save = () => {
204
+ if (Object.values(inputsUsingAi ?? {}).some((input) => !!input?.using)) {
205
+ console.warn("Para guardar se debe de dejar de usar la generación con IA");
206
+ return;
207
+ }
208
+ onClickSave && onClickSave();
209
+ };
210
+
211
+ return (
212
+ <>
213
+ <Stack
214
+ direction="row"
215
+ alignItems="center"
216
+ flexWrap="wrap"
217
+ gap="10px"
218
+ justifyContent="flex-end"
219
+ sx={{ flex: "0 1 auto", ml: "auto" }}
220
+ >
221
+ <HelpSlides />
222
+ {version && (
223
+ <Tooltip title="Cambiar de versión" arrow>
224
+ <Button
225
+ onClick={(e) => {
226
+ e.stopPropagation();
227
+ setShowVersionSelector(true);
228
+ }}
229
+ sx={{
230
+ minWidth: 25,
231
+ width: 25,
232
+ height: 25,
233
+ p: 0,
234
+ borderRadius: "50%",
235
+ border: `1px solid ${COLORS.color_purple}`,
236
+ color: COLORS.color_purple,
237
+ fontFamily: FONTS.AvenirNext,
238
+ fontSize: 11,
239
+ lineHeight: "25px",
240
+ }}
241
+ >
242
+ V{version}
243
+ </Button>
244
+ </Tooltip>
245
+ )}
246
+ {status !== "-" && status !== "" && <StatusChip status={status} oval />}
247
+ {showSaveButton && (
248
+ <RoundIcon
249
+ label="Guardar"
250
+ icon={saveIcon}
251
+ hoverIcon={saveIconHover}
252
+ bordered={COLORS.color_gray}
253
+ hoverBg={COLORS.color_pink}
254
+ size={undefined}
255
+ onClick={save}
256
+ />
257
+ )}
258
+ {isImagesTab && (
259
+ <>
260
+ {showSaveButton && (
261
+ <RoundIcon
262
+ label="Borrar seleccionadas"
263
+ icon={deleteIcon}
264
+ hoverIcon={deleteIconHover}
265
+ onClick={() => askToDeleteImages && askToDeleteImages()}
266
+ />
267
+ )}
268
+ <RoundIcon
269
+ label="Descargar"
270
+ icon={downloadIcon}
271
+ onClick={() => downloadImages && downloadImages()}
272
+ />
273
+ <Tooltip title={imageLayout ? "Ver como lista" : "Ver como cuadrícula"} arrow>
274
+ <IconButton
275
+ aria-label={imageLayout ? "Ver como lista" : "Ver como cuadrícula"}
276
+ onClick={() => setImageLayout && setImageLayout(!imageLayout)}
277
+ sx={{ width: 24, height: 24, p: 0 }}
278
+ >
279
+ <img
280
+ src={imageLayout ? rowsLayoutIcon : gridLayoutIcon}
281
+ alt=""
282
+ width={24}
283
+ height={24}
284
+ />
285
+ </IconButton>
286
+ </Tooltip>
287
+ </>
288
+ )}
289
+ <Stack direction="row" alignItems="center" gap="6px">
290
+ <AvatarGroup
291
+ max={4}
292
+ sx={{ "& .MuiAvatar-root": { width: 20, height: 20, fontSize: 10, border: 0 } }}
293
+ >
294
+ {assig?.assignations?.map((assignation, index) => (
295
+ <Avatar
296
+ key={`${index}-${assignation.id}`}
297
+ src={getProfilePicture(assignation.id, 20, 20)}
298
+ alt="Persona asignada"
299
+ onClick={(e) => {
300
+ e.stopPropagation();
301
+ setAssignationType(assignation?.collaboratorType);
302
+ setAssignAnchor(e.currentTarget);
303
+ }}
304
+ sx={{ cursor: "pointer" }}
305
+ >
306
+ <img src={defaultProfile} alt="" width={20} height={20} />
307
+ </Avatar>
308
+ ))}
309
+ </AvatarGroup>
310
+ <Typography sx={{ fontFamily: FONTS.AvenirNext, fontSize: 14, color: COLORS.color_gray }}>
311
+ Asig.
312
+ </Typography>
313
+ </Stack>
314
+ </Stack>
315
+
316
+ <Popover
317
+ open={Boolean(assignAnchor) && canAssign}
318
+ anchorEl={assignAnchor}
319
+ onClose={() => setAssignAnchor(null)}
320
+ anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
321
+ transformOrigin={{ vertical: "top", horizontal: "right" }}
322
+ PaperProps={{ sx: { width: 260, maxWidth: "calc(100vw - 24px)", mt: 1, p: 1.5 } }}
323
+ >
324
+ <Stack direction="row" alignItems="center" gap={1} sx={{ mb: 1 }}>
325
+ <Typography sx={{ fontFamily: FONTS.AvenirNext, fontSize: 13, color: COLORS.s4 }}>
326
+ Estatus
327
+ </Typography>
328
+ <StatusChip status={status} oval />
329
+ </Stack>
330
+ {collaborators?.length ? (
331
+ <List dense disablePadding sx={{ maxHeight: 280, overflowY: "auto" }}>
332
+ {collaborators.map((person, index) => (
333
+ <ListItemButton
334
+ key={`${index}-${person.name}`}
335
+ onClick={() => {
336
+ setAssignation && setAssignation(assignationType, person?.id_user);
337
+ setAssignAnchor(null);
338
+ }}
339
+ sx={{ borderRadius: "3px" }}
340
+ >
341
+ <ListItemAvatar sx={{ minWidth: 40 }}>
342
+ <Avatar
343
+ src={getProfilePicture(person?.id_user, 40, 40)}
344
+ sx={{ width: 26, height: 26 }}
345
+ >
346
+ <img src={defaultProfile} alt="" width={26} height={26} />
347
+ </Avatar>
348
+ </ListItemAvatar>
349
+ <ListItemText
350
+ primary={person.name}
351
+ primaryTypographyProps={{
352
+ sx: { fontFamily: FONTS.AvenirNext, fontSize: 14, color: COLORS.s5 },
353
+ }}
354
+ />
355
+ </ListItemButton>
356
+ ))}
357
+ </List>
358
+ ) : (
359
+ <Typography sx={{ fontFamily: FONTS.AvenirNext, fontSize: 13, color: COLORS.s4, py: 1 }}>
360
+ Sin personas para asignar
361
+ </Typography>
362
+ )}
363
+ {isRetailer === 1 && (
364
+ <List dense disablePadding sx={{ mt: 1, borderTop: `1px solid ${COLORS.s2}`, pt: 1 }}>
365
+ <ListItemText
366
+ primary="Solicitar a"
367
+ secondary="Proveedor"
368
+ sx={{ px: 2 }}
369
+ primaryTypographyProps={{ sx: { fontSize: 14, color: COLORS.s5 } }}
370
+ />
371
+ <ListItemText
372
+ primary="Solicitar a"
373
+ secondary="Content-oh!"
374
+ sx={{ px: 2 }}
375
+ primaryTypographyProps={{ sx: { fontSize: 14, color: COLORS.s5 } }}
376
+ />
377
+ </List>
378
+ )}
379
+ </Popover>
380
+ </>
381
+ );
382
+ };
@@ -0,0 +1,37 @@
1
+ import { Chip, Tooltip } from "@mui/material";
2
+
3
+ import { COLORS, FONTS, statusColor, statusName } from "./tokens";
4
+
5
+ /**
6
+ * Etiqueta de estatus ("RA/AS", "AA"...). Mismo color y tamaño que
7
+ * `atoms/Status`; el nombre completo va en el tooltip. `oval` es la variante
8
+ * redondeada de la barra de pestañas.
9
+ */
10
+ export const StatusChip = ({ status, oval = false }) => {
11
+ const label = status ?? "NS";
12
+ const chip = (
13
+ <Chip
14
+ label={label}
15
+ size="small"
16
+ sx={{
17
+ height: 20,
18
+ borderRadius: oval ? "10px" : "3px",
19
+ backgroundColor: statusColor(status),
20
+ color: COLORS.white,
21
+ fontFamily: FONTS.Lato,
22
+ fontSize: 12,
23
+ cursor: "default",
24
+ "& .MuiChip-label": { px: "10px", lineHeight: "20px" },
25
+ }}
26
+ />
27
+ );
28
+
29
+ const name = statusName(status);
30
+ return name ? (
31
+ <Tooltip title={name} arrow placement="top">
32
+ {chip}
33
+ </Tooltip>
34
+ ) : (
35
+ chip
36
+ );
37
+ };
@@ -0,0 +1,187 @@
1
+ import { useState } from "react";
2
+ import {
3
+ Box,
4
+ Button,
5
+ Dialog,
6
+ DialogActions,
7
+ DialogContent,
8
+ DialogContentText,
9
+ DialogTitle,
10
+ Stack,
11
+ Tab,
12
+ Tabs,
13
+ useMediaQuery,
14
+ } from "@mui/material";
15
+
16
+ import { ServiceActions } from "./ServiceActions";
17
+ import { COLORS, FONTS } from "./tokens";
18
+
19
+ /*
20
+ Barra de pestañas (Descripción / Ficha técnica / Imágenes) y sus acciones:
21
+ ayuda, versión, estatus, guardar, borrar/descargar/cambiar vista de imágenes y
22
+ asignaciones.
23
+
24
+ Reemplaza a `organisms/FullTabsMenu` (TabsMenu + StatusAsignationInfo) con
25
+ los mismos props y el mismo comportamiento, hecho con MUI:
26
+ - las pestañas se desplazan de lado si no caben;
27
+ - si no caben junto a las pestañas, las acciones bajan a su propia fila
28
+ (las acciones son `ServiceActions`, compartido con el panel de imagen);
29
+ - cambiar de pestaña con cambios sin guardar pide confirmación.
30
+ */
31
+
32
+ const tabSx = {
33
+ fontFamily: FONTS.Raleway_500,
34
+ textTransform: "none",
35
+ fontSize: { xs: 13, sm: 14 },
36
+ lineHeight: "21px",
37
+ whiteSpace: "nowrap",
38
+ color: COLORS.s5,
39
+ minWidth: { xs: 0, sm: 149 },
40
+ minHeight: 36,
41
+ px: { xs: 0.5, sm: 0 },
42
+ pb: "5px",
43
+ alignItems: "flex-start",
44
+ borderBottom: `2px solid ${COLORS.s3}`,
45
+ transition: "font-size 0.1s",
46
+ "&.Mui-selected": { color: COLORS.s5, fontSize: { xs: 14, sm: 18 } },
47
+ };
48
+
49
+ export const TabsBar = ({
50
+ tabsSections = {},
51
+ status = "",
52
+ activeTab,
53
+ setActiveTab,
54
+ imageLayout,
55
+ setImageLayout,
56
+ assig,
57
+ setAssignation,
58
+ isRetailer,
59
+ downloadImages,
60
+ askToDeleteImages,
61
+ onClickSave,
62
+ showSaveButton,
63
+ canAssign,
64
+ version,
65
+ setShowVersionSelector,
66
+ images,
67
+ updatedDescriptions = [],
68
+ updatedDatasheets = [],
69
+ updatedImages = [],
70
+ setUpdatedDescriptions,
71
+ setUpdatedDatasheets,
72
+ setImages,
73
+ }) => {
74
+ const tabs = Object.keys(tabsSections);
75
+ const initialTab = tabs.find((key) => tabsSections[key]) ?? tabs[0];
76
+ const currentTab = tabs.includes(activeTab) ? activeTab : initialTab;
77
+ const isImagesTab = currentTab === "Imágenes";
78
+
79
+ // En celular las pestañas se reparten a lo ancho: con tres caben todas y no
80
+ // queda ninguna escondida detrás de la flecha de desplazamiento.
81
+ const isPhone = useMediaQuery("(max-width:599.95px)");
82
+
83
+ // --- Cambio de pestaña con cambios sin guardar ---------------------------
84
+ const [pendingTab, setPendingTab] = useState(null);
85
+
86
+ const hasUnsavedChanges = () =>
87
+ updatedDescriptions.length > 0 ||
88
+ updatedDatasheets.length > 0 ||
89
+ updatedImages.length > 0 ||
90
+ images?.values?.filter((f) => !f.id)?.length > 0;
91
+
92
+ const requestTab = (tab) => {
93
+ if (tab === currentTab) return;
94
+ if (hasUnsavedChanges()) setPendingTab(tab);
95
+ else setActiveTab(tab);
96
+ };
97
+
98
+ const confirmTabChange = () => {
99
+ // Igual que TabsMenu: al aceptar se descartan los cambios pendientes.
100
+ setActiveTab(pendingTab);
101
+ setUpdatedDescriptions && setUpdatedDescriptions([]);
102
+ setUpdatedDatasheets && setUpdatedDatasheets([]);
103
+ setImages && setImages({});
104
+ setPendingTab(null);
105
+ };
106
+
107
+ return (
108
+ <Box sx={{ mb: "14px", minWidth: 0 }}>
109
+ {/* Pestañas y acciones comparten fila mientras quepan; si no, las acciones
110
+ bajan a la siguiente. Las pestañas solo se desplazan de lado cuando ni
111
+ solas caben (celular). */}
112
+ <Stack
113
+ direction="row"
114
+ flexWrap="wrap"
115
+ alignItems="flex-end"
116
+ justifyContent="space-between"
117
+ columnGap={2}
118
+ rowGap={1}
119
+ >
120
+ <Tabs
121
+ value={currentTab ?? false}
122
+ onChange={(_, tab) => requestTab(tab)}
123
+ variant={isPhone && tabs.length <= 3 ? "fullWidth" : "scrollable"}
124
+ scrollButtons="auto"
125
+ allowScrollButtonsMobile
126
+ TabIndicatorProps={{ sx: { bgcolor: COLORS.magenta_s2, height: 2 } }}
127
+ sx={{
128
+ flex: "1 1 auto",
129
+ maxWidth: "100%",
130
+ minHeight: 36,
131
+ minWidth: 0,
132
+ "& .MuiTabs-flexContainer": { gap: "10px" },
133
+ }}
134
+ >
135
+ {tabs.map((tab) => (
136
+ <Tab key={tab} value={tab} label={tab} disableRipple sx={tabSx} />
137
+ ))}
138
+ </Tabs>
139
+
140
+ <ServiceActions
141
+ status={status}
142
+ imagesSection={isImagesTab}
143
+ imageLayout={imageLayout}
144
+ setImageLayout={setImageLayout}
145
+ assig={assig}
146
+ setAssignation={setAssignation}
147
+ isRetailer={isRetailer}
148
+ downloadImages={downloadImages}
149
+ askToDeleteImages={askToDeleteImages}
150
+ onClickSave={onClickSave}
151
+ showSaveButton={showSaveButton}
152
+ canAssign={canAssign}
153
+ version={version}
154
+ setShowVersionSelector={setShowVersionSelector}
155
+ resetKey={currentTab}
156
+ />
157
+ </Stack>
158
+
159
+ <Dialog open={Boolean(pendingTab)} onClose={() => setPendingTab(null)} maxWidth="xs" fullWidth>
160
+ <DialogTitle sx={{ fontFamily: FONTS.Raleway_600, color: COLORS.s5 }}>Cambios sin Guardar</DialogTitle>
161
+ <DialogContent>
162
+ <DialogContentText sx={{ fontFamily: FONTS.AvenirNext }}>
163
+ Estás cambiando de vista y aun no guardas cambios ¿Estás seguro?
164
+ </DialogContentText>
165
+ </DialogContent>
166
+ <DialogActions sx={{ px: 3, pb: 2 }}>
167
+ <Button onClick={() => setPendingTab(null)} sx={{ color: COLORS.s5, textTransform: "none" }}>
168
+ Cancelar
169
+ </Button>
170
+ <Button
171
+ variant="contained"
172
+ disableElevation
173
+ onClick={confirmTabChange}
174
+ sx={{
175
+ textTransform: "none",
176
+ borderRadius: "20px",
177
+ bgcolor: COLORS.magenta_s2,
178
+ "&:hover": { bgcolor: COLORS.secondary_magenta },
179
+ }}
180
+ >
181
+ Aceptar
182
+ </Button>
183
+ </DialogActions>
184
+ </Dialog>
185
+ </Box>
186
+ );
187
+ };