contentoh-components-library 21.6.57 → 21.6.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
  3. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
  4. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
  5. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
  6. package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
  7. package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
  8. package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
  9. package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
  10. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
  11. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
  12. package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
  13. package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
  14. package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +2 -1
  15. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +325 -10
  16. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -2
  17. package/dist/components/pages/ProviderProductEdition/index.js +486 -231
  18. package/dist/index.js +13 -0
  19. package/package.json +1 -1
  20. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
  21. package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
  22. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
  23. package/src/components/pages/CreatorsProductEdition/index.js +19 -0
  24. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
  25. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
  26. package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
  27. package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
  28. package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
  29. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
  30. package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
  31. package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
  32. package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
  33. package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
  34. package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
  35. package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
  36. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
  37. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
  38. package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
  39. package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
  40. package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
  41. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
  42. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
  43. package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
  44. package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
  45. package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +1 -0
  46. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +201 -0
  47. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -0
  48. package/src/components/pages/ProviderProductEdition/index.js +228 -7
  49. package/src/index.js +1 -0
@@ -0,0 +1,305 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import {
3
+ Box,
4
+ IconButton,
5
+ ListItemIcon,
6
+ ListItemText,
7
+ Menu,
8
+ MenuItem,
9
+ Stack,
10
+ Tooltip,
11
+ Typography,
12
+ } from "@mui/material";
13
+ import LocalPoliceIcon from "@mui/icons-material/LocalPolice";
14
+ import LocalPoliceOutlinedIcon from "@mui/icons-material/LocalPoliceOutlined";
15
+
16
+ import acceptIcon from "../../../../assets/images/generalButton/acceptIcon.svg";
17
+ import rejectIcon from "../../../../assets/images/generalButton/rejectIcon.svg";
18
+ import nullIcon from "../../../../assets/images/generalButton/nullIcon.svg";
19
+
20
+ import { RetailerMenu } from "./RetailerMenu";
21
+ import { StatusChip } from "./StatusChip";
22
+ import { COLORS, FONTS } from "./tokens";
23
+
24
+ /*
25
+ Cabecera del producto: nombre, estatus, completitud, fecha, datos (UPC, SKU,
26
+ categoría, proveedor), selector de cadena y botón de evaluación.
27
+
28
+ Reemplaza a `organisms/FullProductNameHeader` (y a sus hijos ProductNameHeader,
29
+ FeaturesBar, AvatarAndValidation, ValidationPanel) con los mismos props y el
30
+ mismo comportamiento, hecho con MUI y responsivo: en pantallas chicas las
31
+ filas se acomodan en varias líneas en vez de salirse.
32
+ */
33
+
34
+ const getTime = (date) => (date ? new Date(date).toLocaleDateString() : "");
35
+
36
+ const FeatureTag = ({ feature, value }) => (
37
+ <Box
38
+ component="span"
39
+ sx={{
40
+ display: "inline-flex",
41
+ alignItems: "baseline",
42
+ gap: "5px",
43
+ maxWidth: "100%",
44
+ minHeight: 26,
45
+ px: "5px",
46
+ py: "4px",
47
+ boxSizing: "border-box",
48
+ bgcolor: COLORS.s2,
49
+ borderRadius: "3px",
50
+ }}
51
+ >
52
+ <Typography
53
+ component="span"
54
+ sx={{
55
+ fontFamily: FONTS.Raleway_500,
56
+ fontSize: 14,
57
+ lineHeight: "18px",
58
+ color: COLORS.original_magenta,
59
+ flexShrink: 0,
60
+ }}
61
+ >
62
+ {feature}
63
+ </Typography>
64
+ <Typography
65
+ component="span"
66
+ sx={{
67
+ fontFamily: FONTS.AvenirNext,
68
+ fontSize: 12,
69
+ lineHeight: "16px",
70
+ letterSpacing: "-0.015em",
71
+ color: COLORS.color_gray,
72
+ minWidth: 0,
73
+ overflowWrap: "anywhere",
74
+ }}
75
+ >
76
+ {value}
77
+ </Typography>
78
+ </Box>
79
+ );
80
+
81
+ const CompletenessBadges = ({ percent }) => {
82
+ const rounded = Math.round(percent ?? 0);
83
+ const complete = rounded >= 100;
84
+ const showPercent = percent || percent === 0;
85
+
86
+ return (
87
+ <>
88
+ {percent ? (
89
+ <Tooltip
90
+ arrow
91
+ title={
92
+ <>
93
+ <b>Campos requeridos</b>
94
+ <br />
95
+ Este nuevo icono te indicará cuando completes todos los campos
96
+ requeridos
97
+ </>
98
+ }
99
+ >
100
+ {complete ? (
101
+ <LocalPoliceIcon sx={{ fontSize: 30, color: COLORS.color_pink, cursor: "pointer" }} />
102
+ ) : (
103
+ <LocalPoliceOutlinedIcon sx={{ fontSize: 30, color: COLORS.s3, cursor: "pointer" }} />
104
+ )}
105
+ </Tooltip>
106
+ ) : null}
107
+ {showPercent && (
108
+ <Tooltip
109
+ arrow
110
+ title={
111
+ <>
112
+ <b>Porcentaje de requeridos</b>
113
+ <br />
114
+ Este porcentaje indica la completitud de los atributos requeridos
115
+ para todos los servicios.
116
+ </>
117
+ }
118
+ >
119
+ <Box
120
+ component="span"
121
+ sx={{
122
+ px: "6px",
123
+ height: 27,
124
+ lineHeight: "27px",
125
+ borderRadius: "15px",
126
+ bgcolor: complete ? COLORS.color_pink : COLORS.s3,
127
+ color: COLORS.white,
128
+ fontFamily: FONTS.AvenirNext,
129
+ fontWeight: 600,
130
+ fontSize: 14,
131
+ }}
132
+ >
133
+ {rounded}%
134
+ </Box>
135
+ </Tooltip>
136
+ )}
137
+ </>
138
+ );
139
+ };
140
+
141
+ const EvaluationMenu = ({ approve, reject, approveAll, rejectAll, showApproveRejectAll }) => {
142
+ const [anchor, setAnchor] = useState(null);
143
+ const close = () => setAnchor(null);
144
+ // Como en ValidationPanel: cada opción dispara su acción y el panel se cierra.
145
+ const option = (icon, label, action) => (
146
+ <MenuItem
147
+ key={label}
148
+ onClick={() => {
149
+ close();
150
+ action && action();
151
+ }}
152
+ >
153
+ <ListItemIcon sx={{ minWidth: 30 }}>
154
+ <img src={icon} alt="" width={20} height={20} />
155
+ </ListItemIcon>
156
+ <ListItemText
157
+ primary={label}
158
+ primaryTypographyProps={{
159
+ sx: { fontFamily: FONTS.AvenirNext, fontSize: 13, color: COLORS.s5, whiteSpace: "normal" },
160
+ }}
161
+ />
162
+ </MenuItem>
163
+ );
164
+
165
+ return (
166
+ <>
167
+ <Tooltip title="Evaluar" arrow>
168
+ <IconButton
169
+ aria-label="Evaluar"
170
+ size="small"
171
+ onClick={(e) => setAnchor(e.currentTarget)}
172
+ sx={{ width: 25, height: 25, p: 0 }}
173
+ >
174
+ <img src={nullIcon} alt="" width={25} height={25} />
175
+ </IconButton>
176
+ </Tooltip>
177
+ <Menu
178
+ anchorEl={anchor}
179
+ open={Boolean(anchor)}
180
+ onClose={close}
181
+ anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
182
+ transformOrigin={{ vertical: "top", horizontal: "right" }}
183
+ PaperProps={{ sx: { mt: 1, maxWidth: "calc(100vw - 24px)" } }}
184
+ >
185
+ {option(acceptIcon, "Validado", approve)}
186
+ {option(rejectIcon, "Rechazado", reject)}
187
+ {showApproveRejectAll && option(acceptIcon, "Validar todo y enviar a evaluación", approveAll)}
188
+ {showApproveRejectAll && option(rejectIcon, "Rechazar todo y enviar a evaluación", rejectAll)}
189
+ </Menu>
190
+ </>
191
+ );
192
+ };
193
+
194
+ export const ProductHeader = ({
195
+ headerData,
196
+ percent,
197
+ activeRetailer,
198
+ setActiveRetailer,
199
+ showValidationButtons,
200
+ approve,
201
+ reject,
202
+ approveAll,
203
+ rejectAll,
204
+ servicesData,
205
+ showApproveRejectAll,
206
+ showStatus = true,
207
+ }) => {
208
+ // Misma fuente de cadenas que FullProductNameHeader: desde tareas llega el
209
+ // orden por cadena; si no, la relación categoría-cadena del artículo.
210
+ const [retailers, setRetailers] = useState([]);
211
+ useEffect(() => {
212
+ const fromTasks = Boolean(headerData.statusByRetailer);
213
+ const categoryRetailer =
214
+ headerData.categoryRetailer || headerData.article?.categoryRetailer;
215
+ setRetailers((fromTasks ? headerData.categoryRetailerInOrder : categoryRetailer) ?? []);
216
+ }, [servicesData]);
217
+
218
+ const retailerIndex = useMemo(() => {
219
+ const source =
220
+ headerData?.article?.categoryRetailer ||
221
+ headerData?.categoryRetailerInOrder ||
222
+ headerData?.categoryRetailer;
223
+ return source?.findIndex((retailer) => retailer?.id_retailer === activeRetailer?.id) ?? -1;
224
+ }, [activeRetailer, headerData]);
225
+
226
+ const status = showStatus
227
+ ? headerData?.version_status || headerData?.status || headerData?.article_status
228
+ : null;
229
+
230
+ const category = (
231
+ headerData?.article?.categoryRetailer?.[retailerIndex]?.category_name ||
232
+ headerData?.categoryRetailerInOrder?.[retailerIndex]?.category ||
233
+ headerData?.categoryRetailer?.[retailerIndex]?.category ||
234
+ headerData.categoryName ||
235
+ ""
236
+ ).replace(/\|/g, " | ");
237
+
238
+ const features = [
239
+ { feature: "UPC", value: headerData?.article?.upc || headerData?.upc },
240
+ { feature: "SKU", value: headerData?.article?.sku || headerData?.sku },
241
+ { feature: "Categoría", value: category },
242
+ { feature: "Prov", value: headerData?.article?.company_name },
243
+ ].filter((item) => item.value);
244
+
245
+ return (
246
+ <Box component="header" sx={{ mb: "17px", minWidth: 0 }}>
247
+ <Stack direction="row" alignItems="center" flexWrap="wrap" columnGap="10px" rowGap={0.5}>
248
+ <Typography
249
+ component="h2"
250
+ sx={{
251
+ fontFamily: FONTS.Raleway,
252
+ fontWeight: 500,
253
+ fontSize: { xs: 20, sm: 24 },
254
+ lineHeight: { xs: "28px", sm: "36px" },
255
+ letterSpacing: "-0.015em",
256
+ color: COLORS.color_gray,
257
+ mt: "8px",
258
+ minWidth: 0,
259
+ overflowWrap: "anywhere",
260
+ }}
261
+ >
262
+ {headerData?.article?.name || headerData?.name}
263
+ </Typography>
264
+ {status && <StatusChip status={status} />}
265
+ <CompletenessBadges percent={percent} />
266
+ <Typography
267
+ component="span"
268
+ sx={{ fontFamily: FONTS.AvenirNext, fontSize: 13, lineHeight: "18px", color: COLORS.s4 }}
269
+ >
270
+ {getTime(headerData?.article?.timestamp || headerData?.timestamp)}
271
+ </Typography>
272
+ </Stack>
273
+
274
+ <Stack
275
+ direction="row"
276
+ alignItems="flex-start"
277
+ justifyContent="space-between"
278
+ gap={1}
279
+ sx={{ mt: 0.5 }}
280
+ >
281
+ <Stack direction="row" flexWrap="wrap" gap="8px" sx={{ minWidth: 0 }}>
282
+ {features.map((item) => (
283
+ <FeatureTag key={item.feature} {...item} />
284
+ ))}
285
+ </Stack>
286
+ <Stack direction="row" alignItems="center" gap={1} sx={{ flexShrink: 0 }}>
287
+ <RetailerMenu
288
+ retailers={retailers}
289
+ activeRetailer={activeRetailer}
290
+ setActiveRetailer={setActiveRetailer}
291
+ />
292
+ {showValidationButtons && (
293
+ <EvaluationMenu
294
+ approve={approve}
295
+ reject={reject}
296
+ approveAll={approveAll}
297
+ rejectAll={rejectAll}
298
+ showApproveRejectAll={showApproveRejectAll}
299
+ />
300
+ )}
301
+ </Stack>
302
+ </Stack>
303
+ </Box>
304
+ );
305
+ };
@@ -0,0 +1,101 @@
1
+ import { useState } from "react";
2
+ import { Box } from "@mui/material";
3
+ import PlayCircleFilledIcon from "@mui/icons-material/PlayCircleFilled";
4
+
5
+ import {
6
+ ASSET_PREVIEW,
7
+ formatDuration,
8
+ resolvePreviewKind,
9
+ } from "../../../../global-files/digital-assets/assetTypes";
10
+ import defaultProductImage from "../../../../assets/images/defaultImages/defaultProductImage.png";
11
+
12
+ /*
13
+ Imagen o video de producto, con MUI. Hace lo mismo que `atoms/ProductImage`:
14
+
15
+ - El tipo se decide por `ext`; la URL no sirve porque apunta al
16
+ redimensionador de CloudFront y no conserva la extensión. El regex sobre la
17
+ URL queda de respaldo, igual que en el original.
18
+ - Una miniatura de video NO lleva controles (no caben y se tragan el clic):
19
+ se muestra el primer fotograma, un botón de play y la duración.
20
+ - Si la imagen no carga, se muestra la imagen por defecto.
21
+ */
22
+
23
+ const isVideo = ({ src, ext }) =>
24
+ ext
25
+ ? resolvePreviewKind({ ext }) === ASSET_PREVIEW.VIDEO
26
+ : /\.mp4($|[?#])/i.test(src || "");
27
+
28
+ export const ProductMedia = ({ src, ext, alt = "", thumbnail = false, sx }) => {
29
+ const [failed, setFailed] = useState(false);
30
+ const [duration, setDuration] = useState(null);
31
+
32
+ const fill = {
33
+ width: "100%",
34
+ height: "100%",
35
+ objectFit: "contain",
36
+ display: "block",
37
+ };
38
+
39
+ if (isVideo({ src, ext })) {
40
+ return (
41
+ <Box sx={{ position: "relative", width: "100%", height: "100%", ...sx }}>
42
+ <Box
43
+ component="video"
44
+ src={thumbnail ? `${src}#t=0.5` : src}
45
+ controls={!thumbnail}
46
+ muted={thumbnail}
47
+ playsInline
48
+ preload="metadata"
49
+ onLoadedMetadata={(e) => setDuration(e.target.duration)}
50
+ sx={{ ...fill, bgcolor: "#000", pointerEvents: thumbnail ? "none" : "auto" }}
51
+ />
52
+ {thumbnail && (
53
+ <>
54
+ <PlayCircleFilledIcon
55
+ aria-hidden
56
+ sx={{
57
+ position: "absolute",
58
+ top: "50%",
59
+ left: "50%",
60
+ transform: "translate(-50%, -50%)",
61
+ fontSize: 24,
62
+ color: "rgba(255,255,255,0.9)",
63
+ filter: "drop-shadow(0 0 2px rgba(0,0,0,0.6))",
64
+ pointerEvents: "none",
65
+ }}
66
+ />
67
+ {duration ? (
68
+ <Box
69
+ component="span"
70
+ sx={{
71
+ position: "absolute",
72
+ right: 3,
73
+ bottom: 3,
74
+ px: "4px",
75
+ borderRadius: "3px",
76
+ bgcolor: "rgba(0,0,0,0.75)",
77
+ color: "#fff",
78
+ fontSize: 9,
79
+ lineHeight: "13px",
80
+ pointerEvents: "none",
81
+ }}
82
+ >
83
+ {formatDuration(duration)}
84
+ </Box>
85
+ ) : null}
86
+ </>
87
+ )}
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Box
94
+ component="img"
95
+ src={failed || !src ? defaultProductImage : src}
96
+ alt={alt}
97
+ onError={() => setFailed(true)}
98
+ sx={{ ...fill, ...sx }}
99
+ />
100
+ );
101
+ };
@@ -0,0 +1,125 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogActions,
6
+ DialogContent,
7
+ DialogTitle,
8
+ TextField,
9
+ useMediaQuery,
10
+ } from "@mui/material";
11
+
12
+ import { COLORS, FONTS } from "./tokens";
13
+
14
+ /*
15
+ Mensaje de rechazo antes de enviar la evaluación.
16
+
17
+ Reemplaza a `organisms/Modal` + `TagAndInput` (Quill). Hace lo mismo que el
18
+ original —guarda el mensaje con `createComment` y después rechaza uno o todos
19
+ los servicios—, pero con un campo controlado: el original leía el DOM de
20
+ Quill y, si el mensaje estaba vacío, metía a mano un <div> de error.
21
+
22
+ En celular ocupa la pantalla completa, para que el teclado no tape el campo.
23
+ */
24
+
25
+ const MAX_CHARS = 999;
26
+
27
+ export const RejectDialog = ({ open, rejectAll, onCancel, onConfirm }) => {
28
+ const [text, setText] = useState("");
29
+ const [showError, setShowError] = useState(false);
30
+ const [sending, setSending] = useState(false);
31
+ const isPhone = useMediaQuery("(max-width:599.95px)");
32
+
33
+ // Cada vez que se abre empieza vacío, como el original (se montaba de nuevo).
34
+ useEffect(() => {
35
+ if (open) {
36
+ setText("");
37
+ setShowError(false);
38
+ setSending(false);
39
+ }
40
+ }, [open]);
41
+
42
+ const confirm = async () => {
43
+ const message = text.trim();
44
+ if (!message) {
45
+ setShowError(true);
46
+ return;
47
+ }
48
+ setSending(true);
49
+ try {
50
+ await onConfirm(message);
51
+ } finally {
52
+ setSending(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <Dialog
58
+ open={open}
59
+ onClose={sending ? undefined : onCancel}
60
+ fullScreen={isPhone}
61
+ fullWidth
62
+ maxWidth="sm"
63
+ PaperProps={{ sx: { borderRadius: isPhone ? 0 : "10px" } }}
64
+ >
65
+ <DialogTitle
66
+ sx={{ fontFamily: FONTS.Raleway_600, fontSize: 17, textAlign: "center", color: COLORS.s5 }}
67
+ >
68
+ {rejectAll ? "Agregar mensaje para rechazar todo" : "Agregar mensaje de rechazo"}
69
+ </DialogTitle>
70
+ <DialogContent>
71
+ <TextField
72
+ id="modal-message-box"
73
+ autoFocus
74
+ multiline
75
+ minRows={5}
76
+ maxRows={isPhone ? 14 : 10}
77
+ fullWidth
78
+ placeholder="Escribe el motivo del rechazo"
79
+ value={text}
80
+ onChange={(e) => {
81
+ setText(e.target.value.slice(0, MAX_CHARS));
82
+ if (showError) setShowError(false);
83
+ }}
84
+ error={showError}
85
+ helperText={showError ? "El mensaje no puede estar vacío." : `${text.length}/${MAX_CHARS}`}
86
+ FormHelperTextProps={{ sx: { textAlign: showError ? "left" : "right", mx: 0 } }}
87
+ InputProps={{ sx: { fontFamily: FONTS.AvenirNext, fontSize: 14 } }}
88
+ sx={{ mt: 1 }}
89
+ />
90
+ </DialogContent>
91
+ <DialogActions sx={{ px: 3, pb: 2.5, gap: 1, justifyContent: "center" }}>
92
+ <Button
93
+ variant="outlined"
94
+ disabled={sending}
95
+ onClick={onCancel}
96
+ sx={{
97
+ textTransform: "none",
98
+ borderRadius: "20px",
99
+ minWidth: 110,
100
+ color: COLORS.s5,
101
+ borderColor: COLORS.s3,
102
+ "&:hover": { borderColor: COLORS.s5 },
103
+ }}
104
+ >
105
+ Cancelar
106
+ </Button>
107
+ <Button
108
+ variant="contained"
109
+ disableElevation
110
+ disabled={sending}
111
+ onClick={confirm}
112
+ sx={{
113
+ textTransform: "none",
114
+ borderRadius: "20px",
115
+ minWidth: 110,
116
+ bgcolor: COLORS.magenta_s2,
117
+ "&:hover": { bgcolor: COLORS.secondary_magenta },
118
+ }}
119
+ >
120
+ {sending ? "Enviando…" : "Aceptar"}
121
+ </Button>
122
+ </DialogActions>
123
+ </Dialog>
124
+ );
125
+ };
@@ -0,0 +1,129 @@
1
+ import { useState } from "react";
2
+ import {
3
+ Avatar,
4
+ Box,
5
+ ButtonBase,
6
+ ListItemAvatar,
7
+ ListItemText,
8
+ Menu,
9
+ MenuItem,
10
+ } from "@mui/material";
11
+ import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
12
+ import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
13
+
14
+ import { getNewStatus } from "../../../../global-files/data";
15
+ import { StatusChip } from "./StatusChip";
16
+ import { COLORS, FONTS, retailerId, retailerImage } from "./tokens";
17
+
18
+ /**
19
+ * Selector de la cadena activa. Reemplaza a `molecules/RetailerSelector` con el
20
+ * mismo comportamiento:
21
+ * - el logo de la cadena activa abre la lista;
22
+ * - la activa se marca en rosa suave y con el círculo verde;
23
+ * - una cadena cuyo estatus de servicios es "NA" no se puede elegir.
24
+ */
25
+ export const RetailerMenu = ({ retailers = [], activeRetailer, setActiveRetailer }) => {
26
+ const [anchor, setAnchor] = useState(null);
27
+ const activeId = retailerId(activeRetailer);
28
+
29
+ return (
30
+ <>
31
+ <ButtonBase
32
+ aria-label="Cambiar de cadena"
33
+ aria-haspopup="menu"
34
+ aria-expanded={Boolean(anchor)}
35
+ onClick={(e) => retailers?.length > 0 && setAnchor(e.currentTarget)}
36
+ sx={{ borderRadius: "14px", pr: 0.25 }}
37
+ >
38
+ <Avatar
39
+ src={retailerImage(activeRetailer)}
40
+ alt={activeRetailer?.name}
41
+ sx={{ width: 26, height: 26, bgcolor: COLORS.s2 }}
42
+ />
43
+ {anchor ? (
44
+ <ArrowDropUpIcon sx={{ color: COLORS.s4 }} />
45
+ ) : (
46
+ <ArrowDropDownIcon sx={{ color: COLORS.s4 }} />
47
+ )}
48
+ </ButtonBase>
49
+
50
+ <Menu
51
+ anchorEl={anchor}
52
+ open={Boolean(anchor)}
53
+ onClose={() => setAnchor(null)}
54
+ anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
55
+ transformOrigin={{ vertical: "top", horizontal: "right" }}
56
+ PaperProps={{
57
+ sx: {
58
+ mt: 1,
59
+ bgcolor: COLORS.s2,
60
+ border: `1px solid ${COLORS.s3}`,
61
+ maxWidth: "calc(100vw - 24px)",
62
+ },
63
+ }}
64
+ >
65
+ {retailers.map((retailer, index) => {
66
+ const isActive =
67
+ retailerId(retailer) != null && retailerId(retailer) === activeId;
68
+ const status = retailer?.services ? getNewStatus(retailer.services) : null;
69
+ return (
70
+ <MenuItem
71
+ key={`${index}-${retailer?.name}`}
72
+ selected={isActive}
73
+ onClick={() => {
74
+ if (status === "NA") return;
75
+ setActiveRetailer(retailer);
76
+ setAnchor(null);
77
+ }}
78
+ sx={{
79
+ gap: 1.25,
80
+ borderRadius: "3px",
81
+ mx: 0.75,
82
+ "&.Mui-selected, &.Mui-selected:hover": {
83
+ backgroundColor: `rgba(${COLORS.rgb_color_pink}, 0.1)`,
84
+ },
85
+ "&.Mui-selected:hover": {
86
+ backgroundColor: `rgba(${COLORS.rgb_color_pink}, 0.18)`,
87
+ },
88
+ }}
89
+ >
90
+ <ListItemAvatar sx={{ minWidth: 0 }}>
91
+ <Avatar
92
+ src={retailerImage(retailer)}
93
+ alt={retailer?.name}
94
+ sx={{ width: 26, height: 26, bgcolor: COLORS.white }}
95
+ />
96
+ </ListItemAvatar>
97
+ <ListItemText
98
+ primary={retailer?.name}
99
+ secondary={retailer?.team}
100
+ primaryTypographyProps={{
101
+ sx: {
102
+ fontFamily: FONTS.AvenirNext,
103
+ fontWeight: 500,
104
+ fontSize: 14,
105
+ color: COLORS.s5,
106
+ whiteSpace: "normal",
107
+ },
108
+ }}
109
+ />
110
+ {status && <StatusChip status={status} />}
111
+ {isActive && (
112
+ <Box
113
+ aria-label="Cadena activa"
114
+ sx={{
115
+ width: 20,
116
+ height: 20,
117
+ flexShrink: 0,
118
+ borderRadius: "50%",
119
+ bgcolor: COLORS.exported,
120
+ }}
121
+ />
122
+ )}
123
+ </MenuItem>
124
+ );
125
+ })}
126
+ </Menu>
127
+ </>
128
+ );
129
+ };