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,591 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
ButtonBase,
|
|
5
|
+
Collapse,
|
|
6
|
+
IconButton,
|
|
7
|
+
InputAdornment,
|
|
8
|
+
TextField,
|
|
9
|
+
Tooltip,
|
|
10
|
+
Typography,
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
13
|
+
import SearchIcon from "@mui/icons-material/Search";
|
|
14
|
+
import MenuOpenIcon from "@mui/icons-material/MenuOpen";
|
|
15
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
COLORS,
|
|
19
|
+
FONT_STACK,
|
|
20
|
+
TITLE_STACK,
|
|
21
|
+
PendingDot,
|
|
22
|
+
ServiceIcon,
|
|
23
|
+
StatusChip,
|
|
24
|
+
serviceStatusProps,
|
|
25
|
+
fieldSx,
|
|
26
|
+
statusColor,
|
|
27
|
+
serviceStatusColor,
|
|
28
|
+
statusName,
|
|
29
|
+
} from "./ui";
|
|
30
|
+
import { SUMMARY, groupServices, isMine, isPendingFor } from "./model";
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
Lista lateral de servicios: reemplaza a la barra de pestañas.
|
|
34
|
+
|
|
35
|
+
- Solo muestra los servicios que el producto tiene contratados.
|
|
36
|
+
- «Resumen» va primero: el panorama de todos los servicios.
|
|
37
|
+
- Los servicios van en bloques fijos (Textos, Imágenes...). No se reordenan
|
|
38
|
+
por estatus: cada uno queda siempre en el mismo lugar.
|
|
39
|
+
- «Míos / Todos»: por omisión, solo lo que la persona trabaja. Aparece solo
|
|
40
|
+
cuando sirve (trabaja algunos servicios del producto, no todos).
|
|
41
|
+
- El buscador aparece cuando hay más de 8 servicios a la vista.
|
|
42
|
+
- Se contrae a una columna de íconos para dejarle el ancho al editor.
|
|
43
|
+
- `side`: de qué lado de la pantalla va («left» o «right»). Del lado derecho
|
|
44
|
+
es el espejo del izquierdo: borde, marca del servicio abierto, flechas de
|
|
45
|
+
contraer y globos de ayuda cambian de lado.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export const RAIL_WIDTH = 256;
|
|
49
|
+
export const RAIL_COLLAPSED_WIDTH = 64;
|
|
50
|
+
const SEARCH_THRESHOLD = 8;
|
|
51
|
+
const EXPAND_ALL_THRESHOLD = 12;
|
|
52
|
+
|
|
53
|
+
const CountBadge = ({ children, active = false }) => (
|
|
54
|
+
<Box
|
|
55
|
+
component="span"
|
|
56
|
+
sx={{
|
|
57
|
+
minWidth: 20,
|
|
58
|
+
height: 18,
|
|
59
|
+
px: 0.75,
|
|
60
|
+
borderRadius: "9px",
|
|
61
|
+
display: "inline-flex",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
justifyContent: "center",
|
|
64
|
+
fontFamily: FONT_STACK,
|
|
65
|
+
fontSize: 11,
|
|
66
|
+
fontWeight: 700,
|
|
67
|
+
bgcolor: active ? COLORS.s5 : COLORS.s2,
|
|
68
|
+
color: active ? COLORS.white : COLORS.s4,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const FilterToggle = ({ value, onChange, mineCount, allCount }) => {
|
|
76
|
+
const options = [
|
|
77
|
+
{ key: "mine", label: "Míos", count: mineCount },
|
|
78
|
+
{ key: "all", label: "Todos", count: allCount },
|
|
79
|
+
];
|
|
80
|
+
return (
|
|
81
|
+
<Box
|
|
82
|
+
role="group"
|
|
83
|
+
aria-label="Qué servicios ver"
|
|
84
|
+
sx={{ display: "flex", p: "3px", mx: 2, mb: 1.5, bgcolor: COLORS.s2, borderRadius: "18px" }}
|
|
85
|
+
>
|
|
86
|
+
{options.map((option) => {
|
|
87
|
+
const active = option.key === value;
|
|
88
|
+
return (
|
|
89
|
+
<ButtonBase
|
|
90
|
+
key={option.key}
|
|
91
|
+
aria-pressed={active}
|
|
92
|
+
onClick={() => onChange(option.key)}
|
|
93
|
+
sx={{
|
|
94
|
+
flex: 1,
|
|
95
|
+
height: 28,
|
|
96
|
+
gap: 0.75,
|
|
97
|
+
borderRadius: "15px",
|
|
98
|
+
fontFamily: FONT_STACK,
|
|
99
|
+
fontSize: 13,
|
|
100
|
+
fontWeight: active ? 700 : 400,
|
|
101
|
+
color: active ? COLORS.s5 : COLORS.s4,
|
|
102
|
+
bgcolor: active ? COLORS.white : "transparent",
|
|
103
|
+
boxShadow: active ? "0 1px 3px rgba(80, 61, 102, 0.18)" : "none",
|
|
104
|
+
transition: "background-color 120ms, box-shadow 120ms",
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{option.label}
|
|
108
|
+
<CountBadge active={active}>{option.count}</CountBadge>
|
|
109
|
+
</ButtonBase>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</Box>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const itemSx = (active, side = "left") => ({
|
|
117
|
+
width: "100%",
|
|
118
|
+
display: "flex",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
gap: 1.25,
|
|
121
|
+
height: 36,
|
|
122
|
+
pl: 1.5,
|
|
123
|
+
pr: 1,
|
|
124
|
+
borderRadius: "8px",
|
|
125
|
+
position: "relative",
|
|
126
|
+
textAlign: "left",
|
|
127
|
+
bgcolor: active ? COLORS.s2 : "transparent",
|
|
128
|
+
"&:hover": { bgcolor: COLORS.s2 },
|
|
129
|
+
"&::before": {
|
|
130
|
+
content: '""',
|
|
131
|
+
position: "absolute",
|
|
132
|
+
[side]: 0,
|
|
133
|
+
top: 8,
|
|
134
|
+
bottom: 8,
|
|
135
|
+
width: 3,
|
|
136
|
+
borderRadius: "2px",
|
|
137
|
+
bgcolor: active ? COLORS.magenta_s2 : "transparent",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const ItemLabel = ({ active, children }) => (
|
|
142
|
+
<Typography
|
|
143
|
+
sx={{
|
|
144
|
+
flex: 1,
|
|
145
|
+
minWidth: 0,
|
|
146
|
+
fontFamily: FONT_STACK,
|
|
147
|
+
fontSize: 14,
|
|
148
|
+
fontWeight: active ? 700 : 400,
|
|
149
|
+
color: active ? COLORS.s5 : COLORS.color_gray,
|
|
150
|
+
overflow: "hidden",
|
|
151
|
+
textOverflow: "ellipsis",
|
|
152
|
+
whiteSpace: "nowrap",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</Typography>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
/* ------------------------------------------------------------------------ */
|
|
160
|
+
/* Lista completa */
|
|
161
|
+
/* ------------------------------------------------------------------------ */
|
|
162
|
+
|
|
163
|
+
const ExpandedRail = ({
|
|
164
|
+
services,
|
|
165
|
+
visible,
|
|
166
|
+
selected,
|
|
167
|
+
onSelect,
|
|
168
|
+
userId,
|
|
169
|
+
filter,
|
|
170
|
+
onFilterChange,
|
|
171
|
+
showFilter,
|
|
172
|
+
onCollapse,
|
|
173
|
+
onClose,
|
|
174
|
+
side,
|
|
175
|
+
}) => {
|
|
176
|
+
const [query, setQuery] = useState("");
|
|
177
|
+
const text = query.trim().toLowerCase();
|
|
178
|
+
const matches = text
|
|
179
|
+
? visible.filter((service) => service.display_name.toLowerCase().includes(text))
|
|
180
|
+
: visible;
|
|
181
|
+
const groups = groupServices(matches);
|
|
182
|
+
const pendingCount = services.filter((service) => isPendingFor(service, userId)).length;
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
Bloques abiertos: todos si caben; si no, el del servicio abierto y los que
|
|
186
|
+
tienen algo que le toca a la persona. Al buscar se abren todos.
|
|
187
|
+
*/
|
|
188
|
+
// `visible` es un arreglo nuevo en cada render: se compara por sus nombres.
|
|
189
|
+
const visibleKey = visible.map((service) => service.service_name).join(",");
|
|
190
|
+
const defaultOpen = useMemo(() => {
|
|
191
|
+
const all = groupServices(visible);
|
|
192
|
+
if (visible.length <= EXPAND_ALL_THRESHOLD) return all.map((group) => group.key);
|
|
193
|
+
return all
|
|
194
|
+
.filter((group) =>
|
|
195
|
+
group.services.some(
|
|
196
|
+
(service) => service.service_name === selected || isPendingFor(service, userId)
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
.map((group) => group.key);
|
|
200
|
+
// Solo al cambiar lo que se ve: abrir un servicio no vuelve a cerrar bloques.
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
}, [visibleKey, userId]);
|
|
203
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
204
|
+
useEffect(() => setOpen(defaultOpen), [defaultOpen]);
|
|
205
|
+
|
|
206
|
+
// Si se abre un servicio de un bloque cerrado (desde el Resumen), se abre.
|
|
207
|
+
const selectedGroup = services.find((service) => service.service_name === selected)?.group;
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (selectedGroup) setOpen((prev) => (prev.includes(selectedGroup) ? prev : [...prev, selectedGroup]));
|
|
210
|
+
}, [selectedGroup]);
|
|
211
|
+
|
|
212
|
+
const toggle = (key) =>
|
|
213
|
+
setOpen((prev) => (prev.includes(key) ? prev.filter((item) => item !== key) : [...prev, key]));
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<>
|
|
217
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 2, pt: 2, pb: 1.5 }}>
|
|
218
|
+
<Typography sx={{ flex: 1, fontFamily: TITLE_STACK, fontWeight: 600, fontSize: 16, color: COLORS.s5 }}>
|
|
219
|
+
Servicios
|
|
220
|
+
</Typography>
|
|
221
|
+
<CountBadge>{services.length}</CountBadge>
|
|
222
|
+
{onCollapse && (
|
|
223
|
+
<Tooltip title="Contraer la lista" arrow>
|
|
224
|
+
<IconButton aria-label="Contraer la lista de servicios" size="small" onClick={onCollapse}>
|
|
225
|
+
{/* La flecha apunta hacia el borde al que se contrae. */}
|
|
226
|
+
<MenuOpenIcon
|
|
227
|
+
sx={{ fontSize: 20, color: COLORS.s4, transform: side === "right" ? "scaleX(-1)" : "none" }}
|
|
228
|
+
/>
|
|
229
|
+
</IconButton>
|
|
230
|
+
</Tooltip>
|
|
231
|
+
)}
|
|
232
|
+
{onClose && (
|
|
233
|
+
<IconButton aria-label="Cerrar la lista de servicios" size="small" onClick={onClose}>
|
|
234
|
+
<CloseIcon sx={{ fontSize: 20, color: COLORS.s4 }} />
|
|
235
|
+
</IconButton>
|
|
236
|
+
)}
|
|
237
|
+
</Box>
|
|
238
|
+
|
|
239
|
+
{showFilter && (
|
|
240
|
+
<FilterToggle
|
|
241
|
+
value={filter}
|
|
242
|
+
onChange={onFilterChange}
|
|
243
|
+
mineCount={services.filter((service) => isMine(service, userId)).length}
|
|
244
|
+
allCount={services.length}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{visible.length > SEARCH_THRESHOLD && (
|
|
249
|
+
<Box sx={{ px: 2, mb: 1 }}>
|
|
250
|
+
<TextField
|
|
251
|
+
size="small"
|
|
252
|
+
fullWidth
|
|
253
|
+
placeholder="Buscar servicio…"
|
|
254
|
+
value={query}
|
|
255
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
256
|
+
sx={fieldSx}
|
|
257
|
+
InputProps={{
|
|
258
|
+
startAdornment: (
|
|
259
|
+
<InputAdornment position="start">
|
|
260
|
+
<SearchIcon sx={{ fontSize: 18, color: COLORS.s4 }} />
|
|
261
|
+
</InputAdornment>
|
|
262
|
+
),
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
</Box>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
<Box sx={{ flex: 1, minHeight: 0, overflowY: "auto", px: 1, pb: 1 }}>
|
|
269
|
+
<ButtonBase
|
|
270
|
+
onClick={() => onSelect(SUMMARY)}
|
|
271
|
+
aria-current={selected === SUMMARY ? "true" : undefined}
|
|
272
|
+
sx={{ ...itemSx(selected === SUMMARY, side), mb: 0.5 }}
|
|
273
|
+
>
|
|
274
|
+
<ServiceIcon
|
|
275
|
+
icon="summary"
|
|
276
|
+
sx={{ color: selected === SUMMARY ? COLORS.magenta_s2 : COLORS.s4 }}
|
|
277
|
+
/>
|
|
278
|
+
<ItemLabel active={selected === SUMMARY}>Resumen</ItemLabel>
|
|
279
|
+
{pendingCount > 0 && <CountBadge>{pendingCount}</CountBadge>}
|
|
280
|
+
</ButtonBase>
|
|
281
|
+
|
|
282
|
+
{groups.length === 0 && (
|
|
283
|
+
<Typography sx={{ py: 3, textAlign: "center", fontFamily: FONT_STACK, fontSize: 13, color: COLORS.s4 }}>
|
|
284
|
+
Sin coincidencias
|
|
285
|
+
</Typography>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{groups.map((group) => {
|
|
289
|
+
const isOpen = Boolean(text) || open.includes(group.key);
|
|
290
|
+
return (
|
|
291
|
+
<Box key={group.key} sx={{ mt: 0.5 }}>
|
|
292
|
+
<ButtonBase
|
|
293
|
+
onClick={() => toggle(group.key)}
|
|
294
|
+
aria-expanded={isOpen}
|
|
295
|
+
sx={{
|
|
296
|
+
width: "100%",
|
|
297
|
+
display: "flex",
|
|
298
|
+
alignItems: "center",
|
|
299
|
+
gap: 1,
|
|
300
|
+
px: 1.5,
|
|
301
|
+
py: 0.75,
|
|
302
|
+
borderRadius: "6px",
|
|
303
|
+
"&:hover": { bgcolor: COLORS.s1 },
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
<Typography
|
|
307
|
+
sx={{
|
|
308
|
+
flex: 1,
|
|
309
|
+
textAlign: "left",
|
|
310
|
+
fontFamily: FONT_STACK,
|
|
311
|
+
fontSize: 11,
|
|
312
|
+
fontWeight: 700,
|
|
313
|
+
letterSpacing: "0.06em",
|
|
314
|
+
textTransform: "uppercase",
|
|
315
|
+
color: COLORS.s4,
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
{group.label}
|
|
319
|
+
</Typography>
|
|
320
|
+
{group.services.some((service) => isPendingFor(service, userId)) && !isOpen && (
|
|
321
|
+
<PendingDot size={6} withTooltip={false} />
|
|
322
|
+
)}
|
|
323
|
+
<Typography sx={{ fontFamily: FONT_STACK, fontSize: 11, fontWeight: 700, color: COLORS.s4 }}>
|
|
324
|
+
{group.services.length}
|
|
325
|
+
</Typography>
|
|
326
|
+
<ExpandMoreIcon
|
|
327
|
+
sx={{
|
|
328
|
+
fontSize: 18,
|
|
329
|
+
color: COLORS.s4,
|
|
330
|
+
transition: "transform 150ms",
|
|
331
|
+
transform: isOpen ? "rotate(180deg)" : "none",
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
</ButtonBase>
|
|
335
|
+
<Collapse in={isOpen}>
|
|
336
|
+
{group.services.map((service) => {
|
|
337
|
+
const active = service.service_name === selected;
|
|
338
|
+
return (
|
|
339
|
+
<ButtonBase
|
|
340
|
+
key={service.service_name}
|
|
341
|
+
onClick={() => onSelect(service.service_name)}
|
|
342
|
+
aria-current={active ? "true" : undefined}
|
|
343
|
+
sx={itemSx(active, side)}
|
|
344
|
+
>
|
|
345
|
+
<ServiceIcon
|
|
346
|
+
icon={service.icon}
|
|
347
|
+
sx={{ color: active ? COLORS.magenta_s2 : COLORS.s4 }}
|
|
348
|
+
/>
|
|
349
|
+
<ItemLabel active={active}>{service.display_name}</ItemLabel>
|
|
350
|
+
{isPendingFor(service, userId) && <PendingDot />}
|
|
351
|
+
<StatusChip {...serviceStatusProps(service)} oval />
|
|
352
|
+
</ButtonBase>
|
|
353
|
+
);
|
|
354
|
+
})}
|
|
355
|
+
</Collapse>
|
|
356
|
+
</Box>
|
|
357
|
+
);
|
|
358
|
+
})}
|
|
359
|
+
</Box>
|
|
360
|
+
|
|
361
|
+
{pendingCount > 0 && (
|
|
362
|
+
<Box
|
|
363
|
+
sx={{
|
|
364
|
+
display: "flex",
|
|
365
|
+
alignItems: "center",
|
|
366
|
+
gap: 1,
|
|
367
|
+
px: 2,
|
|
368
|
+
py: 1.25,
|
|
369
|
+
borderTop: `1px solid ${COLORS.s2}`,
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
<PendingDot withTooltip={false} />
|
|
373
|
+
<Typography sx={{ fontFamily: FONT_STACK, fontSize: 12, color: COLORS.s4 }}>
|
|
374
|
+
Te toca a ti · {pendingCount} {pendingCount === 1 ? "servicio" : "servicios"}
|
|
375
|
+
</Typography>
|
|
376
|
+
</Box>
|
|
377
|
+
)}
|
|
378
|
+
</>
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/* ------------------------------------------------------------------------ */
|
|
383
|
+
/* Lista contraída: solo íconos */
|
|
384
|
+
/* ------------------------------------------------------------------------ */
|
|
385
|
+
|
|
386
|
+
const CollapsedRail = ({ visible, selected, onSelect, userId, onExpand, side }) => {
|
|
387
|
+
const groups = groupServices(visible);
|
|
388
|
+
// Los globos de ayuda salen hacia el editor, no hacia el borde de la pantalla.
|
|
389
|
+
const tooltipPlacement = side === "right" ? "left" : "right";
|
|
390
|
+
const iconButtonSx = (active) => ({
|
|
391
|
+
width: 44,
|
|
392
|
+
height: 40,
|
|
393
|
+
borderRadius: "8px",
|
|
394
|
+
position: "relative",
|
|
395
|
+
bgcolor: active ? COLORS.s2 : "transparent",
|
|
396
|
+
"&:hover": { bgcolor: COLORS.s2 },
|
|
397
|
+
"&::before": {
|
|
398
|
+
content: '""',
|
|
399
|
+
position: "absolute",
|
|
400
|
+
[side]: -10,
|
|
401
|
+
top: 8,
|
|
402
|
+
bottom: 8,
|
|
403
|
+
width: 3,
|
|
404
|
+
borderRadius: "2px",
|
|
405
|
+
bgcolor: active ? COLORS.magenta_s2 : "transparent",
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<>
|
|
411
|
+
<Box sx={{ display: "flex", justifyContent: "center", pt: 1.5, pb: 1 }}>
|
|
412
|
+
<Tooltip title="Mostrar la lista de servicios" arrow placement={tooltipPlacement}>
|
|
413
|
+
<IconButton aria-label="Mostrar la lista de servicios" onClick={onExpand}>
|
|
414
|
+
<MenuOpenIcon
|
|
415
|
+
sx={{ fontSize: 22, color: COLORS.s4, transform: side === "right" ? "none" : "scaleX(-1)" }}
|
|
416
|
+
/>
|
|
417
|
+
</IconButton>
|
|
418
|
+
</Tooltip>
|
|
419
|
+
</Box>
|
|
420
|
+
<Box
|
|
421
|
+
sx={{
|
|
422
|
+
flex: 1,
|
|
423
|
+
minHeight: 0,
|
|
424
|
+
overflowY: "auto",
|
|
425
|
+
display: "flex",
|
|
426
|
+
flexDirection: "column",
|
|
427
|
+
alignItems: "center",
|
|
428
|
+
gap: 0.5,
|
|
429
|
+
pb: 1,
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
<Tooltip title="Resumen" arrow placement={tooltipPlacement}>
|
|
433
|
+
<ButtonBase
|
|
434
|
+
aria-label="Resumen"
|
|
435
|
+
aria-current={selected === SUMMARY ? "true" : undefined}
|
|
436
|
+
onClick={() => onSelect(SUMMARY)}
|
|
437
|
+
sx={iconButtonSx(selected === SUMMARY)}
|
|
438
|
+
>
|
|
439
|
+
<ServiceIcon
|
|
440
|
+
icon="summary"
|
|
441
|
+
sx={{ fontSize: 22, color: selected === SUMMARY ? COLORS.magenta_s2 : COLORS.s4 }}
|
|
442
|
+
/>
|
|
443
|
+
</ButtonBase>
|
|
444
|
+
</Tooltip>
|
|
445
|
+
{groups.map((group) => (
|
|
446
|
+
<Box
|
|
447
|
+
key={group.key}
|
|
448
|
+
sx={{
|
|
449
|
+
display: "flex",
|
|
450
|
+
flexDirection: "column",
|
|
451
|
+
alignItems: "center",
|
|
452
|
+
gap: 0.5,
|
|
453
|
+
pt: 0.75,
|
|
454
|
+
mt: 0.25,
|
|
455
|
+
borderTop: `1px solid ${COLORS.s2}`,
|
|
456
|
+
width: 40,
|
|
457
|
+
}}
|
|
458
|
+
>
|
|
459
|
+
{group.services.map((service) => {
|
|
460
|
+
const active = service.service_name === selected;
|
|
461
|
+
const pending = isPendingFor(service, userId);
|
|
462
|
+
const label = [
|
|
463
|
+
service.display_name,
|
|
464
|
+
statusName(service.status) ?? service.status,
|
|
465
|
+
pending ? "Te toca a ti" : null,
|
|
466
|
+
]
|
|
467
|
+
.filter(Boolean)
|
|
468
|
+
.join(" · ");
|
|
469
|
+
return (
|
|
470
|
+
<Tooltip key={service.service_name} title={label} arrow placement={tooltipPlacement}>
|
|
471
|
+
<ButtonBase
|
|
472
|
+
aria-label={label}
|
|
473
|
+
aria-current={active ? "true" : undefined}
|
|
474
|
+
onClick={() => onSelect(service.service_name)}
|
|
475
|
+
sx={iconButtonSx(active)}
|
|
476
|
+
>
|
|
477
|
+
<ServiceIcon
|
|
478
|
+
icon={service.icon}
|
|
479
|
+
sx={{ fontSize: 22, color: active ? COLORS.magenta_s2 : COLORS.s4 }}
|
|
480
|
+
/>
|
|
481
|
+
{/* Estatus: punto con el color de la etiqueta. */}
|
|
482
|
+
<Box
|
|
483
|
+
component="span"
|
|
484
|
+
sx={{
|
|
485
|
+
position: "absolute",
|
|
486
|
+
top: 5,
|
|
487
|
+
right: 6,
|
|
488
|
+
width: 9,
|
|
489
|
+
height: 9,
|
|
490
|
+
borderRadius: "50%",
|
|
491
|
+
bgcolor: serviceStatusColor(service),
|
|
492
|
+
border: `2px solid ${active ? COLORS.s2 : COLORS.white}`,
|
|
493
|
+
}}
|
|
494
|
+
/>
|
|
495
|
+
{pending && (
|
|
496
|
+
<Box
|
|
497
|
+
component="span"
|
|
498
|
+
sx={{
|
|
499
|
+
position: "absolute",
|
|
500
|
+
bottom: 4,
|
|
501
|
+
left: "50%",
|
|
502
|
+
transform: "translateX(-50%)",
|
|
503
|
+
width: 14,
|
|
504
|
+
height: 3,
|
|
505
|
+
borderRadius: "2px",
|
|
506
|
+
bgcolor: COLORS.magenta_s2,
|
|
507
|
+
}}
|
|
508
|
+
/>
|
|
509
|
+
)}
|
|
510
|
+
</ButtonBase>
|
|
511
|
+
</Tooltip>
|
|
512
|
+
);
|
|
513
|
+
})}
|
|
514
|
+
</Box>
|
|
515
|
+
))}
|
|
516
|
+
</Box>
|
|
517
|
+
</>
|
|
518
|
+
);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
/* ------------------------------------------------------------------------ */
|
|
522
|
+
|
|
523
|
+
export const ServicesRail = ({
|
|
524
|
+
services,
|
|
525
|
+
selected,
|
|
526
|
+
onSelect,
|
|
527
|
+
userId,
|
|
528
|
+
filter,
|
|
529
|
+
onFilterChange,
|
|
530
|
+
showFilter,
|
|
531
|
+
collapsed = false,
|
|
532
|
+
onToggleCollapsed,
|
|
533
|
+
onClose,
|
|
534
|
+
variant = "rail",
|
|
535
|
+
side = "left",
|
|
536
|
+
}) => {
|
|
537
|
+
const visible =
|
|
538
|
+
showFilter && filter === "mine"
|
|
539
|
+
? services.filter(
|
|
540
|
+
(service) => isMine(service, userId) || service.service_name === selected
|
|
541
|
+
)
|
|
542
|
+
: services;
|
|
543
|
+
|
|
544
|
+
const isRail = variant === "rail";
|
|
545
|
+
const width = isRail ? (collapsed ? RAIL_COLLAPSED_WIDTH : RAIL_WIDTH) : "100%";
|
|
546
|
+
|
|
547
|
+
return (
|
|
548
|
+
<Box
|
|
549
|
+
component="nav"
|
|
550
|
+
aria-label="Servicios del producto"
|
|
551
|
+
sx={{
|
|
552
|
+
width,
|
|
553
|
+
flexShrink: 0,
|
|
554
|
+
height: "100%",
|
|
555
|
+
minHeight: 0,
|
|
556
|
+
display: "flex",
|
|
557
|
+
flexDirection: "column",
|
|
558
|
+
bgcolor: COLORS.white,
|
|
559
|
+
[side === "right" ? "borderLeft" : "borderRight"]: isRail ? `1px solid ${COLORS.s2}` : "none",
|
|
560
|
+
transition: "width 160ms ease",
|
|
561
|
+
overflow: "hidden",
|
|
562
|
+
fontFamily: FONT_STACK,
|
|
563
|
+
}}
|
|
564
|
+
>
|
|
565
|
+
{isRail && collapsed ? (
|
|
566
|
+
<CollapsedRail
|
|
567
|
+
visible={visible}
|
|
568
|
+
selected={selected}
|
|
569
|
+
onSelect={onSelect}
|
|
570
|
+
userId={userId}
|
|
571
|
+
onExpand={onToggleCollapsed}
|
|
572
|
+
side={side}
|
|
573
|
+
/>
|
|
574
|
+
) : (
|
|
575
|
+
<ExpandedRail
|
|
576
|
+
services={services}
|
|
577
|
+
visible={visible}
|
|
578
|
+
selected={selected}
|
|
579
|
+
onSelect={onSelect}
|
|
580
|
+
userId={userId}
|
|
581
|
+
filter={filter}
|
|
582
|
+
onFilterChange={onFilterChange}
|
|
583
|
+
showFilter={showFilter}
|
|
584
|
+
onCollapse={isRail ? onToggleCollapsed : undefined}
|
|
585
|
+
onClose={onClose}
|
|
586
|
+
side={side}
|
|
587
|
+
/>
|
|
588
|
+
)}
|
|
589
|
+
</Box>
|
|
590
|
+
);
|
|
591
|
+
};
|