contentoh-components-library 21.6.61 → 21.6.63

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 (102) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/ai/prompts/attribute.prompt.js +28 -0
  3. package/dist/ai/prompts/description.prompt.js +33 -0
  4. package/dist/ai/schemas/attribute.schema.js +35 -0
  5. package/dist/ai/schemas/description.schema.js +27 -0
  6. package/dist/components/atoms/MetricCard/index.js +3 -1
  7. package/dist/components/organisms/DashboardMetric/DashboardMetric.stories.js +12 -2
  8. package/dist/components/organisms/DashboardMetric/index.js +24 -1
  9. package/dist/components/organisms/DashboardMetric/styles.js +1 -1
  10. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +11 -5
  11. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +58 -31
  12. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +5 -1
  13. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +3 -40
  14. package/dist/components/pages/CreatorsProductEdition/sections/ImagesSection.js +18 -3
  15. package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +4 -13
  16. package/dist/components/pages/CreatorsProductEdition/sections/TabsBar.js +10 -0
  17. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +10 -2
  18. package/dist/components/pages/Dashboard/dashboardUtils.js +3 -3
  19. package/dist/components/pages/Dashboard/index.js +102 -88
  20. package/dist/components/pages/Dashboard/styles.js +1 -1
  21. package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +2 -15
  22. package/dist/components/pages/RetailerProductEdition/index.js +40 -57
  23. package/dist/components/pages/RetailerProductEdition/utils.js +2 -6
  24. package/dist/index.js +0 -13
  25. package/package.json +1 -1
  26. package/src/components/atoms/MetricCard/index.js +2 -2
  27. package/src/components/organisms/DashboardMetric/DashboardMetric.stories.js +11 -0
  28. package/src/components/organisms/DashboardMetric/index.js +13 -1
  29. package/src/components/organisms/DashboardMetric/styles.js +13 -0
  30. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +22 -3
  31. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +36 -1
  32. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +15 -3
  33. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +8 -18
  34. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +24 -2
  35. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +4 -8
  36. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +9 -0
  37. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +6 -0
  38. package/src/components/pages/Dashboard/dashboardUtils.js +3 -3
  39. package/src/components/pages/Dashboard/index.js +62 -106
  40. package/src/components/pages/Dashboard/styles.js +17 -12
  41. package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +0 -12
  42. package/src/components/pages/RetailerProductEdition/index.js +3 -28
  43. package/src/components/pages/RetailerProductEdition/utils.js +0 -5
  44. package/src/index.js +0 -1
  45. package/dist/components/organisms/ServicesNavigation/ServiceToolbar.js +0 -210
  46. package/dist/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +0 -138
  47. package/dist/components/organisms/ServicesNavigation/ServicesRail.js +0 -705
  48. package/dist/components/organisms/ServicesNavigation/catalog.js +0 -505
  49. package/dist/components/organisms/ServicesNavigation/index.js +0 -337
  50. package/dist/components/organisms/ServicesNavigation/model.js +0 -167
  51. package/dist/components/organisms/ServicesNavigation/ui.js +0 -371
  52. package/dist/components/organisms/ServicesNavigation/views.js +0 -1237
  53. package/dist/components/organisms/ServicesWorkspace/AssigneePicker.js +0 -459
  54. package/dist/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +0 -119
  55. package/dist/components/organisms/ServicesWorkspace/demo.js +0 -137
  56. package/dist/components/organisms/ServicesWorkspace/index.js +0 -813
  57. package/dist/components/organisms/ServicesWorkspace/servicesModel.js +0 -351
  58. package/dist/components/organisms/ServicesWorkspace/tooltipStyles.js +0 -37
  59. package/dist/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +0 -387
  60. package/dist/components/pages/ProductEdition/ProductEdition.stories.js +0 -148
  61. package/dist/components/pages/ProductEdition/ProductEditionView.js +0 -1355
  62. package/dist/components/pages/ProductEdition/ServiceHeader.js +0 -142
  63. package/dist/components/pages/ProductEdition/api.js +0 -256
  64. package/dist/components/pages/ProductEdition/index.js +0 -37
  65. package/dist/components/pages/ProductEdition/permissions.js +0 -200
  66. package/dist/components/pages/ProductEdition/permissions.test.js +0 -320
  67. package/dist/components/pages/ProductEdition/sections/CatalogServicePanel.js +0 -237
  68. package/dist/components/pages/ProductEdition/sections/EditionTopBar.js +0 -174
  69. package/dist/components/pages/ProductEdition/sections/ImagePanel.js +0 -440
  70. package/dist/components/pages/ProductEdition/sections/ServiceTeam.js +0 -275
  71. package/dist/components/pages/ProductEdition/sections/ServiceTools.js +0 -186
  72. package/dist/components/pages/ProductEdition/services.js +0 -233
  73. package/dist/components/pages/ProductEdition/services.test.js +0 -195
  74. package/src/components/organisms/ServicesNavigation/ServiceToolbar.jsx +0 -143
  75. package/src/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +0 -101
  76. package/src/components/organisms/ServicesNavigation/ServicesRail.jsx +0 -591
  77. package/src/components/organisms/ServicesNavigation/catalog.js +0 -180
  78. package/src/components/organisms/ServicesNavigation/index.jsx +0 -232
  79. package/src/components/organisms/ServicesNavigation/model.js +0 -85
  80. package/src/components/organisms/ServicesNavigation/ui.jsx +0 -229
  81. package/src/components/organisms/ServicesNavigation/views.jsx +0 -705
  82. package/src/components/organisms/ServicesWorkspace/AssigneePicker.js +0 -348
  83. package/src/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +0 -61
  84. package/src/components/organisms/ServicesWorkspace/demo.js +0 -58
  85. package/src/components/organisms/ServicesWorkspace/index.js +0 -556
  86. package/src/components/organisms/ServicesWorkspace/servicesModel.js +0 -203
  87. package/src/components/organisms/ServicesWorkspace/tooltipStyles.js +0 -27
  88. package/src/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +0 -175
  89. package/src/components/pages/ProductEdition/ProductEdition.stories.js +0 -106
  90. package/src/components/pages/ProductEdition/ProductEditionView.jsx +0 -1142
  91. package/src/components/pages/ProductEdition/ServiceHeader.jsx +0 -104
  92. package/src/components/pages/ProductEdition/api.js +0 -84
  93. package/src/components/pages/ProductEdition/index.js +0 -21
  94. package/src/components/pages/ProductEdition/permissions.js +0 -128
  95. package/src/components/pages/ProductEdition/permissions.test.js +0 -249
  96. package/src/components/pages/ProductEdition/sections/CatalogServicePanel.jsx +0 -123
  97. package/src/components/pages/ProductEdition/sections/EditionTopBar.jsx +0 -134
  98. package/src/components/pages/ProductEdition/sections/ImagePanel.jsx +0 -342
  99. package/src/components/pages/ProductEdition/sections/ServiceTeam.jsx +0 -184
  100. package/src/components/pages/ProductEdition/sections/ServiceTools.jsx +0 -134
  101. package/src/components/pages/ProductEdition/services.js +0 -187
  102. package/src/components/pages/ProductEdition/services.test.js +0 -88
@@ -1,184 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import {
3
- Avatar,
4
- Box,
5
- ButtonBase,
6
- List,
7
- ListItemAvatar,
8
- ListItemButton,
9
- ListItemText,
10
- Popover,
11
- Stack,
12
- Tooltip,
13
- Typography,
14
- } from "@mui/material";
15
-
16
- import { getProfilePicture } from "../../../../global-files/data";
17
- import { COLORS, FONTS } from "../../CreatorsProductEdition/sections/tokens";
18
-
19
- /*
20
- Equipo del servicio abierto: especialista, coordinador y auditor, con foto,
21
- rol y nombre («Tú» para quien está viendo).
22
-
23
- Reemplaza al grupo de avatares «Asig.» de la barra de pestañas, que no decía
24
- quién era quién. Quien puede asignar (permissions.canAssign) hace clic en el
25
- especialista o en el coordinador y elige de la lista, igual que antes.
26
-
27
- `roles` (opcional) limita los roles que se muestran: un servicio del catálogo
28
- de flujo simple solo tiene especialista. Por omisión, los tres.
29
- */
30
-
31
- const ROLES = [
32
- { role: "especialist", label: "Especialista", assignable: true },
33
- { role: "facilitator", label: "Coordinador", assignable: true },
34
- { role: "auditor", label: "Auditor", assignable: false },
35
- ];
36
-
37
- const nameOf = (person) =>
38
- person ? [person.name, person.last_name].filter(Boolean).join(" ").trim() : "";
39
-
40
- const initialsOf = (person) =>
41
- nameOf(person)
42
- .split(/\s+/)
43
- .filter(Boolean)
44
- .slice(0, 2)
45
- .map((word) => word[0])
46
- .join("")
47
- .toUpperCase();
48
-
49
- const PersonAvatar = ({ person, isMe, size = 26 }) => (
50
- <Avatar
51
- src={person?.id_user ? getProfilePicture(person.id_user, 48, 48) : undefined}
52
- alt={nameOf(person)}
53
- sx={{
54
- width: size,
55
- height: size,
56
- fontSize: size * 0.4,
57
- fontFamily: FONTS.Lato,
58
- fontWeight: 700,
59
- bgcolor: person ? COLORS.s2 : COLORS.white,
60
- color: person ? COLORS.s5 : COLORS.s3,
61
- border: `2px solid ${isMe ? COLORS.magenta_s2 : COLORS.white}`,
62
- outline: person ? "none" : `1px dashed ${COLORS.s3}`,
63
- outlineOffset: -3,
64
- }}
65
- >
66
- {person ? initialsOf(person) : "?"}
67
- </Avatar>
68
- );
69
-
70
- export const ServiceTeam = ({
71
- assignees = {},
72
- userId,
73
- canAssign = false,
74
- assig,
75
- setAssignation,
76
- resetKey,
77
- roles,
78
- }) => {
79
- const [anchor, setAnchor] = useState(null);
80
- const [role, setRole] = useState(null);
81
- useEffect(() => setAnchor(null), [resetKey]);
82
-
83
- const options = assig?.collaborators?.[role] ?? [];
84
-
85
- return (
86
- <>
87
- <Stack direction="row" alignItems="center" spacing={1.5} sx={{ flexWrap: "wrap", rowGap: 0.75, minWidth: 0 }}>
88
- {ROLES.filter((item) => !roles || roles.includes(item.role)).map((item) => {
89
- const person = assignees[item.role];
90
- const isMe = Boolean(person) && person.id_user === userId;
91
- const clickable = canAssign && item.assignable;
92
- const content = (
93
- <Stack direction="row" alignItems="center" spacing={0.75}>
94
- <PersonAvatar person={person} isMe={isMe} />
95
- <Box sx={{ lineHeight: 1.1, textAlign: "left", minWidth: 0 }}>
96
- <Typography
97
- sx={{
98
- fontFamily: FONTS.Lato,
99
- fontSize: 10,
100
- color: COLORS.s4,
101
- textTransform: "uppercase",
102
- letterSpacing: "0.05em",
103
- }}
104
- >
105
- {item.label}
106
- </Typography>
107
- <Typography
108
- noWrap
109
- sx={{
110
- maxWidth: 120,
111
- fontFamily: FONTS.Lato,
112
- fontSize: 12,
113
- fontWeight: isMe ? 700 : 400,
114
- color: person ? COLORS.s5 : COLORS.s3,
115
- }}
116
- >
117
- {person ? (isMe ? "Tú" : nameOf(person)) : "Sin asignar"}
118
- </Typography>
119
- </Box>
120
- </Stack>
121
- );
122
- const title = person ? `${item.label}: ${nameOf(person)}` : `${item.label}: sin asignar`;
123
- return clickable ? (
124
- <Tooltip key={item.role} title={`${title} · Clic para asignar`} arrow>
125
- <ButtonBase
126
- onClick={(event) => {
127
- setRole(item.role);
128
- setAnchor(event.currentTarget);
129
- }}
130
- sx={{ borderRadius: "8px", px: 0.5, py: 0.25, "&:hover": { bgcolor: COLORS.s2 } }}
131
- >
132
- {content}
133
- </ButtonBase>
134
- </Tooltip>
135
- ) : (
136
- <Tooltip key={item.role} title={title} arrow>
137
- <Box sx={{ px: 0.5, py: 0.25 }}>{content}</Box>
138
- </Tooltip>
139
- );
140
- })}
141
- </Stack>
142
-
143
- <Popover
144
- open={Boolean(anchor)}
145
- anchorEl={anchor}
146
- onClose={() => setAnchor(null)}
147
- anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
148
- transformOrigin={{ vertical: "top", horizontal: "left" }}
149
- PaperProps={{ sx: { width: 260, maxWidth: "calc(100vw - 24px)", mt: 1, p: 1.5, borderRadius: "10px" } }}
150
- >
151
- <Typography sx={{ fontFamily: FONTS.Lato, fontSize: 12, fontWeight: 700, color: COLORS.s4, mb: 1 }}>
152
- Asignar {ROLES.find((item) => item.role === role)?.label.toLowerCase()}
153
- </Typography>
154
- {options.length ? (
155
- <List dense disablePadding sx={{ maxHeight: 280, overflowY: "auto" }}>
156
- {options.map((person, index) => (
157
- <ListItemButton
158
- key={`${index}-${person?.id_user}`}
159
- selected={person?.id_user === assignees[role]?.id_user}
160
- onClick={() => {
161
- setAssignation && setAssignation(role, person?.id_user);
162
- setAnchor(null);
163
- }}
164
- sx={{ borderRadius: "6px" }}
165
- >
166
- <ListItemAvatar sx={{ minWidth: 38 }}>
167
- <PersonAvatar person={person} />
168
- </ListItemAvatar>
169
- <ListItemText
170
- primary={nameOf(person)}
171
- primaryTypographyProps={{ sx: { fontFamily: FONTS.Lato, fontSize: 14, color: COLORS.s5 } }}
172
- />
173
- </ListItemButton>
174
- ))}
175
- </List>
176
- ) : (
177
- <Typography sx={{ fontFamily: FONTS.Lato, fontSize: 13, color: COLORS.s4, py: 1 }}>
178
- Sin personas para asignar
179
- </Typography>
180
- )}
181
- </Popover>
182
- </>
183
- );
184
- };
@@ -1,134 +0,0 @@
1
- import { Button, IconButton, Stack, Tooltip } from "@mui/material";
2
-
3
- import { useAiProductEdition } from "../../../../contexts/AiProductEdition";
4
-
5
- import saveIcon from "../../../../assets/images/generalButton/saveIcon.svg";
6
- import saveIconHover from "../../../../assets/images/generalButton/saveIconHover.svg";
7
- import deleteIcon from "../../../../assets/images/generalButton/deleteIcon.svg";
8
- import deleteIconHover from "../../../../assets/images/generalButton/deleteIconHover.svg";
9
- import downloadIcon from "../../../../assets/images/generalButton/downloadIcon.svg";
10
- import rowsLayoutIcon from "../../../../assets/images/generalButton/rowLayout.svg";
11
- import gridLayoutIcon from "../../../../assets/images/generalButton/gridLayout.svg";
12
-
13
- import { COLORS, FONTS } from "../../CreatorsProductEdition/sections/tokens";
14
-
15
- /*
16
- Herramientas del servicio abierto: versión, guardar y, en imágenes, borrar,
17
- descargar y cambiar de vista.
18
-
19
- Sale de `ServiceActions` de CreatorsProductEdition sin tres cosas que ahora
20
- viven en otro lado o ya no se usan:
21
- - el botón de ayuda (las diapositivas de «Valida tu producto…»): el equipo
22
- ya no lo usa;
23
- - el estatus: lo muestra el encabezado del servicio junto a su nombre;
24
- - las asignaciones: las muestra ServiceTeam con nombre y rol.
25
- */
26
-
27
- /** Botón redondo con el ícono de siempre y su versión «hover». */
28
- const RoundIcon = ({ label, icon, hoverIcon, onClick, bordered = COLORS.s3, hoverBg = COLORS.magenta_s2, size = 15 }) => (
29
- <Tooltip title={label} arrow>
30
- <IconButton
31
- aria-label={label}
32
- onClick={onClick}
33
- sx={{
34
- width: 28,
35
- height: 28,
36
- p: 0,
37
- border: bordered ? `1px solid ${bordered}` : "none",
38
- backgroundImage: `url(${icon})`,
39
- backgroundRepeat: "no-repeat",
40
- backgroundPosition: "center",
41
- backgroundSize: size,
42
- "&:hover": {
43
- backgroundColor: hoverBg,
44
- borderColor: hoverBg,
45
- ...(hoverIcon ? { backgroundImage: `url(${hoverIcon})` } : {}),
46
- },
47
- }}
48
- />
49
- </Tooltip>
50
- );
51
-
52
- export const ServiceTools = ({
53
- imagesSection = false,
54
- imageLayout,
55
- setImageLayout,
56
- downloadImages,
57
- askToDeleteImages,
58
- onClickSave,
59
- showSaveButton,
60
- version,
61
- setShowVersionSelector,
62
- }) => {
63
- const { inputsUsingAi } = useAiProductEdition();
64
-
65
- const save = () => {
66
- if (Object.values(inputsUsingAi ?? {}).some((input) => !!input?.using)) {
67
- console.warn("Para guardar se debe de dejar de usar la generación con IA");
68
- return;
69
- }
70
- onClickSave && onClickSave();
71
- };
72
-
73
- return (
74
- <Stack direction="row" alignItems="center" gap="8px" sx={{ flexShrink: 0 }}>
75
- {version && (
76
- <Tooltip title="Cambiar de versión" arrow>
77
- <Button
78
- onClick={(event) => {
79
- event.stopPropagation();
80
- setShowVersionSelector(true);
81
- }}
82
- sx={{
83
- minWidth: 28,
84
- width: 28,
85
- height: 28,
86
- p: 0,
87
- borderRadius: "50%",
88
- border: `1px solid ${COLORS.color_purple}`,
89
- color: COLORS.color_purple,
90
- fontFamily: FONTS.AvenirNext,
91
- fontSize: 11,
92
- lineHeight: "28px",
93
- }}
94
- >
95
- V{version}
96
- </Button>
97
- </Tooltip>
98
- )}
99
- {showSaveButton && (
100
- <RoundIcon
101
- label="Guardar"
102
- icon={saveIcon}
103
- hoverIcon={saveIconHover}
104
- bordered={COLORS.color_gray}
105
- hoverBg={COLORS.color_pink}
106
- size={undefined}
107
- onClick={save}
108
- />
109
- )}
110
- {imagesSection && (
111
- <>
112
- {showSaveButton && (
113
- <RoundIcon
114
- label="Borrar seleccionadas"
115
- icon={deleteIcon}
116
- hoverIcon={deleteIconHover}
117
- onClick={() => askToDeleteImages && askToDeleteImages()}
118
- />
119
- )}
120
- <RoundIcon label="Descargar" icon={downloadIcon} onClick={() => downloadImages && downloadImages()} />
121
- <Tooltip title={imageLayout ? "Ver como lista" : "Ver como cuadrícula"} arrow>
122
- <IconButton
123
- aria-label={imageLayout ? "Ver como lista" : "Ver como cuadrícula"}
124
- onClick={() => setImageLayout && setImageLayout(!imageLayout)}
125
- sx={{ width: 28, height: 28, p: 0 }}
126
- >
127
- <img src={imageLayout ? rowsLayoutIcon : gridLayoutIcon} alt="" width={24} height={24} />
128
- </IconButton>
129
- </Tooltip>
130
- </>
131
- )}
132
- </Stack>
133
- );
134
- };
@@ -1,187 +0,0 @@
1
- import { SERVICE_CATALOG } from "../../organisms/ServicesNavigation/catalog";
2
- import {
3
- CLASSIC_SERVICES,
4
- isAssignedToService,
5
- isAuditorAssigned,
6
- isPendingFor,
7
- rolesInService,
8
- tabOf,
9
- } from "./permissions";
10
-
11
- /*
12
- Servicios del producto para la lista lateral de la edición.
13
-
14
- Hoy los servicios con editor son los tres de siempre: salen de los estatus
15
- del producto (`<servicio>_status`) y el de la cadena activa sale de
16
- `services_data`. Un servicio con estatus vacío o «NS» no está contratado y
17
- no aparece.
18
-
19
- Esta lista es el respaldo de cuando GET /product-services no responde
20
- (buildServicesFromApi, abajo): solo los servicios de siempre. Los servicios
21
- del producto nunca salen de datos de muestra; la prop `demoServices` es solo
22
- para las historias de Storybook.
23
- */
24
-
25
- const NOT_REQUESTED = [undefined, null, "", "NS", "NULL", "-"];
26
-
27
- const catalogEntry = (serviceName) =>
28
- SERVICE_CATALOG.find((item) => item.service_name === serviceName) ?? {
29
- service_name: serviceName,
30
- group: "review",
31
- view: "files",
32
- icon: null,
33
- };
34
-
35
- /** Persona de las listas de asignables (`collaborator_assignations`). */
36
- const findUser = (collaborators, id) => {
37
- if (!id) return null;
38
- const all = Object.values(collaborators ?? {}).flat();
39
- const user = all.find((item) => item?.id_user === id);
40
- return user ?? { id_user: id, name: "Asignado", last_name: "" };
41
- };
42
-
43
- // `services_data` y `collaborator_assignations` llegan en null mientras carga.
44
- const retailerStatus = (servicesData, serviceName, retailerId, fallback) =>
45
- (servicesData ?? []).find((item) => item.service === serviceName && item.id_retailer === retailerId)?.status ??
46
- fallback;
47
-
48
- /**
49
- * @returns {Array<{service_name, display_name, group, view, icon, status,
50
- * tab, assignees, mine, pending, is_demo}>}
51
- */
52
- export const buildEditionServices = ({
53
- product,
54
- user,
55
- servicesData,
56
- activeRetailerId,
57
- collaboratorAssignations,
58
- // Auditores (rol 6): `collaborator_assignations` solo trae especialistas y
59
- // coordinadores, y sin esta lista el auditor no tendría nombre.
60
- auditors,
61
- demoServices,
62
- }) => {
63
- if (!product) return [];
64
-
65
- const classic = CLASSIC_SERVICES.filter(
66
- (item) => !NOT_REQUESTED.includes(product[`${item.service_name}_status`])
67
- ).map((item) => {
68
- const definition = catalogEntry(item.service_name);
69
- const collaborators = collaboratorAssignations?.[item.tab]?.collaborators ?? {};
70
- const status = retailerStatus(
71
- servicesData,
72
- item.service_name,
73
- activeRetailerId,
74
- product[`${item.service_name}_status`]
75
- );
76
- return {
77
- ...definition,
78
- status,
79
- tab: item.tab,
80
- assignees: {
81
- especialist: findUser(collaborators, product[`id_${item.service_name}_especialist`]),
82
- facilitator: findUser(collaborators, product[`id_${item.service_name}_facilitator`]),
83
- auditor: product.id_auditor
84
- ? findUser({ ...collaborators, auditors: auditors ?? [] }, product.id_auditor)
85
- : null,
86
- },
87
- mine:
88
- rolesInService(product, user, item.service_name).length > 0 ||
89
- isAssignedToService(product, user, item.service_name) ||
90
- isAuditorAssigned(product, user),
91
- pending: isPendingFor(product, user, item.service_name, status),
92
- is_demo: false,
93
- };
94
- });
95
-
96
- // Solo historias (`demoServices`): el auditor del producto lo es de todos sus
97
- // servicios, así «Míos» de un auditor no esconde los que no tienen editor.
98
- const productAuditor = product.id_auditor ? findUser({ auditors: auditors ?? [] }, product.id_auditor) : null;
99
- const auditorIsMe = isAuditorAssigned(product, user);
100
-
101
- const demo = (demoServices ?? []).map((service) => ({
102
- ...catalogEntry(service.service_name),
103
- display_name: service.display_name ?? catalogEntry(service.service_name).display_name ?? service.service_name,
104
- status: service.status,
105
- tab: null,
106
- assignees: { especialist: null, facilitator: null, auditor: productAuditor },
107
- mine: auditorIsMe,
108
- pending: false,
109
- is_demo: true,
110
- }));
111
-
112
- return [...classic, ...demo];
113
- };
114
-
115
- /* ------------------------------------------------------------------------ */
116
- /* Desde el backend (GET /product-services) */
117
- /* ------------------------------------------------------------------------ */
118
-
119
- /*
120
- Los servicios del producto como los devuelve GET /product-services: leídos
121
- de las columnas del catálogo de rel_order_services (id_service, id_status,
122
- id_especialist, id_facilitator). Trae todos los servicios del producto, los
123
- de siempre y los del catálogo, con su estatus por cadena, su equipo y lo que
124
- la persona puede hacer con cada uno (`actions`).
125
-
126
- Cuando esta lista está, la edición la usa en lugar de armar los servicios con
127
- los campos del producto (buildEditionServices).
128
- */
129
-
130
- /** En qué estatus le toca actuar a cada rol (flujo completo, el del motor). */
131
- const WORKFLOW_ACTIONABLE = {
132
- especialist: ["AS", "CA", "RC", "RA"],
133
- facilitator: ["PA", "IE"],
134
- auditor: ["AC", "RP", "RCA", "SAC"],
135
- };
136
-
137
- /** Roles de la persona en el servicio, según su equipo. */
138
- const rolesOfUser = (assignees, userId) =>
139
- Object.entries(assignees ?? {})
140
- .filter(([, person]) => person && userId && person.id_user === userId)
141
- .map(([role]) => role);
142
-
143
- /**
144
- * @param {{ productServices: { services: Array }, user: Object, activeRetailerId?: number }} params
145
- * @returns {Array} servicios con la forma de la lista lateral, más `api` (el
146
- * servicio tal como llegó) para los paneles que lo necesitan.
147
- */
148
- export const buildServicesFromApi = ({ productServices, user, activeRetailerId }) => {
149
- const userId = user?.id_user;
150
- return (productServices?.services ?? []).map((service) => {
151
- const entry = catalogEntry(service.service_name);
152
- const flowType = service.status_flow?.flow_type ?? "workflow";
153
- const simple = flowType === "simple";
154
- // Estatus de la cadena activa; si el servicio no es por cadena (o no está
155
- // en ella), el general.
156
- const status =
157
- service.retailers?.find((retailer) => retailer.id_retailer === activeRetailerId)?.status ??
158
- service.status ??
159
- null;
160
- const assignees = service.assignees ?? {};
161
- const roles = rolesOfUser(assignees, userId);
162
- const parts = String(status?.status_name ?? "").split("/");
163
- const pending = simple
164
- ? roles.includes("especialist") && Boolean(status) && !status.is_final
165
- : roles.some((role) => parts.some((part) => WORKFLOW_ACTIONABLE[role]?.includes(part)));
166
-
167
- return {
168
- ...entry,
169
- service_name: service.service_name,
170
- display_name: service.display_name ?? entry.display_name ?? service.service_name,
171
- group: service.category?.category_name ?? entry.group,
172
- view: simple ? "simple" : entry.view,
173
- icon: service.icon_name ?? entry.icon,
174
- status: status?.status_name ?? null,
175
- status_label: status?.display_name ?? null,
176
- status_color: status?.color ?? null,
177
- status_flow_type: flowType,
178
- tab: tabOf(service.service_name),
179
- assignees,
180
- mine: roles.length > 0,
181
- pending,
182
- is_demo: false,
183
- is_catalog: !service.is_legacy,
184
- api: service,
185
- };
186
- });
187
- };
@@ -1,88 +0,0 @@
1
- /**
2
- * Lista lateral de la edición a partir de GET /product-services.
3
- *
4
- * npx vitest run src/components/pages/ProductEdition
5
- */
6
- import { describe, expect, it } from "vitest";
7
- import { buildServicesFromApi } from "./services";
8
-
9
- const person = (id_user, name) => ({ id_user, name, last_name: "Prueba" });
10
- const status = (status_name, extra = {}) => ({ status_name, display_name: status_name, color: null, ...extra });
11
-
12
- const productServices = {
13
- article_id: 10,
14
- order_id: 500,
15
- services: [
16
- {
17
- service_name: "datasheet",
18
- display_name: "Ficha técnica",
19
- category: { category_name: "texts" },
20
- status_flow: { flow_name: "workflow", flow_type: "workflow" },
21
- is_legacy: true,
22
- status: status("PA"),
23
- retailers: [
24
- { id_retailer: 58, status: status("PA") },
25
- { id_retailer: 59, status: status("RA/AS") },
26
- ],
27
- roles: ["especialist", "facilitator", "auditor"],
28
- assignees: { especialist: person(426, "Textos"), facilitator: person(901, "Coord"), auditor: person(903, "QA") },
29
- actions: { set_status: false, assign: false },
30
- },
31
- {
32
- service_name: "productPurchase",
33
- display_name: "Compra de producto",
34
- icon_name: null,
35
- category: { category_name: "logistics" },
36
- status_flow: { flow_name: "checkbox", flow_type: "simple" },
37
- is_legacy: false,
38
- per_retailer: false,
39
- status: status("PE", { display_name: "Pendiente", color: "#FFAA00", is_final: false }),
40
- retailers: [{ id_retailer: 0, status: status("PE", { display_name: "Pendiente", color: "#FFAA00", is_final: false }) }],
41
- status_options: [status("PE"), status("HE", { is_final: true })],
42
- roles: ["especialist"],
43
- assignees: { especialist: person(426, "Textos"), facilitator: null, auditor: null },
44
- actions: { set_status: true, assign: true },
45
- },
46
- ],
47
- };
48
-
49
- describe("buildServicesFromApi", () => {
50
- const specialist = { id_user: 426, id_role: 7 };
51
-
52
- it("toma el estatus de la cadena activa; si no está en ella, el general", () => {
53
- const [datasheet, purchase] = buildServicesFromApi({ productServices, user: specialist, activeRetailerId: 59 });
54
- expect(datasheet.status).toBe("RA/AS");
55
- expect(purchase.status).toBe("PE");
56
- });
57
-
58
- it("los de siempre conservan su pestaña; los del catálogo usan su bloque y la vista simple", () => {
59
- const [datasheet, purchase] = buildServicesFromApi({ productServices, user: specialist, activeRetailerId: 58 });
60
- expect(datasheet).toMatchObject({ tab: "Ficha técnica", group: "texts", is_catalog: false });
61
- expect(purchase).toMatchObject({
62
- tab: null,
63
- group: "logistics",
64
- view: "simple",
65
- icon: "purchase",
66
- status_label: "Pendiente",
67
- status_color: "#FFAA00",
68
- status_flow_type: "simple",
69
- is_catalog: true,
70
- });
71
- expect(purchase.api).toBe(productServices.services[1]);
72
- });
73
-
74
- it("«Míos» y «te toca» salen del equipo del servicio", () => {
75
- const [datasheet, purchase] = buildServicesFromApi({ productServices, user: specialist, activeRetailerId: 59 });
76
- // RA/AS: le toca a la especialista asignada.
77
- expect(datasheet).toMatchObject({ mine: true, pending: true });
78
- // Flujo simple sin terminar: le toca a su especialista.
79
- expect(purchase).toMatchObject({ mine: true, pending: true });
80
-
81
- const [, other] = buildServicesFromApi({ productServices, user: { id_user: 1 }, activeRetailerId: 59 });
82
- expect(other).toMatchObject({ mine: false, pending: false });
83
- });
84
-
85
- it("sin respuesta del backend, lista vacía", () => {
86
- expect(buildServicesFromApi({ productServices: null, user: specialist })).toEqual([]);
87
- });
88
- });