contentoh-components-library 21.6.58 → 21.6.60

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 (42) hide show
  1. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
  2. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
  3. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
  4. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
  5. package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
  6. package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
  7. package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
  8. package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
  9. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
  10. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
  11. package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
  12. package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
  13. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +2 -2
  14. package/dist/index.js +13 -0
  15. package/package.json +1 -1
  16. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
  17. package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
  18. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
  19. package/src/components/pages/CreatorsProductEdition/index.js +19 -0
  20. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
  21. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
  22. package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
  23. package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
  24. package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
  25. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
  26. package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
  27. package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
  28. package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
  29. package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
  30. package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
  31. package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
  32. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
  33. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
  34. package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
  35. package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
  36. package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
  37. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
  38. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
  39. package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
  40. package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
  41. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +6 -2
  42. package/src/index.js +1 -0
@@ -0,0 +1,269 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ Box,
4
+ ButtonBase,
5
+ Dialog,
6
+ IconButton,
7
+ ListItemIcon,
8
+ ListItemText,
9
+ Menu,
10
+ MenuItem,
11
+ Stack,
12
+ Tooltip,
13
+ Typography,
14
+ useMediaQuery,
15
+ } from "@mui/material";
16
+ import CloseIcon from "@mui/icons-material/Close";
17
+ import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
18
+ import ChevronRightIcon from "@mui/icons-material/ChevronRight";
19
+ import { Swiper, SwiperSlide } from "swiper/react";
20
+ import { FreeMode, Mousewheel } from "swiper";
21
+ import "swiper/swiper.min.css";
22
+
23
+ import acceptIcon from "../../../../assets/images/generalButton/acceptIcon.svg";
24
+ import rejectIcon from "../../../../assets/images/generalButton/rejectIcon.svg";
25
+ import nullIcon from "../../../../assets/images/generalButton/nullIcon.svg";
26
+
27
+ import { ProductMedia } from "./ProductMedia";
28
+ import { COLORS, FONTS } from "./tokens";
29
+
30
+ /*
31
+ Galería de imágenes en grande.
32
+
33
+ Reemplaza a `organisms/ProductImageModal` con los mismos props y hecho con el
34
+ Dialog de MUI. El original ponía la imagen y un panel de 375px mínimo lado a
35
+ lado, así que en celular no cabía. Aquí:
36
+ - en desktop, imagen a la izquierda y datos + miniaturas a la derecha;
37
+ - en celular, pantalla completa: imagen arriba y datos debajo.
38
+ Se navega con las flechas del teclado o con los botones sobre la imagen.
39
+
40
+ La imagen grande es la original de S3 (`srcDB`), como en el original; las
41
+ miniaturas usan la versión reducida (`src`). Una imagen recién agregada aún
42
+ no tiene `srcDB`, así que se usa su `src` local.
43
+ */
44
+
45
+ const bucketUrl = (image) =>
46
+ image?.srcDB
47
+ ? `https://${process.env.REACT_APP_IMAGES_BUCKET}.s3.amazonaws.com/${image.srcDB}`
48
+ : image?.src;
49
+
50
+ const VALIDATION_ICONS = { approve: acceptIcon, reject: rejectIcon };
51
+
52
+ const DataRow = ({ label, value }) => (
53
+ <Stack direction="row" justifyContent="space-between" gap={2} sx={{ py: "3px" }}>
54
+ <Typography sx={{ fontFamily: FONTS.AvenirNext, fontSize: 12, color: COLORS.s3 }}>{label}</Typography>
55
+ <Typography
56
+ sx={{ fontFamily: FONTS.AvenirNext, fontSize: 12, color: COLORS.white, textAlign: "right", overflowWrap: "anywhere" }}
57
+ >
58
+ {value || "-"}
59
+ </Typography>
60
+ </Stack>
61
+ );
62
+
63
+ const arrowSx = {
64
+ position: "absolute",
65
+ top: "50%",
66
+ transform: "translateY(-50%)",
67
+ bgcolor: "rgba(40, 31, 51, 0.55)",
68
+ color: COLORS.white,
69
+ "&:hover": { bgcolor: "rgba(40, 31, 51, 0.8)" },
70
+ };
71
+
72
+ export const ImageGallery = ({
73
+ open = true,
74
+ images = {},
75
+ initialIndex = 0,
76
+ setShowModal,
77
+ sendToFacilitator,
78
+ approveRejectButtons = false,
79
+ }) => {
80
+ const values = images?.values ?? [];
81
+ const [index, setIndex] = useState(initialIndex);
82
+ const [validation, setValidation] = useState(null);
83
+ const [menuAnchor, setMenuAnchor] = useState(null);
84
+ const isPhone = useMediaQuery("(max-width:599.95px)");
85
+
86
+ useEffect(() => {
87
+ if (open) setIndex(Math.min(initialIndex, Math.max(values.length - 1, 0)));
88
+ }, [open, initialIndex]); // eslint-disable-line react-hooks/exhaustive-deps
89
+
90
+ const image = values[index];
91
+ const hasMany = values.length > 1;
92
+ const go = (step) => setIndex((i) => (i + step + values.length) % values.length);
93
+ const close = () => setShowModal(false);
94
+
95
+ const onKeyDown = (e) => {
96
+ if (!hasMany) return;
97
+ if (e.key === "ArrowRight") go(1);
98
+ if (e.key === "ArrowLeft") go(-1);
99
+ };
100
+
101
+ const validate = (kind) => {
102
+ setMenuAnchor(null);
103
+ setValidation(kind);
104
+ sendToFacilitator && sendToFacilitator(kind === "approve" ? "A" : "R");
105
+ };
106
+
107
+ const shotName = images?.inputs?.find((f) => f.id === +image?.image_id)?.name;
108
+ const size = image?.width && image?.height ? `${image.width}x${image.height}px` : null;
109
+
110
+ return (
111
+ <Dialog
112
+ open={open}
113
+ onClose={close}
114
+ onKeyDown={onKeyDown}
115
+ fullScreen={isPhone}
116
+ maxWidth={false}
117
+ PaperProps={{
118
+ sx: {
119
+ m: { sm: 2 },
120
+ width: { sm: "min(1200px, calc(100% - 32px))" },
121
+ height: { sm: "min(760px, calc(100% - 32px))" },
122
+ maxHeight: "none",
123
+ borderRadius: { xs: 0, sm: "4px" },
124
+ overflow: "hidden",
125
+ display: "flex",
126
+ flexDirection: { xs: "column", sm: "row" },
127
+ bgcolor: COLORS.deep_gray,
128
+ },
129
+ }}
130
+ >
131
+ {/* Imagen */}
132
+ <Box
133
+ sx={{
134
+ position: "relative",
135
+ flex: { xs: "0 0 auto", sm: "1 1 auto" },
136
+ height: { xs: "min(60vh, 100vw)", sm: "auto" },
137
+ minWidth: 0,
138
+ bgcolor: COLORS.white,
139
+ }}
140
+ >
141
+ {image ? (
142
+ <ProductMedia key={index} src={bucketUrl(image)} ext={image.ext} alt={image.name} />
143
+ ) : (
144
+ <Stack alignItems="center" justifyContent="center" sx={{ height: "100%" }}>
145
+ <Typography sx={{ color: COLORS.s4 }}>Sin imágenes</Typography>
146
+ </Stack>
147
+ )}
148
+ {hasMany && (
149
+ <>
150
+ <IconButton aria-label="Imagen anterior" onClick={() => go(-1)} sx={{ ...arrowSx, left: 8 }}>
151
+ <ChevronLeftIcon />
152
+ </IconButton>
153
+ <IconButton aria-label="Imagen siguiente" onClick={() => go(1)} sx={{ ...arrowSx, right: 8 }}>
154
+ <ChevronRightIcon />
155
+ </IconButton>
156
+ </>
157
+ )}
158
+ </Box>
159
+
160
+ {/* Datos y miniaturas */}
161
+ <Stack
162
+ sx={{
163
+ flex: { xs: "1 1 auto", sm: "0 0 375px" },
164
+ minHeight: 0,
165
+ p: "15px",
166
+ gap: 2,
167
+ overflowY: "auto",
168
+ color: COLORS.white,
169
+ }}
170
+ >
171
+ <Stack direction="row" alignItems="center" gap={1}>
172
+ <Typography
173
+ title={image?.name}
174
+ sx={{
175
+ flex: 1,
176
+ minWidth: 0,
177
+ fontFamily: FONTS.Raleway,
178
+ fontSize: 14,
179
+ overflow: "hidden",
180
+ textOverflow: "ellipsis",
181
+ whiteSpace: "nowrap",
182
+ }}
183
+ >
184
+ {image?.name || "-"}
185
+ </Typography>
186
+ {hasMany && (
187
+ <Typography sx={{ fontFamily: FONTS.AvenirNext, fontSize: 12, color: COLORS.s3 }}>
188
+ {index + 1}/{values.length}
189
+ </Typography>
190
+ )}
191
+ {approveRejectButtons && (
192
+ <>
193
+ <Tooltip title="Validar" arrow>
194
+ <IconButton
195
+ aria-label="Validar"
196
+ onClick={(e) => setMenuAnchor(e.currentTarget)}
197
+ sx={{ width: 30, height: 30, p: 0, border: `1px solid ${COLORS.s2}`, borderRadius: "50%" }}
198
+ >
199
+ <Box component="img" src={VALIDATION_ICONS[validation] ?? nullIcon} alt="" sx={{ width: 16 }} />
200
+ </IconButton>
201
+ </Tooltip>
202
+ <Menu anchorEl={menuAnchor} open={Boolean(menuAnchor)} onClose={() => setMenuAnchor(null)}>
203
+ <MenuItem onClick={() => validate("approve")}>
204
+ <ListItemIcon>
205
+ <Box component="img" src={acceptIcon} alt="" sx={{ width: 16 }} />
206
+ </ListItemIcon>
207
+ <ListItemText>Validado</ListItemText>
208
+ </MenuItem>
209
+ <MenuItem onClick={() => validate("reject")}>
210
+ <ListItemIcon>
211
+ <Box component="img" src={rejectIcon} alt="" sx={{ width: 16 }} />
212
+ </ListItemIcon>
213
+ <ListItemText>Rechazado</ListItemText>
214
+ </MenuItem>
215
+ </Menu>
216
+ </>
217
+ )}
218
+ <IconButton
219
+ aria-label="Cerrar"
220
+ onClick={close}
221
+ sx={{ width: 30, height: 30, color: COLORS.white, border: `1px solid ${COLORS.s2}`, borderRadius: "3px" }}
222
+ >
223
+ <CloseIcon fontSize="small" />
224
+ </IconButton>
225
+ </Stack>
226
+
227
+ <Box sx={{ borderTop: `1px solid ${COLORS.s4}`, pt: 1 }}>
228
+ <DataRow label="Tipo de toma" value={shotName} />
229
+ <DataRow label="Formato" value={image?.ext} />
230
+ <DataRow label="Tamaño de imagen" value={size} />
231
+ </Box>
232
+
233
+ {hasMany && (
234
+ <Box sx={{ mt: { sm: "auto" } }}>
235
+ <Swiper
236
+ modules={[FreeMode, Mousewheel]}
237
+ slidesPerView="auto"
238
+ spaceBetween={6}
239
+ freeMode
240
+ mousewheel={{ forceToAxis: true }}
241
+ style={{ width: "100%" }}
242
+ >
243
+ {values.map((item, i) => (
244
+ <SwiperSlide key={`${i}-${item?.name}`} style={{ width: 85 }}>
245
+ <ButtonBase
246
+ aria-label={`Ver imagen ${i + 1}`}
247
+ aria-pressed={i === index}
248
+ onClick={() => setIndex(i)}
249
+ sx={{
250
+ width: 85,
251
+ height: 55,
252
+ borderRadius: "3px",
253
+ overflow: "hidden",
254
+ bgcolor: COLORS.white,
255
+ outline: i === index ? `2px solid ${COLORS.magenta_s2}` : "none",
256
+ outlineOffset: -2,
257
+ }}
258
+ >
259
+ <ProductMedia src={item.src} ext={item.ext} alt={item.altText} thumbnail />
260
+ </ButtonBase>
261
+ </SwiperSlide>
262
+ ))}
263
+ </Swiper>
264
+ </Box>
265
+ )}
266
+ </Stack>
267
+ </Dialog>
268
+ );
269
+ };
@@ -0,0 +1,332 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import {
3
+ Box,
4
+ ButtonBase,
5
+ IconButton,
6
+ InputBase,
7
+ Paper,
8
+ Stack,
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableRow,
13
+ Tooltip,
14
+ Typography,
15
+ } from "@mui/material";
16
+ import { Swiper, SwiperSlide } from "swiper/react";
17
+ import { FreeMode, Keyboard, Mousewheel } from "swiper";
18
+ import "swiper/swiper.min.css";
19
+
20
+ import openModalIcon from "../../../../assets/images/generalButton/openModal.svg";
21
+
22
+ import { ProductMedia } from "./ProductMedia";
23
+ import { ServiceActions } from "./ServiceActions";
24
+ import { COLORS, FONTS } from "./tokens";
25
+
26
+ /*
27
+ Panel de imagen: la imagen activa, las miniaturas y los datos de la imagen.
28
+
29
+ Reemplaza a `organisms/ImagePreviewer` (EditionActiveImage + ImageSelector) y a
30
+ `organisms/ImageDataTable` (TableHeader + filas), con los mismos props, hecho
31
+ con MUI y Swiper. Se estira al ancho de su columna en vez de medir 340px fijos:
32
+ en desktop ocupa el panel izquierdo y en tablet/celular va arriba del contenido.
33
+ */
34
+
35
+ // --- Vista previa ----------------------------------------------------------
36
+
37
+ const Preview = ({ imagesArray, activeImage, setActiveImage, setShowModal, currentIndex }) => {
38
+ const images = imagesArray?.values ?? [];
39
+
40
+ return (
41
+ <Box>
42
+ <Box
43
+ sx={{
44
+ position: "relative",
45
+ width: "100%",
46
+ // Misma proporción que el visor original (340 x 295).
47
+ aspectRatio: "340 / 295",
48
+ bgcolor: COLORS.white,
49
+ }}
50
+ >
51
+ <ProductMedia src={activeImage?.src} ext={activeImage?.ext} alt={activeImage?.name} />
52
+ <Tooltip title="Ver en grande" arrow>
53
+ <IconButton
54
+ aria-label="Ver en grande"
55
+ onClick={() => setShowModal(true)}
56
+ sx={{
57
+ position: "absolute",
58
+ right: 4,
59
+ bottom: 4,
60
+ width: 30,
61
+ height: 30,
62
+ p: 0,
63
+ backgroundImage: `url(${openModalIcon})`,
64
+ backgroundRepeat: "no-repeat",
65
+ backgroundPosition: "center",
66
+ }}
67
+ />
68
+ </Tooltip>
69
+ </Box>
70
+
71
+ {images.length > 0 && (
72
+ <Box sx={{ mt: 1 }}>
73
+ <Swiper
74
+ modules={[FreeMode, Mousewheel, Keyboard]}
75
+ slidesPerView="auto"
76
+ spaceBetween={4}
77
+ freeMode
78
+ mousewheel={{ forceToAxis: true }}
79
+ keyboard={{ enabled: true }}
80
+ style={{ width: "100%" }}
81
+ >
82
+ {images.map((image, index) => {
83
+ const selected = index === currentIndex;
84
+ return (
85
+ <SwiperSlide key={`${index}-${image?.name}`} style={{ width: 85 }}>
86
+ <ButtonBase
87
+ aria-label={`Ver imagen ${index + 1}`}
88
+ aria-pressed={selected}
89
+ onClick={() => setActiveImage(index)}
90
+ sx={{
91
+ width: 85,
92
+ height: 55,
93
+ borderRadius: "3px",
94
+ overflow: "hidden",
95
+ outline: selected ? `2px solid ${COLORS.magenta_s2}` : "none",
96
+ outlineOffset: -2,
97
+ bgcolor: COLORS.white,
98
+ }}
99
+ >
100
+ <ProductMedia src={image.src} ext={image.ext} alt={image.altText} thumbnail />
101
+ </ButtonBase>
102
+ </SwiperSlide>
103
+ );
104
+ })}
105
+ </Swiper>
106
+ </Box>
107
+ )}
108
+ </Box>
109
+ );
110
+ };
111
+
112
+ // --- Datos de la imagen ----------------------------------------------------
113
+
114
+ const labelSx = {
115
+ fontFamily: FONTS.AvenirNext,
116
+ fontSize: 11,
117
+ lineHeight: "18px",
118
+ color: COLORS.s4,
119
+ };
120
+
121
+ const DataRow = ({ label, children }) => (
122
+ <TableRow sx={{ "& td, & th": { border: 0, py: "2px", px: 0 } }}>
123
+ <TableCell component="th" scope="row" sx={labelSx}>
124
+ {label}
125
+ </TableCell>
126
+ <TableCell align="right" sx={{ ...labelSx, pl: 1 }}>
127
+ {children}
128
+ </TableCell>
129
+ </TableRow>
130
+ );
131
+
132
+ const GrayValue = ({ children }) => (
133
+ <Box
134
+ component="span"
135
+ sx={{
136
+ display: "inline-block",
137
+ maxWidth: "100%",
138
+ px: "3px",
139
+ py: "1px",
140
+ borderRadius: "3px",
141
+ bgcolor: COLORS.s2,
142
+ color: COLORS.s5,
143
+ fontFamily: FONTS.AvenirNext,
144
+ fontSize: 11,
145
+ lineHeight: "19px",
146
+ overflowWrap: "anywhere",
147
+ }}
148
+ >
149
+ {children}
150
+ </Box>
151
+ );
152
+
153
+ /**
154
+ * Valor que se edita con un clic, como `atoms/LabelToInput`: se muestra como
155
+ * etiqueta y al hacer clic se vuelve campo de texto; al salir, vuelve a ser
156
+ * etiqueta. Cada cambio se avisa con el mismo payload que el original.
157
+ */
158
+ const EditableValue = ({ defaultValue, onChange }) => {
159
+ const [editing, setEditing] = useState(false);
160
+ const [value, setValue] = useState(defaultValue === "" ? "-" : defaultValue);
161
+ const inputRef = useRef(null);
162
+
163
+ useEffect(() => setValue(defaultValue === "" ? "-" : defaultValue), [defaultValue]);
164
+ useEffect(() => {
165
+ if (editing) inputRef.current?.focus();
166
+ }, [editing]);
167
+
168
+ if (!editing) {
169
+ return (
170
+ <ButtonBase
171
+ onClick={() => setEditing(true)}
172
+ aria-label="Editar valor"
173
+ sx={{ borderRadius: "3px", justifyContent: "flex-end", maxWidth: "100%" }}
174
+ >
175
+ <GrayValue>{value ?? "-"}</GrayValue>
176
+ </ButtonBase>
177
+ );
178
+ }
179
+
180
+ return (
181
+ <InputBase
182
+ inputRef={inputRef}
183
+ value={value ?? ""}
184
+ onChange={(e) => {
185
+ setValue(e.target.value);
186
+ onChange(e);
187
+ }}
188
+ onBlur={() => setEditing(false)}
189
+ onKeyDown={(e) => e.key === "Enter" && setEditing(false)}
190
+ sx={{
191
+ width: 120,
192
+ maxWidth: "100%",
193
+ fontFamily: FONTS.AvenirNext,
194
+ fontSize: 12,
195
+ border: `1px solid ${COLORS.s3}`,
196
+ borderRadius: "3px",
197
+ px: 0.5,
198
+ }}
199
+ />
200
+ );
201
+ };
202
+
203
+ const ImageData = ({
204
+ activeImage,
205
+ lists,
206
+ retailerSelected,
207
+ setImages,
208
+ assignationsImages,
209
+ imagesStatus,
210
+ setAssignation,
211
+ isRetailer,
212
+ onClickSave,
213
+ showSaveButton,
214
+ version,
215
+ setShowVersionSelector,
216
+ shotThd,
217
+ }) => {
218
+ const shotName = lists?.inputs?.find((f) => f.id === +activeImage?.image_id)?.name || "-";
219
+ const thdShot =
220
+ lists?.imageShotType?.find((f) => f.id === +activeImage?.image_shot_type)?.type_shot || "-";
221
+ const size =
222
+ activeImage?.width && activeImage.height ? `${activeImage.width}x${activeImage.height}px` : "-";
223
+
224
+ return (
225
+ <Paper
226
+ variant="outlined"
227
+ sx={{ mt: { xs: 2, sm: 0, md: 2 }, p: "10px", borderColor: COLORS.s2, borderRadius: "3px", minWidth: 0 }}
228
+ >
229
+ <Stack
230
+ direction="row"
231
+ alignItems="center"
232
+ flexWrap="wrap"
233
+ gap={1}
234
+ sx={{ pb: "5px", borderBottom: `1px solid ${COLORS.s2}` }}
235
+ >
236
+ <Typography
237
+ title={activeImage?.name || "-"}
238
+ sx={{
239
+ fontFamily: FONTS.Raleway,
240
+ fontSize: 14,
241
+ lineHeight: "23px",
242
+ color: COLORS.s5,
243
+ flex: "1 1 90px",
244
+ minWidth: 0,
245
+ overflow: "hidden",
246
+ textOverflow: "ellipsis",
247
+ whiteSpace: "nowrap",
248
+ }}
249
+ >
250
+ {activeImage?.name || "-"}
251
+ </Typography>
252
+ {/* Igual que en la vista original: aquí no hay panel de asignación
253
+ (canAssign no se pasa) ni botones de imágenes. */}
254
+ <ServiceActions
255
+ status={imagesStatus}
256
+ assig={assignationsImages}
257
+ setAssignation={setAssignation}
258
+ isRetailer={isRetailer}
259
+ onClickSave={onClickSave}
260
+ showSaveButton={showSaveButton}
261
+ version={version}
262
+ setShowVersionSelector={setShowVersionSelector}
263
+ />
264
+ </Stack>
265
+
266
+ <Table size="small" sx={{ mt: "4px", tableLayout: "fixed" }}>
267
+ <TableBody>
268
+ <DataRow label="Tipo de toma">
269
+ <GrayValue>{shotName}</GrayValue>
270
+ </DataRow>
271
+ {shotThd && (
272
+ <DataRow label="Tipo de imagen THD">
273
+ <GrayValue>{thdShot}</GrayValue>
274
+ </DataRow>
275
+ )}
276
+ <DataRow label="Formato">{activeImage?.ext || "-"}</DataRow>
277
+ <DataRow label="Tamaño de imagen">{size}</DataRow>
278
+ {retailerSelected &&
279
+ lists?.attrForImgs?.general?.map((attr, index) => (
280
+ <DataRow key={`row-${index}-${attr?.name}`} label={attr?.name || "-"}>
281
+ <EditableValue
282
+ defaultValue={attr?.value}
283
+ onChange={(e) =>
284
+ setImages({
285
+ action: "changeAttrValue",
286
+ value: e.target.value || "-",
287
+ id: attr.id,
288
+ })
289
+ }
290
+ />
291
+ </DataRow>
292
+ ))}
293
+ </TableBody>
294
+ </Table>
295
+ </Paper>
296
+ );
297
+ };
298
+
299
+ // --- Panel completo --------------------------------------------------------
300
+
301
+ export const ImagePanel = ({
302
+ // vista previa
303
+ imagesArray,
304
+ activeImage,
305
+ currentIndex,
306
+ setActiveImage,
307
+ setShowModal,
308
+ // datos (mismos props que ImageDataTable)
309
+ ...dataProps
310
+ }) => (
311
+ // En tablet la imagen y sus datos van lado a lado: apilados, la imagen
312
+ // ocuparía todo el ancho y empujaría el contenido muy abajo.
313
+ <Box
314
+ sx={{
315
+ width: "100%",
316
+ minWidth: 0,
317
+ display: "grid",
318
+ gridTemplateColumns: { xs: "minmax(0, 1fr)", sm: "repeat(2, minmax(0, 1fr))", md: "minmax(0, 1fr)" },
319
+ columnGap: 2,
320
+ alignItems: "start",
321
+ }}
322
+ >
323
+ <Preview
324
+ imagesArray={imagesArray}
325
+ activeImage={activeImage}
326
+ currentIndex={currentIndex}
327
+ setActiveImage={setActiveImage}
328
+ setShowModal={setShowModal}
329
+ />
330
+ <ImageData activeImage={activeImage} {...dataProps} />
331
+ </Box>
332
+ );