contentoh-components-library 21.6.61 → 21.6.62

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 (88) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/ai/prompts/attribute.prompt.js +28 -0
  3. package/dist/ai/prompts/description.prompt.js +33 -0
  4. package/dist/ai/schemas/attribute.schema.js +35 -0
  5. package/dist/ai/schemas/description.schema.js +27 -0
  6. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +11 -5
  7. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +58 -31
  8. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +5 -1
  9. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +3 -40
  10. package/dist/components/pages/CreatorsProductEdition/sections/ImagesSection.js +18 -3
  11. package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +4 -13
  12. package/dist/components/pages/CreatorsProductEdition/sections/TabsBar.js +10 -0
  13. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +10 -2
  14. package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +2 -15
  15. package/dist/components/pages/RetailerProductEdition/index.js +40 -57
  16. package/dist/components/pages/RetailerProductEdition/utils.js +2 -6
  17. package/dist/index.js +0 -13
  18. package/package.json +1 -1
  19. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +22 -3
  20. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +36 -1
  21. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +15 -3
  22. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +8 -18
  23. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +24 -2
  24. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +4 -8
  25. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +9 -0
  26. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +6 -0
  27. package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +0 -12
  28. package/src/components/pages/RetailerProductEdition/index.js +3 -28
  29. package/src/components/pages/RetailerProductEdition/utils.js +0 -5
  30. package/src/index.js +0 -1
  31. package/dist/components/organisms/ServicesNavigation/ServiceToolbar.js +0 -210
  32. package/dist/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +0 -138
  33. package/dist/components/organisms/ServicesNavigation/ServicesRail.js +0 -705
  34. package/dist/components/organisms/ServicesNavigation/catalog.js +0 -505
  35. package/dist/components/organisms/ServicesNavigation/index.js +0 -337
  36. package/dist/components/organisms/ServicesNavigation/model.js +0 -167
  37. package/dist/components/organisms/ServicesNavigation/ui.js +0 -371
  38. package/dist/components/organisms/ServicesNavigation/views.js +0 -1237
  39. package/dist/components/organisms/ServicesWorkspace/AssigneePicker.js +0 -459
  40. package/dist/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +0 -119
  41. package/dist/components/organisms/ServicesWorkspace/demo.js +0 -137
  42. package/dist/components/organisms/ServicesWorkspace/index.js +0 -813
  43. package/dist/components/organisms/ServicesWorkspace/servicesModel.js +0 -351
  44. package/dist/components/organisms/ServicesWorkspace/tooltipStyles.js +0 -37
  45. package/dist/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +0 -387
  46. package/dist/components/pages/ProductEdition/ProductEdition.stories.js +0 -148
  47. package/dist/components/pages/ProductEdition/ProductEditionView.js +0 -1355
  48. package/dist/components/pages/ProductEdition/ServiceHeader.js +0 -142
  49. package/dist/components/pages/ProductEdition/api.js +0 -256
  50. package/dist/components/pages/ProductEdition/index.js +0 -37
  51. package/dist/components/pages/ProductEdition/permissions.js +0 -200
  52. package/dist/components/pages/ProductEdition/permissions.test.js +0 -320
  53. package/dist/components/pages/ProductEdition/sections/CatalogServicePanel.js +0 -237
  54. package/dist/components/pages/ProductEdition/sections/EditionTopBar.js +0 -174
  55. package/dist/components/pages/ProductEdition/sections/ImagePanel.js +0 -440
  56. package/dist/components/pages/ProductEdition/sections/ServiceTeam.js +0 -275
  57. package/dist/components/pages/ProductEdition/sections/ServiceTools.js +0 -186
  58. package/dist/components/pages/ProductEdition/services.js +0 -233
  59. package/dist/components/pages/ProductEdition/services.test.js +0 -195
  60. package/src/components/organisms/ServicesNavigation/ServiceToolbar.jsx +0 -143
  61. package/src/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +0 -101
  62. package/src/components/organisms/ServicesNavigation/ServicesRail.jsx +0 -591
  63. package/src/components/organisms/ServicesNavigation/catalog.js +0 -180
  64. package/src/components/organisms/ServicesNavigation/index.jsx +0 -232
  65. package/src/components/organisms/ServicesNavigation/model.js +0 -85
  66. package/src/components/organisms/ServicesNavigation/ui.jsx +0 -229
  67. package/src/components/organisms/ServicesNavigation/views.jsx +0 -705
  68. package/src/components/organisms/ServicesWorkspace/AssigneePicker.js +0 -348
  69. package/src/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +0 -61
  70. package/src/components/organisms/ServicesWorkspace/demo.js +0 -58
  71. package/src/components/organisms/ServicesWorkspace/index.js +0 -556
  72. package/src/components/organisms/ServicesWorkspace/servicesModel.js +0 -203
  73. package/src/components/organisms/ServicesWorkspace/tooltipStyles.js +0 -27
  74. package/src/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +0 -175
  75. package/src/components/pages/ProductEdition/ProductEdition.stories.js +0 -106
  76. package/src/components/pages/ProductEdition/ProductEditionView.jsx +0 -1142
  77. package/src/components/pages/ProductEdition/ServiceHeader.jsx +0 -104
  78. package/src/components/pages/ProductEdition/api.js +0 -84
  79. package/src/components/pages/ProductEdition/index.js +0 -21
  80. package/src/components/pages/ProductEdition/permissions.js +0 -128
  81. package/src/components/pages/ProductEdition/permissions.test.js +0 -249
  82. package/src/components/pages/ProductEdition/sections/CatalogServicePanel.jsx +0 -123
  83. package/src/components/pages/ProductEdition/sections/EditionTopBar.jsx +0 -134
  84. package/src/components/pages/ProductEdition/sections/ImagePanel.jsx +0 -342
  85. package/src/components/pages/ProductEdition/sections/ServiceTeam.jsx +0 -184
  86. package/src/components/pages/ProductEdition/sections/ServiceTools.jsx +0 -134
  87. package/src/components/pages/ProductEdition/services.js +0 -187
  88. package/src/components/pages/ProductEdition/services.test.js +0 -88
@@ -1,123 +0,0 @@
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
- };
@@ -1,134 +0,0 @@
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
- };
@@ -1,342 +0,0 @@
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
- );