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.
- package/.claude/settings.local.json +7 -0
- package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
- package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
- package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
- package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
- package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
- package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
- package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
- package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
- package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +2 -1
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +325 -10
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -2
- package/dist/components/pages/ProviderProductEdition/index.js +486 -231
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
- package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
- package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
- package/src/components/pages/CreatorsProductEdition/index.js +19 -0
- package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
- package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
- package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
- package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
- package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
- package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
- package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
- package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
- package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
- package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
- package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
- package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
- package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
- package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
- package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
- package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
- package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
- package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
- package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +1 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +201 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -0
- package/src/components/pages/ProviderProductEdition/index.js +228 -7
- 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
|
+
};
|