contentoh-components-library 21.6.59 → 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.
Files changed (78) hide show
  1. package/dist/components/organisms/ServicesNavigation/ServiceToolbar.js +210 -0
  2. package/dist/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +138 -0
  3. package/dist/components/organisms/ServicesNavigation/ServicesRail.js +705 -0
  4. package/dist/components/organisms/ServicesNavigation/catalog.js +505 -0
  5. package/dist/components/organisms/ServicesNavigation/index.js +337 -0
  6. package/dist/components/organisms/ServicesNavigation/model.js +167 -0
  7. package/dist/components/organisms/ServicesNavigation/ui.js +371 -0
  8. package/dist/components/organisms/ServicesNavigation/views.js +1237 -0
  9. package/dist/components/organisms/ServicesWorkspace/AssigneePicker.js +459 -0
  10. package/dist/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +119 -0
  11. package/dist/components/organisms/ServicesWorkspace/demo.js +137 -0
  12. package/dist/components/organisms/ServicesWorkspace/index.js +813 -0
  13. package/dist/components/organisms/ServicesWorkspace/servicesModel.js +351 -0
  14. package/dist/components/organisms/ServicesWorkspace/tooltipStyles.js +37 -0
  15. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +40 -0
  16. package/dist/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +387 -0
  17. package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +13 -4
  18. package/dist/components/pages/ProductEdition/ProductEdition.stories.js +148 -0
  19. package/dist/components/pages/ProductEdition/ProductEditionView.js +1355 -0
  20. package/dist/components/pages/ProductEdition/ServiceHeader.js +142 -0
  21. package/dist/components/pages/ProductEdition/api.js +256 -0
  22. package/dist/components/pages/ProductEdition/index.js +37 -0
  23. package/dist/components/pages/ProductEdition/permissions.js +200 -0
  24. package/dist/components/pages/ProductEdition/permissions.test.js +320 -0
  25. package/dist/components/pages/ProductEdition/sections/CatalogServicePanel.js +237 -0
  26. package/dist/components/pages/ProductEdition/sections/EditionTopBar.js +174 -0
  27. package/dist/components/pages/ProductEdition/sections/ImagePanel.js +440 -0
  28. package/dist/components/pages/ProductEdition/sections/ServiceTeam.js +275 -0
  29. package/dist/components/pages/ProductEdition/sections/ServiceTools.js +186 -0
  30. package/dist/components/pages/ProductEdition/services.js +233 -0
  31. package/dist/components/pages/ProductEdition/services.test.js +195 -0
  32. package/dist/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +15 -2
  33. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +2 -2
  34. package/dist/components/pages/RetailerProductEdition/index.js +57 -40
  35. package/dist/components/pages/RetailerProductEdition/utils.js +6 -2
  36. package/dist/index.js +13 -0
  37. package/package.json +1 -1
  38. package/src/components/organisms/ServicesNavigation/ServiceToolbar.jsx +143 -0
  39. package/src/components/organisms/ServicesNavigation/ServicesNavigation.stories.js +101 -0
  40. package/src/components/organisms/ServicesNavigation/ServicesRail.jsx +591 -0
  41. package/src/components/organisms/ServicesNavigation/catalog.js +180 -0
  42. package/src/components/organisms/ServicesNavigation/index.jsx +232 -0
  43. package/src/components/organisms/ServicesNavigation/model.js +85 -0
  44. package/src/components/organisms/ServicesNavigation/ui.jsx +229 -0
  45. package/src/components/organisms/ServicesNavigation/views.jsx +705 -0
  46. package/src/components/organisms/ServicesWorkspace/AssigneePicker.js +348 -0
  47. package/src/components/organisms/ServicesWorkspace/ServicesWorkspace.stories.js +61 -0
  48. package/src/components/organisms/ServicesWorkspace/demo.js +58 -0
  49. package/src/components/organisms/ServicesWorkspace/index.js +556 -0
  50. package/src/components/organisms/ServicesWorkspace/servicesModel.js +203 -0
  51. package/src/components/organisms/ServicesWorkspace/tooltipStyles.js +27 -0
  52. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +17 -0
  53. package/src/components/pages/CreatorsProductEdition/mocks/productServices.fixtures.js +175 -0
  54. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +8 -4
  55. package/src/components/pages/ProductEdition/ProductEdition.stories.js +106 -0
  56. package/src/components/pages/ProductEdition/ProductEditionView.jsx +1142 -0
  57. package/src/components/pages/ProductEdition/ServiceHeader.jsx +104 -0
  58. package/src/components/pages/ProductEdition/api.js +84 -0
  59. package/src/components/pages/ProductEdition/index.js +21 -0
  60. package/src/components/pages/ProductEdition/permissions.js +128 -0
  61. package/src/components/pages/ProductEdition/permissions.test.js +249 -0
  62. package/src/components/pages/ProductEdition/sections/CatalogServicePanel.jsx +123 -0
  63. package/src/components/pages/ProductEdition/sections/EditionTopBar.jsx +134 -0
  64. package/src/components/pages/ProductEdition/sections/ImagePanel.jsx +342 -0
  65. package/src/components/pages/ProductEdition/sections/ServiceTeam.jsx +184 -0
  66. package/src/components/pages/ProductEdition/sections/ServiceTools.jsx +134 -0
  67. package/src/components/pages/ProductEdition/services.js +187 -0
  68. package/src/components/pages/ProductEdition/services.test.js +88 -0
  69. package/src/components/pages/RetailerProductEdition/RetailerProductEdition.stories.js +12 -0
  70. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +6 -2
  71. package/src/components/pages/RetailerProductEdition/index.js +28 -3
  72. package/src/components/pages/RetailerProductEdition/utils.js +5 -0
  73. package/src/index.js +1 -0
  74. package/.claude/settings.local.json +0 -7
  75. package/dist/ai/prompts/attribute.prompt.js +0 -28
  76. package/dist/ai/prompts/description.prompt.js +0 -33
  77. package/dist/ai/schemas/attribute.schema.js +0 -35
  78. package/dist/ai/schemas/description.schema.js +0 -27
@@ -0,0 +1,104 @@
1
+ import { Box, Button, Stack, Typography } from "@mui/material";
2
+ import MenuIcon from "@mui/icons-material/Menu";
3
+
4
+ import {
5
+ COLORS,
6
+ FONT_STACK,
7
+ TITLE_STACK,
8
+ ServiceIcon,
9
+ StatusChip,
10
+ serviceStatusProps,
11
+ outlineButtonSx,
12
+ } from "../../organisms/ServicesNavigation/ui";
13
+ import { groupLabel } from "../../organisms/ServicesNavigation/model";
14
+
15
+ /*
16
+ Encabezado del servicio abierto. Ocupa el lugar de la barra de pestañas,
17
+ con el diseño de la propuesta de navegación por servicios:
18
+
19
+ [ícono] GRUPO [equipo] [herramientas]
20
+ Nombre del servicio [estatus]
21
+
22
+ - `team`: especialista, coordinador y auditor (sections/ServiceTeam).
23
+ - `children`: versión, guardar e imágenes (sections/ServiceTools).
24
+ */
25
+ export const ServiceHeader = ({ service, isSummary, subtitle, onOpenRail, team, children }) => (
26
+ <Box
27
+ sx={{
28
+ display: "flex",
29
+ flexWrap: "wrap",
30
+ alignItems: "center",
31
+ columnGap: 2.5,
32
+ rowGap: 1.25,
33
+ pb: 1.5,
34
+ mb: 2,
35
+ borderBottom: `2px solid ${COLORS.s2}`,
36
+ minWidth: 0,
37
+ }}
38
+ >
39
+ {onOpenRail && (
40
+ <Button onClick={onOpenRail} startIcon={<MenuIcon sx={{ fontSize: 18 }} />} sx={outlineButtonSx}>
41
+ Servicios
42
+ </Button>
43
+ )}
44
+
45
+ <Stack direction="row" alignItems="center" spacing={1.25} sx={{ minWidth: 0, flex: "1 1 200px" }}>
46
+ <Box
47
+ sx={{
48
+ width: 40,
49
+ height: 40,
50
+ borderRadius: "10px",
51
+ bgcolor: COLORS.s2,
52
+ display: "flex",
53
+ alignItems: "center",
54
+ justifyContent: "center",
55
+ flexShrink: 0,
56
+ }}
57
+ >
58
+ <ServiceIcon icon={isSummary ? "summary" : service?.icon} sx={{ fontSize: 22, color: COLORS.magenta_s2 }} />
59
+ </Box>
60
+ <Box sx={{ minWidth: 0 }}>
61
+ <Typography
62
+ sx={{
63
+ fontFamily: FONT_STACK,
64
+ fontSize: 11,
65
+ fontWeight: 700,
66
+ letterSpacing: "0.06em",
67
+ textTransform: "uppercase",
68
+ color: COLORS.s4,
69
+ }}
70
+ >
71
+ {isSummary ? "Todos los servicios" : groupLabel(service?.group)}
72
+ </Typography>
73
+ <Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
74
+ <Typography
75
+ component="h3"
76
+ sx={{
77
+ fontFamily: TITLE_STACK,
78
+ fontWeight: 500,
79
+ fontSize: 20,
80
+ lineHeight: "26px",
81
+ color: COLORS.s5,
82
+ overflow: "hidden",
83
+ textOverflow: "ellipsis",
84
+ whiteSpace: "nowrap",
85
+ }}
86
+ >
87
+ {isSummary ? "Resumen" : service?.display_name}
88
+ </Typography>
89
+ {!isSummary && service?.status && service.status !== "-" && (
90
+ <StatusChip {...serviceStatusProps(service)} oval />
91
+ )}
92
+ </Stack>
93
+ </Box>
94
+ </Stack>
95
+
96
+ {subtitle && (
97
+ <Typography sx={{ fontFamily: FONT_STACK, fontSize: 13, color: COLORS.s4 }}>{subtitle}</Typography>
98
+ )}
99
+
100
+ {team}
101
+
102
+ {children && <Box sx={{ ml: "auto", flexShrink: 0 }}>{children}</Box>}
103
+ </Box>
104
+ );
@@ -0,0 +1,84 @@
1
+ import axios from "axios";
2
+
3
+ /*
4
+ Servicios del producto en el backend (Lambda `product-services`, ver
5
+ CRUD/services/docs/product-services.md en content-management-backend):
6
+
7
+ GET /product-services?articleId=&orderId= servicios, estatus, equipo y acciones
8
+ PUT /product-services/set-status estatus de un servicio de flujo simple
9
+ PUT /product-services/assign especialista o coordinador
10
+ GET /service-catalog?view=assignables a quién se puede asignar un rol
11
+
12
+ Las URL salen de REACT_APP_PRODUCT_SERVICES_ENDPOINT y
13
+ REACT_APP_SERVICE_CATALOG_ENDPOINT o, si no están, de
14
+ REACT_APP_SERVICES_ENDPOINT cambiando /services por la ruta nueva (mismo
15
+ API Gateway).
16
+ */
17
+
18
+ const fromServicesEndpoint = (path) => {
19
+ const services = process.env.REACT_APP_SERVICES_ENDPOINT;
20
+ return services ? services.replace(/\/services\/?$/, path) : null;
21
+ };
22
+
23
+ export const productServicesUrl = () =>
24
+ process.env.REACT_APP_PRODUCT_SERVICES_ENDPOINT || fromServicesEndpoint("/product-services");
25
+
26
+ const serviceCatalogUrl = () =>
27
+ process.env.REACT_APP_SERVICE_CATALOG_ENDPOINT || fromServicesEndpoint("/service-catalog");
28
+
29
+ /** Error con el mensaje del backend, para mostrarlo tal cual. */
30
+ export class ProductServicesError extends Error {
31
+ constructor(message, status, detail) {
32
+ super(message);
33
+ this.name = "ProductServicesError";
34
+ this.status = status;
35
+ this.detail = detail;
36
+ }
37
+ }
38
+
39
+ /*
40
+ Las Lambdas usan `integration: lambda`: API Gateway responde 200 y el
41
+ código real viene en `data.statusCode`, con el cuerpo como texto en
42
+ `data.body`.
43
+ */
44
+ const unwrap = (response) => {
45
+ const data = response?.data ?? {};
46
+ const body = typeof data.body === "string" ? JSON.parse(data.body) : data.body ?? data;
47
+ const status = Number(data.statusCode ?? response?.status ?? 200);
48
+ if (status >= 400) {
49
+ throw new ProductServicesError(body?.message ?? "Error en los servicios del producto", status, body?.detail);
50
+ }
51
+ return body;
52
+ };
53
+
54
+ export const fetchProductServices = async ({ articleId, orderId, token }) => {
55
+ const url = productServicesUrl();
56
+ if (!url) throw new ProductServicesError("Falta REACT_APP_SERVICES_ENDPOINT", 0);
57
+ const response = await axios.get(url, {
58
+ params: { articleId, orderId },
59
+ headers: { Authorization: token },
60
+ });
61
+ return unwrap(response);
62
+ };
63
+
64
+ const putAction = async (action, data, token) => {
65
+ const response = await axios.put(`${productServicesUrl()}/${action}`, data, {
66
+ headers: { Authorization: token },
67
+ });
68
+ return unwrap(response);
69
+ };
70
+
71
+ export const setServiceStatus = ({ articleId, orderId, serviceName, statusName, retailerIds, token }) =>
72
+ putAction("set-status", { articleId, orderId, serviceName, statusName, retailerIds }, token);
73
+
74
+ export const assignService = ({ articleId, orderId, serviceName, role, userId, token }) =>
75
+ putAction("assign", { articleId, orderId, serviceName, role, userId }, token);
76
+
77
+ /** Personas que pueden ocupar el rol del servicio. */
78
+ export const fetchAssignables = async ({ serviceName, role, token }) => {
79
+ const response = await axios.get(serviceCatalogUrl(), {
80
+ params: { view: "assignables", service_name: serviceName, assignment_role: role },
81
+ headers: { Authorization: token },
82
+ });
83
+ return unwrap(response)?.users ?? [];
84
+ };
@@ -0,0 +1,21 @@
1
+ import { ProviderProductEditionProvider } from "../RetailerProductEdition/context/provider-product-edition.context";
2
+ import { ProductEditionView } from "./ProductEditionView";
3
+
4
+ /**
5
+ * Edición de producto unificada: una sola pantalla para Creators y para
6
+ * Content Management, con la lista lateral de servicios. Lo que cambia entre
7
+ * aplicaciones son los permisos (`permissions.js`).
8
+ *
9
+ * Props: las de CreatorsProductEdition (`tabsSections`, `productSelected`,
10
+ * `user`, `token`, `location`, `enableDigitalAssets`) y además:
11
+ * - `app`: "creators" (fase 1). "management" llega en la fase 2.
12
+ * - `demoServices`: solo para historias de Storybook.
13
+ *
14
+ * Los servicios salen de GET /product-services; si no responde, la lista
15
+ * muestra los servicios de siempre del producto.
16
+ */
17
+ export const ProductEdition = (props) => (
18
+ <ProviderProductEditionProvider>
19
+ <ProductEditionView {...props} />
20
+ </ProviderProductEditionProvider>
21
+ );
@@ -0,0 +1,128 @@
1
+ /*
2
+ Permisos de la edición de producto, en un solo lugar.
3
+
4
+ Antes estas reglas vivían repartidas en `if` dentro de cada vista
5
+ (RetailerProductEdition, ProviderProductEdition y la copia del hook de
6
+ CreatorsProductEdition). Aquí son funciones puras: reciben el producto, la
7
+ persona y el servicio, y responden sí o no. Así se pueden leer como una tabla
8
+ y probar sin montar la pantalla.
9
+
10
+ FASE 1 — app «creators» (equipo Content-oh). Las reglas son las mismas que
11
+ usa hoy `useCreatorsProductEdition`, copiadas tal cual:
12
+
13
+ Rol 1 Administrador todo
14
+ Rol 4 Coordinador de textos evalúa lo que está en IE (información enviada)
15
+ Rol 5 Coordinador de imágenes evalúa lo que está en IE
16
+ Rol 6 Auditor evalúa lo que está en AC, RP, RCA, SAC
17
+ Rol 7 Especialista de textos produce; también valida lo que está en AC
18
+ Rol 8 Especialista de imágenes produce; también valida lo que está en AC
19
+
20
+ Además del rol cuenta la ASIGNACIÓN del producto: `id_<servicio>_especialist`,
21
+ `id_<servicio>_facilitator` (coordinador) e `id_auditor`. Quien está asignado
22
+ a un servicio puede trabajarlo aunque su rol sea otro.
23
+
24
+ FASE 2 — app «management» (proveedor y cadena) se agrega aquí mismo.
25
+ */
26
+
27
+ /** Servicios que hoy tienen editor y su pestaña en la edición. */
28
+ export const CLASSIC_SERVICES = [
29
+ { service_name: "datasheet", tab: "Ficha técnica" },
30
+ { service_name: "description", tab: "Descripción" },
31
+ { service_name: "images", tab: "Imágenes" },
32
+ ];
33
+
34
+ export const tabOf = (serviceName) =>
35
+ CLASSIC_SERVICES.find((item) => item.service_name === serviceName)?.tab ?? null;
36
+
37
+ export const serviceOfTab = (tab) =>
38
+ CLASSIC_SERVICES.find((item) => item.tab === tab)?.service_name ?? null;
39
+
40
+ /** Estatus base: «RA/AS» → «AS» (la parte vigente es la última). */
41
+ export const currentStatus = (status) => String(status ?? "").replace(/.*\//, "");
42
+
43
+ const roleOf = (user) => Number(user?.id_role);
44
+
45
+ export const isAdmin = (user) => roleOf(user) === 1;
46
+
47
+ export const isAssignedAs = (product, user, role, serviceName) => {
48
+ const assigned = product?.[`id_${serviceName}_${role}`];
49
+ return Boolean(assigned) && assigned === user?.id_user;
50
+ };
51
+
52
+ export const isAuditorAssigned = (product, user) =>
53
+ Boolean(product?.id_auditor) && product.id_auditor === user?.id_user;
54
+
55
+ /**
56
+ * Trabaja el servicio: administrador, o especialista/coordinador asignado con
57
+ * un rol del equipo. Igual que `isUserAssignedToService` del hook.
58
+ */
59
+ export const isAssignedToService = (product, user, serviceName, role) => {
60
+ if (![1, 4, 5, 6, 7, 8].includes(roleOf(user))) return false;
61
+ if (isAdmin(user)) return true;
62
+ return role
63
+ ? isAssignedAs(product, user, role, serviceName)
64
+ : isAssignedAs(product, user, "especialist", serviceName) ||
65
+ isAssignedAs(product, user, "facilitator", serviceName);
66
+ };
67
+
68
+ /** Ve el botón de guardar: el auditor asignado o quien trabaja el servicio. */
69
+ export const canSave = (product, user, serviceName) =>
70
+ isAuditorAssigned(product, user) || isAssignedToService(product, user, serviceName);
71
+
72
+ /** Puede asignar personas: todos menos los especialistas (roles 7 y 8). */
73
+ export const canAssign = (user) => ![7, 8].includes(roleOf(user));
74
+
75
+ /** Envía su trabajo al coordinador: los especialistas. */
76
+ export const canSendToFacilitator = (user) => [7, 8].includes(roleOf(user));
77
+
78
+ /** Ve «Cambio de estatus»: una lista fija de cuentas (constants.js). */
79
+ export const canChangeStatus = (user, allowedAccounts = []) => allowedAccounts.includes(user?.email);
80
+
81
+ /**
82
+ * Puede validar o rechazar el servicio en la cadena activa. Misma regla que
83
+ * `canShowSingleEvaluationButtons` del hook.
84
+ */
85
+ export const canEvaluate = (product, user, serviceName, retailerStatus) => {
86
+ const status = currentStatus(retailerStatus);
87
+ const role = roleOf(user);
88
+ const asFacilitator =
89
+ status === "IE" && ([1, 4, 5].includes(role) || isAssignedAs(product, user, "facilitator", serviceName));
90
+ const asAuditor =
91
+ ["AC", "RP", "RCA", "SAC"].includes(status) && ([1, 6].includes(role) || isAuditorAssigned(product, user));
92
+ const asSpecialist = status === "AC" && [7, 8].includes(role);
93
+ return asFacilitator || asAuditor || asSpecialist;
94
+ };
95
+
96
+ /* ------------------------------------------------------------------------ */
97
+ /* Lista lateral */
98
+ /* ------------------------------------------------------------------------ */
99
+
100
+ /*
101
+ «Te toca a ti»: el estatus del servicio está en una etapa que le
102
+ corresponde a alguno de los papeles que la persona tiene asignados en él.
103
+ Un estatus compuesto («RA/AS») cuenta por cualquiera de sus partes.
104
+
105
+ especialista asignado, capturando o rechazado por coordinador/auditor
106
+ coordinador por asignar o con información enviada (IE) para revisar
107
+ auditor lo mismo que puede evaluar: AC, RP, RCA, SAC
108
+ */
109
+ const ACTIONABLE = {
110
+ especialist: ["AS", "CA", "RC", "RA"],
111
+ facilitator: ["PA", "IE"],
112
+ auditor: ["AC", "RP", "RCA", "SAC"],
113
+ };
114
+
115
+ export const rolesInService = (product, user, serviceName) => {
116
+ const roles = [];
117
+ if (isAssignedAs(product, user, "especialist", serviceName)) roles.push("especialist");
118
+ if (isAssignedAs(product, user, "facilitator", serviceName)) roles.push("facilitator");
119
+ if (isAuditorAssigned(product, user)) roles.push("auditor");
120
+ return roles;
121
+ };
122
+
123
+ export const isPendingFor = (product, user, serviceName, status) => {
124
+ const parts = String(status ?? "").split("/");
125
+ return rolesInService(product, user, serviceName).some((role) =>
126
+ parts.some((part) => ACTIONABLE[role].includes(part))
127
+ );
128
+ };
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Matriz de permisos de la edición (fase 1, app «creators»).
3
+ *
4
+ * La librería no tiene corredor de pruebas propio; se corre con
5
+ * npx vitest run src/components/pages/ProductEdition
6
+ *
7
+ * Cada caso fija una regla tal como funciona hoy en
8
+ * `useCreatorsProductEdition`: si una prueba falla, cambió quién puede hacer
9
+ * qué, y eso hay que decidirlo a propósito.
10
+ */
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ canAssign,
14
+ canChangeStatus,
15
+ canEvaluate,
16
+ canSave,
17
+ canSendToFacilitator,
18
+ currentStatus,
19
+ isAssignedToService,
20
+ isPendingFor,
21
+ serviceOfTab,
22
+ tabOf,
23
+ } from "./permissions";
24
+ import { buildEditionServices } from "./services";
25
+
26
+ const product = {
27
+ datasheet_status: "AS",
28
+ description_status: "RA/AS",
29
+ images_status: "NS",
30
+ id_datasheet_especialist: 426,
31
+ id_datasheet_facilitator: 430,
32
+ id_description_especialist: 426,
33
+ id_description_facilitator: 430,
34
+ id_images_especialist: 427,
35
+ id_auditor: 425,
36
+ };
37
+
38
+ const user = (id_user, id_role, extra = {}) => ({ id_user, id_role, ...extra });
39
+ const admin = user(1, 1);
40
+ const textSpecialist = user(426, 7);
41
+ const imageSpecialist = user(427, 8);
42
+ const coordinator = user(430, 4);
43
+ const unassignedCoordinator = user(431, 5);
44
+ const auditor = user(425, 6);
45
+ const unassignedAuditor = user(432, 6);
46
+
47
+ describe("pestañas y estatus", () => {
48
+ it("relaciona servicio y pestaña en los dos sentidos", () => {
49
+ expect(tabOf("datasheet")).toBe("Ficha técnica");
50
+ expect(serviceOfTab("Imágenes")).toBe("images");
51
+ expect(tabOf("translation")).toBeNull();
52
+ });
53
+
54
+ it("toma la parte vigente de un estatus compuesto", () => {
55
+ expect(currentStatus("RA/AS")).toBe("AS");
56
+ expect(currentStatus("AC")).toBe("AC");
57
+ expect(currentStatus(null)).toBe("");
58
+ });
59
+ });
60
+
61
+ describe("trabajar y guardar un servicio", () => {
62
+ it("el administrador puede en todos", () => {
63
+ expect(canSave(product, admin, "images")).toBe(true);
64
+ });
65
+
66
+ it("el especialista solo en los servicios que tiene asignados", () => {
67
+ expect(canSave(product, textSpecialist, "datasheet")).toBe(true);
68
+ expect(canSave(product, textSpecialist, "images")).toBe(false);
69
+ expect(canSave(product, imageSpecialist, "images")).toBe(true);
70
+ });
71
+
72
+ it("el coordinador asignado puede; el que no está asignado, no", () => {
73
+ expect(canSave(product, coordinator, "description")).toBe(true);
74
+ expect(canSave(product, unassignedCoordinator, "description")).toBe(false);
75
+ });
76
+
77
+ it("el auditor asignado al producto puede en todos sus servicios", () => {
78
+ expect(canSave(product, auditor, "images")).toBe(true);
79
+ expect(canSave(product, unassignedAuditor, "images")).toBe(false);
80
+ });
81
+
82
+ it("un rol fuera del equipo no trabaja servicios aunque coincida el id", () => {
83
+ expect(isAssignedToService(product, user(426, 3), "datasheet")).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe("asignar, enviar al coordinador y cambiar estatus", () => {
88
+ it("asignan todos menos los especialistas", () => {
89
+ expect(canAssign(admin)).toBe(true);
90
+ expect(canAssign(coordinator)).toBe(true);
91
+ expect(canAssign(auditor)).toBe(true);
92
+ expect(canAssign(textSpecialist)).toBe(false);
93
+ expect(canAssign(imageSpecialist)).toBe(false);
94
+ });
95
+
96
+ it("envían al coordinador solo los especialistas", () => {
97
+ expect(canSendToFacilitator(textSpecialist)).toBe(true);
98
+ expect(canSendToFacilitator(coordinator)).toBe(false);
99
+ });
100
+
101
+ it("«Cambio de estatus» depende de la lista de cuentas", () => {
102
+ const allowed = ["oserna@contentoh.com"];
103
+ expect(canChangeStatus(user(1, 1, { email: "oserna@contentoh.com" }), allowed)).toBe(true);
104
+ expect(canChangeStatus(user(1, 1, { email: "otra@contentoh.com" }), allowed)).toBe(false);
105
+ });
106
+ });
107
+
108
+ describe("validar o rechazar en la cadena activa", () => {
109
+ it("IE: coordinadores por rol o el coordinador asignado", () => {
110
+ expect(canEvaluate(product, unassignedCoordinator, "datasheet", "IE")).toBe(true);
111
+ expect(canEvaluate(product, admin, "datasheet", "IE")).toBe(true);
112
+ expect(canEvaluate(product, auditor, "datasheet", "IE")).toBe(false);
113
+ expect(canEvaluate(product, textSpecialist, "datasheet", "IE")).toBe(false);
114
+ });
115
+
116
+ it("AC, RP, RCA y SAC: auditor por rol o asignado", () => {
117
+ ["AC", "RP", "RCA", "SAC"].forEach((status) => {
118
+ expect(canEvaluate(product, unassignedAuditor, "images", status)).toBe(true);
119
+ });
120
+ expect(canEvaluate(product, coordinator, "images", "RP")).toBe(false);
121
+ });
122
+
123
+ it("AC: también los especialistas", () => {
124
+ expect(canEvaluate(product, textSpecialist, "datasheet", "AC")).toBe(true);
125
+ expect(canEvaluate(product, imageSpecialist, "images", "AC")).toBe(true);
126
+ });
127
+
128
+ it("en captura nadie evalúa, salvo que la parte vigente lo permita", () => {
129
+ expect(canEvaluate(product, admin, "datasheet", "CA")).toBe(false);
130
+ expect(canEvaluate(product, auditor, "datasheet", "RA/AC")).toBe(true);
131
+ });
132
+ });
133
+
134
+ describe("«te toca a ti» en la lista lateral", () => {
135
+ it("al especialista le toca lo asignado, en captura o rechazado", () => {
136
+ expect(isPendingFor(product, textSpecialist, "datasheet", "AS")).toBe(true);
137
+ expect(isPendingFor(product, textSpecialist, "description", "RA/AS")).toBe(true);
138
+ expect(isPendingFor(product, textSpecialist, "datasheet", "AC")).toBe(false);
139
+ });
140
+
141
+ it("al coordinador asignado, lo que está por asignar o en IE", () => {
142
+ expect(isPendingFor(product, coordinator, "datasheet", "IE")).toBe(true);
143
+ expect(isPendingFor(product, coordinator, "datasheet", "CA")).toBe(false);
144
+ });
145
+
146
+ it("al auditor asignado, lo que puede evaluar", () => {
147
+ expect(isPendingFor(product, auditor, "images", "AC")).toBe(true);
148
+ expect(isPendingFor(product, auditor, "images", "AS")).toBe(false);
149
+ });
150
+
151
+ it("a quien no está asignado nunca le toca, aunque su rol pueda evaluar", () => {
152
+ expect(isPendingFor(product, unassignedAuditor, "images", "AC")).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe("servicios de la lista lateral", () => {
157
+ const servicesData = [
158
+ { service: "datasheet", id_retailer: 4, status: "CA" },
159
+ { service: "datasheet", id_retailer: 9, status: "AC" },
160
+ ];
161
+
162
+ it("solo aparecen los servicios contratados", () => {
163
+ const services = buildEditionServices({ product, user: textSpecialist, servicesData, activeRetailerId: 4 });
164
+ expect(services.map((item) => item.service_name)).toEqual(["datasheet", "description"]);
165
+ });
166
+
167
+ it("el estatus es el de la cadena activa, con el del producto de respaldo", () => {
168
+ const at4 = buildEditionServices({ product, user: textSpecialist, servicesData, activeRetailerId: 4 });
169
+ const at9 = buildEditionServices({ product, user: textSpecialist, servicesData, activeRetailerId: 9 });
170
+ expect(at4[0].status).toBe("CA");
171
+ expect(at9[0].status).toBe("AC");
172
+ expect(at4[1].status).toBe("RA/AS");
173
+ });
174
+
175
+ it("marca los servicios de la persona y lo que le toca", () => {
176
+ const services = buildEditionServices({ product, user: imageSpecialist, servicesData, activeRetailerId: 4 });
177
+ expect(services.every((item) => item.mine === false)).toBe(true);
178
+ const mine = buildEditionServices({ product, user: textSpecialist, servicesData, activeRetailerId: 4 });
179
+ expect(mine.map((item) => [item.mine, item.pending])).toEqual([
180
+ [true, true],
181
+ [true, true],
182
+ ]);
183
+ });
184
+
185
+ it("agrega los servicios de ejemplo al final, marcados como tales", () => {
186
+ const services = buildEditionServices({
187
+ product,
188
+ user: admin,
189
+ servicesData,
190
+ activeRetailerId: 4,
191
+ demoServices: [{ service_name: "translation", status: "CA" }],
192
+ });
193
+ const demo = services[services.length - 1];
194
+ expect(demo).toMatchObject({ service_name: "translation", display_name: "Traducción", is_demo: true, tab: null });
195
+ });
196
+
197
+ it("ignora los servicios de muestra que traiga el producto (?demoServicios=1)", () => {
198
+ const services = buildEditionServices({
199
+ product: { ...product, demo_services: [{ service_name: "seo", status: "AS" }] },
200
+ user: admin,
201
+ servicesData,
202
+ activeRetailerId: 4,
203
+ });
204
+ expect(services.some((item) => item.is_demo)).toBe(false);
205
+ });
206
+
207
+ it("los de ejemplo son del auditor del producto y de nadie más", () => {
208
+ const demoServices = [{ service_name: "seo", status: "AS" }];
209
+ const forAuditor = buildEditionServices({ product, user: auditor, servicesData, demoServices });
210
+ const forSpecialist = buildEditionServices({ product, user: textSpecialist, servicesData, demoServices });
211
+ expect(forAuditor.every((item) => item.mine)).toBe(true);
212
+ expect(forAuditor.at(-1).assignees.auditor).toMatchObject({ id_user: 425 });
213
+ expect(forSpecialist.at(-1).mine).toBe(false);
214
+ });
215
+
216
+ it("sin producto no hay servicios", () => {
217
+ expect(buildEditionServices({ product: null, user: admin })).toEqual([]);
218
+ });
219
+ });
220
+
221
+ describe("servicios mientras carga la edición", () => {
222
+ it("no truena con services_data y asignaciones en null", () => {
223
+ const services = buildEditionServices({
224
+ product,
225
+ user: textSpecialist,
226
+ servicesData: null,
227
+ activeRetailerId: undefined,
228
+ collaboratorAssignations: null,
229
+ });
230
+ expect(services.map((item) => item.status)).toEqual(["AS", "RA/AS"]);
231
+ });
232
+ });
233
+
234
+ describe("equipo del servicio", () => {
235
+ it("le pone nombre al auditor con la lista de auditores", () => {
236
+ const [first] = buildEditionServices({
237
+ product,
238
+ user: textSpecialist,
239
+ servicesData: [],
240
+ auditors: [{ id_user: 425, name: "Auditor QA", last_name: null }],
241
+ });
242
+ expect(first.assignees.auditor).toMatchObject({ id_user: 425, name: "Auditor QA" });
243
+ });
244
+
245
+ it("sin la lista, el auditor queda como «Asignado»", () => {
246
+ const [first] = buildEditionServices({ product, user: textSpecialist, servicesData: [] });
247
+ expect(first.assignees.auditor).toMatchObject({ id_user: 425, name: "Asignado" });
248
+ });
249
+ });