contentoh-components-library 21.6.61 → 21.6.62

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 (88) 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/pages/CreatorsProductEdition/CreatorsProductEditionView.js +11 -5
  7. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +58 -31
  8. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +5 -1
  9. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +3 -40
  10. package/dist/components/pages/CreatorsProductEdition/sections/ImagesSection.js +18 -3
  11. package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +4 -13
  12. package/dist/components/pages/CreatorsProductEdition/sections/TabsBar.js +10 -0
  13. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +10 -2
  14. package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +2 -15
  15. package/dist/components/pages/RetailerProductEdition/index.js +40 -57
  16. package/dist/components/pages/RetailerProductEdition/utils.js +2 -6
  17. package/dist/index.js +0 -13
  18. package/package.json +1 -1
  19. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +22 -3
  20. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +36 -1
  21. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +15 -3
  22. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +8 -18
  23. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +24 -2
  24. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +4 -8
  25. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +9 -0
  26. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +6 -0
  27. package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +0 -12
  28. package/src/components/pages/RetailerProductEdition/index.js +3 -28
  29. package/src/components/pages/RetailerProductEdition/utils.js +0 -5
  30. package/src/index.js +0 -1
  31. package/dist/components/organisms/ServicesNavigation/ServiceToolbar.js +0 -210
  32. package/dist/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +0 -138
  33. package/dist/components/organisms/ServicesNavigation/ServicesRail.js +0 -705
  34. package/dist/components/organisms/ServicesNavigation/catalog.js +0 -505
  35. package/dist/components/organisms/ServicesNavigation/index.js +0 -337
  36. package/dist/components/organisms/ServicesNavigation/model.js +0 -167
  37. package/dist/components/organisms/ServicesNavigation/ui.js +0 -371
  38. package/dist/components/organisms/ServicesNavigation/views.js +0 -1237
  39. package/dist/components/organisms/ServicesWorkspace/AssigneePicker.js +0 -459
  40. package/dist/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +0 -119
  41. package/dist/components/organisms/ServicesWorkspace/demo.js +0 -137
  42. package/dist/components/organisms/ServicesWorkspace/index.js +0 -813
  43. package/dist/components/organisms/ServicesWorkspace/servicesModel.js +0 -351
  44. package/dist/components/organisms/ServicesWorkspace/tooltipStyles.js +0 -37
  45. package/dist/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +0 -387
  46. package/dist/components/pages/ProductEdition/ProductEdition.stories.js +0 -148
  47. package/dist/components/pages/ProductEdition/ProductEditionView.js +0 -1355
  48. package/dist/components/pages/ProductEdition/ServiceHeader.js +0 -142
  49. package/dist/components/pages/ProductEdition/api.js +0 -256
  50. package/dist/components/pages/ProductEdition/index.js +0 -37
  51. package/dist/components/pages/ProductEdition/permissions.js +0 -200
  52. package/dist/components/pages/ProductEdition/permissions.test.js +0 -320
  53. package/dist/components/pages/ProductEdition/sections/CatalogServicePanel.js +0 -237
  54. package/dist/components/pages/ProductEdition/sections/EditionTopBar.js +0 -174
  55. package/dist/components/pages/ProductEdition/sections/ImagePanel.js +0 -440
  56. package/dist/components/pages/ProductEdition/sections/ServiceTeam.js +0 -275
  57. package/dist/components/pages/ProductEdition/sections/ServiceTools.js +0 -186
  58. package/dist/components/pages/ProductEdition/services.js +0 -233
  59. package/dist/components/pages/ProductEdition/services.test.js +0 -195
  60. package/src/components/organisms/ServicesNavigation/ServiceToolbar.jsx +0 -143
  61. package/src/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +0 -101
  62. package/src/components/organisms/ServicesNavigation/ServicesRail.jsx +0 -591
  63. package/src/components/organisms/ServicesNavigation/catalog.js +0 -180
  64. package/src/components/organisms/ServicesNavigation/index.jsx +0 -232
  65. package/src/components/organisms/ServicesNavigation/model.js +0 -85
  66. package/src/components/organisms/ServicesNavigation/ui.jsx +0 -229
  67. package/src/components/organisms/ServicesNavigation/views.jsx +0 -705
  68. package/src/components/organisms/ServicesWorkspace/AssigneePicker.js +0 -348
  69. package/src/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +0 -61
  70. package/src/components/organisms/ServicesWorkspace/demo.js +0 -58
  71. package/src/components/organisms/ServicesWorkspace/index.js +0 -556
  72. package/src/components/organisms/ServicesWorkspace/servicesModel.js +0 -203
  73. package/src/components/organisms/ServicesWorkspace/tooltipStyles.js +0 -27
  74. package/src/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +0 -175
  75. package/src/components/pages/ProductEdition/ProductEdition.stories.js +0 -106
  76. package/src/components/pages/ProductEdition/ProductEditionView.jsx +0 -1142
  77. package/src/components/pages/ProductEdition/ServiceHeader.jsx +0 -104
  78. package/src/components/pages/ProductEdition/api.js +0 -84
  79. package/src/components/pages/ProductEdition/index.js +0 -21
  80. package/src/components/pages/ProductEdition/permissions.js +0 -128
  81. package/src/components/pages/ProductEdition/permissions.test.js +0 -249
  82. package/src/components/pages/ProductEdition/sections/CatalogServicePanel.jsx +0 -123
  83. package/src/components/pages/ProductEdition/sections/EditionTopBar.jsx +0 -134
  84. package/src/components/pages/ProductEdition/sections/ImagePanel.jsx +0 -342
  85. package/src/components/pages/ProductEdition/sections/ServiceTeam.jsx +0 -184
  86. package/src/components/pages/ProductEdition/sections/ServiceTools.jsx +0 -134
  87. package/src/components/pages/ProductEdition/services.js +0 -187
  88. 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
- });