contentoh-components-library 21.6.60 → 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.
- package/dist/components/organisms/ServicesNavigation/ServiceToolbar.js +210 -0
- package/dist/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +138 -0
- package/dist/components/organisms/ServicesNavigation/ServicesRail.js +705 -0
- package/dist/components/organisms/ServicesNavigation/catalog.js +505 -0
- package/dist/components/organisms/ServicesNavigation/index.js +337 -0
- package/dist/components/organisms/ServicesNavigation/model.js +167 -0
- package/dist/components/organisms/ServicesNavigation/ui.js +371 -0
- package/dist/components/organisms/ServicesNavigation/views.js +1237 -0
- package/dist/components/organisms/ServicesWorkspace/AssigneePicker.js +459 -0
- package/dist/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +119 -0
- package/dist/components/organisms/ServicesWorkspace/demo.js +137 -0
- package/dist/components/organisms/ServicesWorkspace/index.js +813 -0
- package/dist/components/organisms/ServicesWorkspace/servicesModel.js +351 -0
- package/dist/components/organisms/ServicesWorkspace/tooltipStyles.js +37 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +40 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +387 -0
- package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +13 -4
- package/dist/components/pages/ProductEdition/ProductEdition.stories.js +148 -0
- package/dist/components/pages/ProductEdition/ProductEditionView.js +1355 -0
- package/dist/components/pages/ProductEdition/ServiceHeader.js +142 -0
- package/dist/components/pages/ProductEdition/api.js +256 -0
- package/dist/components/pages/ProductEdition/index.js +37 -0
- package/dist/components/pages/ProductEdition/permissions.js +200 -0
- package/dist/components/pages/ProductEdition/permissions.test.js +320 -0
- package/dist/components/pages/ProductEdition/sections/CatalogServicePanel.js +237 -0
- package/dist/components/pages/ProductEdition/sections/EditionTopBar.js +174 -0
- package/dist/components/pages/ProductEdition/sections/ImagePanel.js +440 -0
- package/dist/components/pages/ProductEdition/sections/ServiceTeam.js +275 -0
- package/dist/components/pages/ProductEdition/sections/ServiceTools.js +186 -0
- package/dist/components/pages/ProductEdition/services.js +233 -0
- package/dist/components/pages/ProductEdition/services.test.js +195 -0
- package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +15 -2
- package/dist/components/pages/RetailerProductEdition/index.js +57 -40
- package/dist/components/pages/RetailerProductEdition/utils.js +6 -2
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/components/organisms/ServicesNavigation/ServiceToolbar.jsx +143 -0
- package/src/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +101 -0
- package/src/components/organisms/ServicesNavigation/ServicesRail.jsx +591 -0
- package/src/components/organisms/ServicesNavigation/catalog.js +180 -0
- package/src/components/organisms/ServicesNavigation/index.jsx +232 -0
- package/src/components/organisms/ServicesNavigation/model.js +85 -0
- package/src/components/organisms/ServicesNavigation/ui.jsx +229 -0
- package/src/components/organisms/ServicesNavigation/views.jsx +705 -0
- package/src/components/organisms/ServicesWorkspace/AssigneePicker.js +348 -0
- package/src/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +61 -0
- package/src/components/organisms/ServicesWorkspace/demo.js +58 -0
- package/src/components/organisms/ServicesWorkspace/index.js +556 -0
- package/src/components/organisms/ServicesWorkspace/servicesModel.js +203 -0
- package/src/components/organisms/ServicesWorkspace/tooltipStyles.js +27 -0
- package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +17 -0
- package/src/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +175 -0
- package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +8 -4
- package/src/components/pages/ProductEdition/ProductEdition.stories.js +106 -0
- package/src/components/pages/ProductEdition/ProductEditionView.jsx +1142 -0
- package/src/components/pages/ProductEdition/ServiceHeader.jsx +104 -0
- package/src/components/pages/ProductEdition/api.js +84 -0
- package/src/components/pages/ProductEdition/index.js +21 -0
- package/src/components/pages/ProductEdition/permissions.js +128 -0
- package/src/components/pages/ProductEdition/permissions.test.js +249 -0
- package/src/components/pages/ProductEdition/sections/CatalogServicePanel.jsx +123 -0
- package/src/components/pages/ProductEdition/sections/EditionTopBar.jsx +134 -0
- package/src/components/pages/ProductEdition/sections/ImagePanel.jsx +342 -0
- package/src/components/pages/ProductEdition/sections/ServiceTeam.jsx +184 -0
- package/src/components/pages/ProductEdition/sections/ServiceTools.jsx +134 -0
- package/src/components/pages/ProductEdition/services.js +187 -0
- package/src/components/pages/ProductEdition/services.test.js +88 -0
- package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +12 -0
- package/src/components/pages/RetailerProductEdition/index.js +28 -3
- package/src/components/pages/RetailerProductEdition/utils.js +5 -0
- package/src/index.js +1 -0
- package/.claude/settings.local.json +0 -7
- package/dist/ai/prompts/attribute.prompt.js +0 -28
- package/dist/ai/prompts/description.prompt.js +0 -33
- package/dist/ai/schemas/attribute.schema.js +0 -35
- 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
|
+
);
|