contentoh-components-library 21.6.59 → 21.6.61

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 (78) hide show
  1. package/dist/components/organisms/ServicesNavigation/ServiceToolbar.js +210 -0
  2. package/dist/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +138 -0
  3. package/dist/components/organisms/ServicesNavigation/ServicesRail.js +705 -0
  4. package/dist/components/organisms/ServicesNavigation/catalog.js +505 -0
  5. package/dist/components/organisms/ServicesNavigation/index.js +337 -0
  6. package/dist/components/organisms/ServicesNavigation/model.js +167 -0
  7. package/dist/components/organisms/ServicesNavigation/ui.js +371 -0
  8. package/dist/components/organisms/ServicesNavigation/views.js +1237 -0
  9. package/dist/components/organisms/ServicesWorkspace/AssigneePicker.js +459 -0
  10. package/dist/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +119 -0
  11. package/dist/components/organisms/ServicesWorkspace/demo.js +137 -0
  12. package/dist/components/organisms/ServicesWorkspace/index.js +813 -0
  13. package/dist/components/organisms/ServicesWorkspace/servicesModel.js +351 -0
  14. package/dist/components/organisms/ServicesWorkspace/tooltipStyles.js +37 -0
  15. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +40 -0
  16. package/dist/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +387 -0
  17. package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +13 -4
  18. package/dist/components/pages/ProductEdition/ProductEdition.stories.js +148 -0
  19. package/dist/components/pages/ProductEdition/ProductEditionView.js +1355 -0
  20. package/dist/components/pages/ProductEdition/ServiceHeader.js +142 -0
  21. package/dist/components/pages/ProductEdition/api.js +256 -0
  22. package/dist/components/pages/ProductEdition/index.js +37 -0
  23. package/dist/components/pages/ProductEdition/permissions.js +200 -0
  24. package/dist/components/pages/ProductEdition/permissions.test.js +320 -0
  25. package/dist/components/pages/ProductEdition/sections/CatalogServicePanel.js +237 -0
  26. package/dist/components/pages/ProductEdition/sections/EditionTopBar.js +174 -0
  27. package/dist/components/pages/ProductEdition/sections/ImagePanel.js +440 -0
  28. package/dist/components/pages/ProductEdition/sections/ServiceTeam.js +275 -0
  29. package/dist/components/pages/ProductEdition/sections/ServiceTools.js +186 -0
  30. package/dist/components/pages/ProductEdition/services.js +233 -0
  31. package/dist/components/pages/ProductEdition/services.test.js +195 -0
  32. package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +15 -2
  33. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +2 -2
  34. package/dist/components/pages/RetailerProductEdition/index.js +57 -40
  35. package/dist/components/pages/RetailerProductEdition/utils.js +6 -2
  36. package/dist/index.js +13 -0
  37. package/package.json +1 -1
  38. package/src/components/organisms/ServicesNavigation/ServiceToolbar.jsx +143 -0
  39. package/src/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +101 -0
  40. package/src/components/organisms/ServicesNavigation/ServicesRail.jsx +591 -0
  41. package/src/components/organisms/ServicesNavigation/catalog.js +180 -0
  42. package/src/components/organisms/ServicesNavigation/index.jsx +232 -0
  43. package/src/components/organisms/ServicesNavigation/model.js +85 -0
  44. package/src/components/organisms/ServicesNavigation/ui.jsx +229 -0
  45. package/src/components/organisms/ServicesNavigation/views.jsx +705 -0
  46. package/src/components/organisms/ServicesWorkspace/AssigneePicker.js +348 -0
  47. package/src/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +61 -0
  48. package/src/components/organisms/ServicesWorkspace/demo.js +58 -0
  49. package/src/components/organisms/ServicesWorkspace/index.js +556 -0
  50. package/src/components/organisms/ServicesWorkspace/servicesModel.js +203 -0
  51. package/src/components/organisms/ServicesWorkspace/tooltipStyles.js +27 -0
  52. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +17 -0
  53. package/src/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +175 -0
  54. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +8 -4
  55. package/src/components/pages/ProductEdition/ProductEdition.stories.js +106 -0
  56. package/src/components/pages/ProductEdition/ProductEditionView.jsx +1142 -0
  57. package/src/components/pages/ProductEdition/ServiceHeader.jsx +104 -0
  58. package/src/components/pages/ProductEdition/api.js +84 -0
  59. package/src/components/pages/ProductEdition/index.js +21 -0
  60. package/src/components/pages/ProductEdition/permissions.js +128 -0
  61. package/src/components/pages/ProductEdition/permissions.test.js +249 -0
  62. package/src/components/pages/ProductEdition/sections/CatalogServicePanel.jsx +123 -0
  63. package/src/components/pages/ProductEdition/sections/EditionTopBar.jsx +134 -0
  64. package/src/components/pages/ProductEdition/sections/ImagePanel.jsx +342 -0
  65. package/src/components/pages/ProductEdition/sections/ServiceTeam.jsx +184 -0
  66. package/src/components/pages/ProductEdition/sections/ServiceTools.jsx +134 -0
  67. package/src/components/pages/ProductEdition/services.js +187 -0
  68. package/src/components/pages/ProductEdition/services.test.js +88 -0
  69. package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +12 -0
  70. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +6 -2
  71. package/src/components/pages/RetailerProductEdition/index.js +28 -3
  72. package/src/components/pages/RetailerProductEdition/utils.js +5 -0
  73. package/src/index.js +1 -0
  74. package/.claude/settings.local.json +0 -7
  75. package/dist/ai/prompts/attribute.prompt.js +0 -28
  76. package/dist/ai/prompts/description.prompt.js +0 -33
  77. package/dist/ai/schemas/attribute.schema.js +0 -35
  78. package/dist/ai/schemas/description.schema.js +0 -27
@@ -0,0 +1,123 @@
1
+ import { useState } from "react";
2
+ import { Alert, Box, CircularProgress, MenuItem, Select, Stack, Typography } from "@mui/material";
3
+
4
+ import { StatusChip } from "../../CreatorsProductEdition/sections/StatusChip";
5
+ import { COLORS, FONTS } from "../../CreatorsProductEdition/sections/tokens";
6
+
7
+ /*
8
+ Panel de un servicio del catálogo en la edición (los que no tienen editor):
9
+ su estatus en cada cadena y, si la persona puede, el selector para cambiarlo.
10
+
11
+ Sale de GET /product-services (`service.api`):
12
+ - Flujo simple (casilla, avance): se elige el estatus de `status_options`
13
+ con PUT /product-services/set-status, por cadena.
14
+ - Flujo completo: solo se ve; lo moverá el motor de estatus.
15
+ - De registro: solo se ve.
16
+ Los servicios de los uno por producto aparecen como «Producto» (cadena 0).
17
+ */
18
+
19
+ const FLOW_NOTE = {
20
+ workflow:
21
+ "Este servicio sigue el flujo de aprobación (especialista, coordinador y auditor). " +
22
+ "Su estatus todavía no se cambia desde aquí: lo moverá el motor de estatus.",
23
+ flag: "Este servicio solo se registra en el producto: no cambia de estatus.",
24
+ };
25
+
26
+ const chipProps = (status) =>
27
+ status
28
+ ? { status: status.status_name, label: status.display_name ?? status.status_name, color: status.color ?? undefined }
29
+ : { status: null, label: "Sin estatus" };
30
+
31
+ export const CatalogServicePanel = ({ service, onSetStatus }) => {
32
+ const api = service?.api;
33
+ const [savingRetailer, setSavingRetailer] = useState(null);
34
+ const [error, setError] = useState(null);
35
+ if (!api) return null;
36
+
37
+ const flowType = api.status_flow?.flow_type ?? "workflow";
38
+ const canSet = Boolean(api.actions?.set_status) && flowType === "simple";
39
+ const options = api.status_options ?? [];
40
+
41
+ const change = async (retailerId, statusName) => {
42
+ setError(null);
43
+ setSavingRetailer(retailerId);
44
+ try {
45
+ await onSetStatus(statusName, [retailerId]);
46
+ } catch (err) {
47
+ setError(err?.message ?? "No se pudo cambiar el estatus.");
48
+ } finally {
49
+ setSavingRetailer(null);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <Box
55
+ sx={{
56
+ p: 2,
57
+ borderRadius: "10px",
58
+ border: `1px solid ${COLORS.s2}`,
59
+ bgcolor: COLORS.white,
60
+ maxWidth: 720,
61
+ }}
62
+ >
63
+ <Typography sx={{ fontFamily: FONTS.Lato, fontSize: 14, fontWeight: 700, color: COLORS.s5, mb: 0.5 }}>
64
+ Estatus {api.per_retailer ? "por cadena" : "del producto"}
65
+ </Typography>
66
+ {FLOW_NOTE[flowType] && (
67
+ <Typography sx={{ fontFamily: FONTS.Lato, fontSize: 13, color: COLORS.s4, mb: 1.5 }}>
68
+ {FLOW_NOTE[flowType]}
69
+ </Typography>
70
+ )}
71
+
72
+ <Stack spacing={1}>
73
+ {(api.retailers ?? []).map((retailer) => (
74
+ <Stack
75
+ key={retailer.id_retailer}
76
+ direction="row"
77
+ alignItems="center"
78
+ spacing={1.5}
79
+ sx={{ py: 0.75, borderTop: `1px solid ${COLORS.s2}`, "&:first-of-type": { borderTop: "none" } }}
80
+ >
81
+ <Typography sx={{ fontFamily: FONTS.Lato, fontSize: 13, color: COLORS.s5, flex: "1 1 auto", minWidth: 0 }} noWrap>
82
+ {api.per_retailer ? retailer.retailer_name ?? `Cadena ${retailer.id_retailer}` : "Producto"}
83
+ </Typography>
84
+ {canSet ? (
85
+ <Select
86
+ size="small"
87
+ value={retailer.status?.status_name ?? ""}
88
+ disabled={savingRetailer !== null}
89
+ onChange={(event) => change(retailer.id_retailer, event.target.value)}
90
+ renderValue={(value) => {
91
+ const option = options.find((item) => item.status_name === value) ?? retailer.status;
92
+ return <StatusChip {...chipProps(option)} oval />;
93
+ }}
94
+ sx={{ minWidth: 170, fontFamily: FONTS.Lato, fontSize: 13 }}
95
+ inputProps={{ "aria-label": `Estatus de ${service.display_name}` }}
96
+ >
97
+ {options.map((option) => (
98
+ <MenuItem key={option.status_name} value={option.status_name}>
99
+ <StatusChip {...chipProps(option)} oval />
100
+ </MenuItem>
101
+ ))}
102
+ </Select>
103
+ ) : (
104
+ <StatusChip {...chipProps(retailer.status)} oval />
105
+ )}
106
+ {savingRetailer === retailer.id_retailer && <CircularProgress size={16} sx={{ color: COLORS.magenta_s2 }} />}
107
+ </Stack>
108
+ ))}
109
+ </Stack>
110
+
111
+ {flowType === "simple" && !canSet && (
112
+ <Typography sx={{ fontFamily: FONTS.Lato, fontSize: 12, color: COLORS.s4, mt: 1.5 }}>
113
+ Lo cambia quien está asignado al servicio, un administrador o un coordinador.
114
+ </Typography>
115
+ )}
116
+ {error && (
117
+ <Alert severity="error" sx={{ mt: 1.5, fontFamily: FONTS.Lato, fontSize: 13 }} onClose={() => setError(null)}>
118
+ {error}
119
+ </Alert>
120
+ )}
121
+ </Box>
122
+ );
123
+ };
@@ -0,0 +1,134 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { Box, IconButton, Stack, Tooltip, Typography } from "@mui/material";
3
+ import CloseIcon from "@mui/icons-material/Close";
4
+
5
+ import { Chat } from "../../../organisms/Chat";
6
+ import auditIcon from "../../../../assets/images/generalButton/Icono AB.svg";
7
+ import { COLORS, FONTS } from "../../CreatorsProductEdition/sections/tokens";
8
+
9
+ /*
10
+ Barra superior de la edición. Reemplaza a `molecules/HeaderTop` con lo mismo
11
+ (título, chat, comparar versión auditable y cerrar), pero:
12
+ - el título es más chico y la barra tiene aire y una línea abajo, para que
13
+ no quede encimado sobre la imagen y el nombre del producto;
14
+ - el botón de cerrar queda dentro del margen y con borde, para que siempre
15
+ se vea (antes iba pegado al borde derecho).
16
+
17
+ Reporta su alto con `setHeaderTop`, como HeaderTop: la pantalla lo resta para
18
+ calcular el alto de las columnas. Incluye el margen de abajo.
19
+ */
20
+
21
+ const GAP_BELOW = 12;
22
+
23
+ const buttonSx = {
24
+ width: 32,
25
+ height: 32,
26
+ border: `1px solid ${COLORS.s3}`,
27
+ bgcolor: COLORS.white,
28
+ "&:hover": { bgcolor: COLORS.s2, borderColor: COLORS.s4 },
29
+ };
30
+
31
+ export const EditionTopBar = ({
32
+ setHeaderTop,
33
+ withChat,
34
+ chatType,
35
+ productSelected,
36
+ token,
37
+ auditableVersion,
38
+ setCompare,
39
+ isAuditor,
40
+ activeRetailer,
41
+ onClose = () => {
42
+ window.location.href = "/products";
43
+ },
44
+ }) => {
45
+ const ref = useRef(null);
46
+
47
+ useEffect(() => {
48
+ if (ref.current && setHeaderTop) setHeaderTop(ref.current.offsetHeight + GAP_BELOW);
49
+ // eslint-disable-next-line react-hooks/exhaustive-deps
50
+ }, []);
51
+
52
+ // Mismo chat que HeaderTop, según `chatType`.
53
+ const chat = useMemo(() => {
54
+ if (!withChat || !chatType || !productSelected || !token) return null;
55
+ if (chatType === "product_status") {
56
+ if (!activeRetailer?.id) return null;
57
+ return (
58
+ <Chat
59
+ chatType="product_status"
60
+ chatContainerType="popUp"
61
+ chatData={{
62
+ id: productSelected?.article?.id_article || productSelected?.id_article,
63
+ version: productSelected?.version,
64
+ retailerId: activeRetailer.id,
65
+ status: productSelected?.status,
66
+ userToken: token,
67
+ }}
68
+ size={18}
69
+ activeRetailer={activeRetailer}
70
+ />
71
+ );
72
+ }
73
+ return (
74
+ <Chat
75
+ chatType="merchant_product"
76
+ chatContainerType="popUp"
77
+ chatData={{ userToken: token, id: productSelected.id_article, version: productSelected.version }}
78
+ size={18}
79
+ activeRetailer={activeRetailer}
80
+ />
81
+ );
82
+ // eslint-disable-next-line react-hooks/exhaustive-deps
83
+ }, [withChat, chatType, productSelected?.id_article, productSelected?.version, token, activeRetailer?.id]);
84
+
85
+ return (
86
+ <Box
87
+ ref={ref}
88
+ component="header"
89
+ sx={{
90
+ display: "flex",
91
+ alignItems: "center",
92
+ gap: 1.5,
93
+ px: { xs: 1.5, sm: 2 },
94
+ py: 1,
95
+ mb: `${GAP_BELOW}px`,
96
+ borderBottom: `1px solid ${COLORS.s2}`,
97
+ minWidth: 0,
98
+ }}
99
+ >
100
+ <Typography
101
+ component="h1"
102
+ sx={{
103
+ flex: 1,
104
+ minWidth: 0,
105
+ fontFamily: FONTS.Raleway_600,
106
+ fontSize: 16,
107
+ lineHeight: "24px",
108
+ color: COLORS.s5,
109
+ overflow: "hidden",
110
+ textOverflow: "ellipsis",
111
+ whiteSpace: "nowrap",
112
+ }}
113
+ >
114
+ Edición de producto
115
+ </Typography>
116
+
117
+ <Stack direction="row" alignItems="center" spacing={1} sx={{ flexShrink: 0 }}>
118
+ {chat}
119
+ {isAuditor && auditableVersion && (
120
+ <Tooltip title="Comparar con la versión auditable" arrow>
121
+ <IconButton aria-label="Comparar con la versión auditable" onClick={() => setCompare((value) => !value)} sx={buttonSx}>
122
+ <img src={auditIcon} alt="" width={16} height={16} />
123
+ </IconButton>
124
+ </Tooltip>
125
+ )}
126
+ <Tooltip title="Cerrar" arrow>
127
+ <IconButton aria-label="Cerrar la edición" onClick={onClose} sx={buttonSx}>
128
+ <CloseIcon sx={{ fontSize: 18, color: COLORS.s5 }} />
129
+ </IconButton>
130
+ </Tooltip>
131
+ </Stack>
132
+ </Box>
133
+ );
134
+ };
@@ -0,0 +1,342 @@
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 "../../CreatorsProductEdition/sections/ProductMedia";
23
+ import { StatusChip } from "../../CreatorsProductEdition/sections/StatusChip";
24
+ import { COLORS, FONTS } from "../../CreatorsProductEdition/sections/tokens";
25
+ import { ServiceTools } from "./ServiceTools";
26
+
27
+ /*
28
+ Panel de imagen de ProductEdition: la imagen activa, las miniaturas y los
29
+ datos de la imagen.
30
+
31
+ Copia del de CreatorsProductEdition con el diseño de la propuesta de
32
+ navegación por servicios: la imagen en una tarjeta con fondo claro y bordes
33
+ redondeados, miniaturas cuadradas con borde rosa en la activa, y la cabecera
34
+ de los datos sin el botón de ayuda (estatus + versión + guardar).
35
+
36
+ Reemplaza a `organisms/ImagePreviewer` (EditionActiveImage + ImageSelector) y a
37
+ `organisms/ImageDataTable` (TableHeader + filas), con los mismos props, hecho
38
+ con MUI y Swiper. Se estira al ancho de su columna en vez de medir 340px fijos:
39
+ en desktop ocupa el panel izquierdo y en tablet/celular va arriba del contenido.
40
+ */
41
+
42
+ // --- Vista previa ----------------------------------------------------------
43
+
44
+ const Preview = ({ imagesArray, activeImage, setActiveImage, setShowModal, currentIndex }) => {
45
+ const images = imagesArray?.values ?? [];
46
+
47
+ return (
48
+ <Box>
49
+ <Box
50
+ sx={{
51
+ position: "relative",
52
+ width: "100%",
53
+ // Misma proporción que el visor original (340 x 295).
54
+ aspectRatio: "340 / 295",
55
+ p: 1.5,
56
+ boxSizing: "border-box",
57
+ borderRadius: "10px",
58
+ border: `1px solid ${COLORS.s2}`,
59
+ background: `linear-gradient(135deg, ${COLORS.white}, ${COLORS.s1})`,
60
+ overflow: "hidden",
61
+ }}
62
+ >
63
+ <ProductMedia src={activeImage?.src} ext={activeImage?.ext} alt={activeImage?.name} />
64
+ <Tooltip title="Ver en grande" arrow>
65
+ <IconButton
66
+ aria-label="Ver en grande"
67
+ onClick={() => setShowModal(true)}
68
+ sx={{
69
+ position: "absolute",
70
+ right: 8,
71
+ bottom: 8,
72
+ width: 32,
73
+ height: 32,
74
+ p: 0,
75
+ bgcolor: COLORS.white,
76
+ border: `1px solid ${COLORS.s2}`,
77
+ backgroundImage: `url(${openModalIcon})`,
78
+ backgroundRepeat: "no-repeat",
79
+ backgroundPosition: "center",
80
+ backgroundSize: 16,
81
+ "&:hover": { bgcolor: COLORS.s2 },
82
+ }}
83
+ />
84
+ </Tooltip>
85
+ </Box>
86
+
87
+ {images.length > 0 && (
88
+ <Box sx={{ mt: 1.25 }}>
89
+ <Swiper
90
+ modules={[FreeMode, Mousewheel, Keyboard]}
91
+ slidesPerView="auto"
92
+ spaceBetween={8}
93
+ freeMode
94
+ mousewheel={{ forceToAxis: true }}
95
+ keyboard={{ enabled: true }}
96
+ style={{ width: "100%" }}
97
+ >
98
+ {images.map((image, index) => {
99
+ const selected = index === currentIndex;
100
+ return (
101
+ <SwiperSlide key={`${index}-${image?.name}`} style={{ width: 64 }}>
102
+ <ButtonBase
103
+ aria-label={`Ver imagen ${index + 1}`}
104
+ aria-pressed={selected}
105
+ onClick={() => setActiveImage(index)}
106
+ sx={{
107
+ width: 64,
108
+ height: 64,
109
+ p: "4px",
110
+ boxSizing: "border-box",
111
+ borderRadius: "8px",
112
+ overflow: "hidden",
113
+ border: `${selected ? 2 : 1}px solid ${selected ? COLORS.magenta_s2 : COLORS.s2}`,
114
+ bgcolor: COLORS.white,
115
+ "&:hover": { borderColor: selected ? COLORS.magenta_s2 : COLORS.s3 },
116
+ }}
117
+ >
118
+ <ProductMedia src={image.src} ext={image.ext} alt={image.altText} thumbnail />
119
+ </ButtonBase>
120
+ </SwiperSlide>
121
+ );
122
+ })}
123
+ </Swiper>
124
+ </Box>
125
+ )}
126
+ </Box>
127
+ );
128
+ };
129
+
130
+ // --- Datos de la imagen ----------------------------------------------------
131
+
132
+ const labelSx = {
133
+ fontFamily: FONTS.AvenirNext,
134
+ fontSize: 11,
135
+ lineHeight: "18px",
136
+ color: COLORS.s4,
137
+ };
138
+
139
+ const DataRow = ({ label, children }) => (
140
+ <TableRow sx={{ "& td, & th": { border: 0, py: "2px", px: 0 } }}>
141
+ <TableCell component="th" scope="row" sx={labelSx}>
142
+ {label}
143
+ </TableCell>
144
+ <TableCell align="right" sx={{ ...labelSx, pl: 1 }}>
145
+ {children}
146
+ </TableCell>
147
+ </TableRow>
148
+ );
149
+
150
+ const GrayValue = ({ children }) => (
151
+ <Box
152
+ component="span"
153
+ sx={{
154
+ display: "inline-block",
155
+ maxWidth: "100%",
156
+ px: "3px",
157
+ py: "1px",
158
+ borderRadius: "3px",
159
+ bgcolor: COLORS.s2,
160
+ color: COLORS.s5,
161
+ fontFamily: FONTS.AvenirNext,
162
+ fontSize: 11,
163
+ lineHeight: "19px",
164
+ overflowWrap: "anywhere",
165
+ }}
166
+ >
167
+ {children}
168
+ </Box>
169
+ );
170
+
171
+ /**
172
+ * Valor que se edita con un clic, como `atoms/LabelToInput`: se muestra como
173
+ * etiqueta y al hacer clic se vuelve campo de texto; al salir, vuelve a ser
174
+ * etiqueta. Cada cambio se avisa con el mismo payload que el original.
175
+ */
176
+ const EditableValue = ({ defaultValue, onChange }) => {
177
+ const [editing, setEditing] = useState(false);
178
+ const [value, setValue] = useState(defaultValue === "" ? "-" : defaultValue);
179
+ const inputRef = useRef(null);
180
+
181
+ useEffect(() => setValue(defaultValue === "" ? "-" : defaultValue), [defaultValue]);
182
+ useEffect(() => {
183
+ if (editing) inputRef.current?.focus();
184
+ }, [editing]);
185
+
186
+ if (!editing) {
187
+ return (
188
+ <ButtonBase
189
+ onClick={() => setEditing(true)}
190
+ aria-label="Editar valor"
191
+ sx={{ borderRadius: "3px", justifyContent: "flex-end", maxWidth: "100%" }}
192
+ >
193
+ <GrayValue>{value ?? "-"}</GrayValue>
194
+ </ButtonBase>
195
+ );
196
+ }
197
+
198
+ return (
199
+ <InputBase
200
+ inputRef={inputRef}
201
+ value={value ?? ""}
202
+ onChange={(e) => {
203
+ setValue(e.target.value);
204
+ onChange(e);
205
+ }}
206
+ onBlur={() => setEditing(false)}
207
+ onKeyDown={(e) => e.key === "Enter" && setEditing(false)}
208
+ sx={{
209
+ width: 120,
210
+ maxWidth: "100%",
211
+ fontFamily: FONTS.AvenirNext,
212
+ fontSize: 12,
213
+ border: `1px solid ${COLORS.s3}`,
214
+ borderRadius: "3px",
215
+ px: 0.5,
216
+ }}
217
+ />
218
+ );
219
+ };
220
+
221
+ const ImageData = ({
222
+ activeImage,
223
+ lists,
224
+ retailerSelected,
225
+ setImages,
226
+ imagesStatus,
227
+ onClickSave,
228
+ showSaveButton,
229
+ version,
230
+ setShowVersionSelector,
231
+ shotThd,
232
+ }) => {
233
+ const shotName = lists?.inputs?.find((f) => f.id === +activeImage?.image_id)?.name || "-";
234
+ const thdShot =
235
+ lists?.imageShotType?.find((f) => f.id === +activeImage?.image_shot_type)?.type_shot || "-";
236
+ const size =
237
+ activeImage?.width && activeImage.height ? `${activeImage.width}x${activeImage.height}px` : "-";
238
+
239
+ return (
240
+ <Paper
241
+ variant="outlined"
242
+ sx={{ mt: { xs: 2, sm: 0, md: 2 }, p: 1.5, borderColor: COLORS.s2, borderRadius: "10px", minWidth: 0 }}
243
+ >
244
+ <Stack
245
+ direction="row"
246
+ alignItems="center"
247
+ flexWrap="wrap"
248
+ gap={1}
249
+ sx={{ pb: "5px", borderBottom: `1px solid ${COLORS.s2}` }}
250
+ >
251
+ <Typography
252
+ title={activeImage?.name || "-"}
253
+ sx={{
254
+ fontFamily: FONTS.Raleway,
255
+ fontSize: 14,
256
+ lineHeight: "23px",
257
+ color: COLORS.s5,
258
+ flex: "1 1 90px",
259
+ minWidth: 0,
260
+ overflow: "hidden",
261
+ textOverflow: "ellipsis",
262
+ whiteSpace: "nowrap",
263
+ }}
264
+ >
265
+ {activeImage?.name || "-"}
266
+ </Typography>
267
+ {imagesStatus && imagesStatus !== "-" && <StatusChip status={imagesStatus} oval />}
268
+ <ServiceTools
269
+ onClickSave={onClickSave}
270
+ showSaveButton={showSaveButton}
271
+ version={version}
272
+ setShowVersionSelector={setShowVersionSelector}
273
+ />
274
+ </Stack>
275
+
276
+ <Table size="small" sx={{ mt: "4px", tableLayout: "fixed" }}>
277
+ <TableBody>
278
+ <DataRow label="Tipo de toma">
279
+ <GrayValue>{shotName}</GrayValue>
280
+ </DataRow>
281
+ {shotThd && (
282
+ <DataRow label="Tipo de imagen THD">
283
+ <GrayValue>{thdShot}</GrayValue>
284
+ </DataRow>
285
+ )}
286
+ <DataRow label="Formato">{activeImage?.ext || "-"}</DataRow>
287
+ <DataRow label="Tamaño de imagen">{size}</DataRow>
288
+ {retailerSelected &&
289
+ lists?.attrForImgs?.general?.map((attr, index) => (
290
+ <DataRow key={`row-${index}-${attr?.name}`} label={attr?.name || "-"}>
291
+ <EditableValue
292
+ defaultValue={attr?.value}
293
+ onChange={(e) =>
294
+ setImages({
295
+ action: "changeAttrValue",
296
+ value: e.target.value || "-",
297
+ id: attr.id,
298
+ })
299
+ }
300
+ />
301
+ </DataRow>
302
+ ))}
303
+ </TableBody>
304
+ </Table>
305
+ </Paper>
306
+ );
307
+ };
308
+
309
+ // --- Panel completo --------------------------------------------------------
310
+
311
+ export const ImagePanel = ({
312
+ // vista previa
313
+ imagesArray,
314
+ activeImage,
315
+ currentIndex,
316
+ setActiveImage,
317
+ setShowModal,
318
+ // datos (mismos props que ImageDataTable)
319
+ ...dataProps
320
+ }) => (
321
+ // En tablet la imagen y sus datos van lado a lado: apilados, la imagen
322
+ // ocuparía todo el ancho y empujaría el contenido muy abajo.
323
+ <Box
324
+ sx={{
325
+ width: "100%",
326
+ minWidth: 0,
327
+ display: "grid",
328
+ gridTemplateColumns: { xs: "minmax(0, 1fr)", sm: "repeat(2, minmax(0, 1fr))", md: "minmax(0, 1fr)" },
329
+ columnGap: 2,
330
+ alignItems: "start",
331
+ }}
332
+ >
333
+ <Preview
334
+ imagesArray={imagesArray}
335
+ activeImage={activeImage}
336
+ currentIndex={currentIndex}
337
+ setActiveImage={setActiveImage}
338
+ setShowModal={setShowModal}
339
+ />
340
+ <ImageData activeImage={activeImage} {...dataProps} />
341
+ </Box>
342
+ );