contentoh-components-library 21.6.57 → 21.6.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
- package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
- package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
- package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
- package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
- package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
- package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
- package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
- package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
- package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
- package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
- package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +2 -1
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +325 -10
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -2
- package/dist/components/pages/ProviderProductEdition/index.js +486 -231
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
- package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
- package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
- package/src/components/pages/CreatorsProductEdition/index.js +19 -0
- package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
- package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
- package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
- package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
- package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
- package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
- package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
- package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
- package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
- package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
- package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
- package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
- package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
- package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
- package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
- package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
- package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
- package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
- package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
- package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +1 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +201 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -0
- package/src/components/pages/ProviderProductEdition/index.js +228 -7
- package/src/index.js +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProviderProductEditionProvider } from "../RetailerProductEdition/context/provider-product-edition.context";
|
|
2
|
+
import { CreatorsProductEditionView } from "./CreatorsProductEditionView";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Edición de producto de Creators, responsiva (celular, tablet y desktop).
|
|
6
|
+
*
|
|
7
|
+
* Reemplazo de `RetailerProductEdition` con los mismos props:
|
|
8
|
+
* `tabsSections`, `productSelected`, `user`, `token`, `location` y
|
|
9
|
+
* `enableDigitalAssets`. Comparte el contexto (reducer y guardados) con la vista
|
|
10
|
+
* original; la lógica del componente es una copia en
|
|
11
|
+
* `hooks/useCreatorsProductEdition.js`.
|
|
12
|
+
*/
|
|
13
|
+
export const CreatorsProductEdition = (props) => {
|
|
14
|
+
return (
|
|
15
|
+
<ProviderProductEditionProvider>
|
|
16
|
+
<CreatorsProductEditionView {...props} />
|
|
17
|
+
</ProviderProductEditionProvider>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Respuestas de la API para las historias de CreatorsProductEdition.
|
|
3
|
+
|
|
4
|
+
Capturadas de dev (producto 144814, orden 10421, versión 3) el 2026-09-28 y
|
|
5
|
+
recortadas: la lista de tomas de imagen se dejó en seis. Los nombres del
|
|
6
|
+
personal se reemplazaron por genéricos: esta librería se publica en npm.
|
|
7
|
+
|
|
8
|
+
Cada respuesta conserva la envoltura que devuelve su lambda —unas mandan
|
|
9
|
+
`{ statusCode, body: "<json>" }` y otras `{ data }` directo—, porque el código
|
|
10
|
+
de la vista depende de esa forma.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const ARTICLE_ID = 144814;
|
|
14
|
+
const VERSION = 3;
|
|
15
|
+
|
|
16
|
+
/** Envoltura de `integration: lambda`: el cuerpo viaja como cadena. */
|
|
17
|
+
const lambda = (body) => ({
|
|
18
|
+
statusCode: 200,
|
|
19
|
+
headers: { "Access-Control-Allow-Origin": "*" },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const RETAILERS = {
|
|
24
|
+
36: { id: 36, name: "Oxxo", country: "México", id_region: 1, is_active: 1, active: 1 },
|
|
25
|
+
38: { id: 38, name: "Del Sol", country: "México", id_region: 1, is_active: 1, active: 1 },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const comment = (id, concept, message) => ({
|
|
29
|
+
id,
|
|
30
|
+
user_id: 425,
|
|
31
|
+
article_id: ARTICLE_ID,
|
|
32
|
+
order_id: 10421,
|
|
33
|
+
message,
|
|
34
|
+
timestamp: "2025-11-15T21:12:00.000Z",
|
|
35
|
+
completed: 0,
|
|
36
|
+
version_id: VERSION,
|
|
37
|
+
concept,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const descriptionInputs = [
|
|
41
|
+
{
|
|
42
|
+
id_version: VERSION,
|
|
43
|
+
id: 4592,
|
|
44
|
+
id_retailer: 38,
|
|
45
|
+
name: "Header Del Sol",
|
|
46
|
+
id_description_type: null,
|
|
47
|
+
description:
|
|
48
|
+
"Encabezado: Nombre genérico del producto + Marca + Sub Marca (Opcional) + Modelo / Estilo / Tipo / Talla / Color / Capacidad / Tamaño / Edad Mínimo 40 Máximo 70 Caracteres",
|
|
49
|
+
min_words: null,
|
|
50
|
+
max_words: null,
|
|
51
|
+
min_chars: 40,
|
|
52
|
+
max_chars: 70,
|
|
53
|
+
type: "Alfanumérico",
|
|
54
|
+
active: 1,
|
|
55
|
+
required: true,
|
|
56
|
+
value: "Yoghurt Danone Frutas Selectas con trozos de fresa 120 g",
|
|
57
|
+
ai_generated: false,
|
|
58
|
+
isOverridden: false,
|
|
59
|
+
inheritable: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id_version: VERSION,
|
|
63
|
+
id: 4593,
|
|
64
|
+
id_retailer: 38,
|
|
65
|
+
name: "Descripción Comercial del Producto Del Sol",
|
|
66
|
+
id_description_type: null,
|
|
67
|
+
description: "Descripción Comercial",
|
|
68
|
+
min_words: null,
|
|
69
|
+
max_words: null,
|
|
70
|
+
min_chars: null,
|
|
71
|
+
max_chars: 1200,
|
|
72
|
+
type: "Alfanumérico",
|
|
73
|
+
active: 1,
|
|
74
|
+
required: true,
|
|
75
|
+
value:
|
|
76
|
+
"Yoghurt batido con trozos de fresa, ahora con más fruta y una textura más cremosa. Ideal para el desayuno o como colación.",
|
|
77
|
+
ai_generated: false,
|
|
78
|
+
isOverridden: false,
|
|
79
|
+
inheritable: true,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const service = (id_retailer, serviceName, status, task_user_group_id) => ({
|
|
84
|
+
id_order: 10421,
|
|
85
|
+
id_retailer,
|
|
86
|
+
id_article: ARTICLE_ID,
|
|
87
|
+
service: serviceName,
|
|
88
|
+
quantity: 1,
|
|
89
|
+
price: 0,
|
|
90
|
+
id_user: 1989,
|
|
91
|
+
datasheet_common: null,
|
|
92
|
+
discount: null,
|
|
93
|
+
status,
|
|
94
|
+
task_user_group_id,
|
|
95
|
+
is_billing: 0,
|
|
96
|
+
last_modified: "2025-11-15T21:10:26.000Z",
|
|
97
|
+
is_active: 1,
|
|
98
|
+
deleted_by: null,
|
|
99
|
+
deleted_at: null,
|
|
100
|
+
final_version_id: null,
|
|
101
|
+
retailerToProvider: null,
|
|
102
|
+
is_retailer: 0,
|
|
103
|
+
retailersId: null,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Tomas de imagen: seis de las 36 que devuelve dev, con su orden por cadena.
|
|
107
|
+
const IMAGE_INPUTS = [
|
|
108
|
+
{ id: 1, name: "Central (frente comercial)", required: 1 },
|
|
109
|
+
{ id: 11, name: "Hero image", required: 0 },
|
|
110
|
+
{ id: 3, name: "Trasera (back)", required: 0 },
|
|
111
|
+
{ id: 2, name: "Fuera del empaque / Abierto interior", required: 0 },
|
|
112
|
+
{ id: 12, name: "Tabla nutrimental / ingredientes", required: 0 },
|
|
113
|
+
{ id: 15, name: "Zoom", required: 0 },
|
|
114
|
+
].map((input) => ({
|
|
115
|
+
vertical_dpi: null,
|
|
116
|
+
horizontal_dpi: null,
|
|
117
|
+
vertical_px: null,
|
|
118
|
+
horizontal_px: null,
|
|
119
|
+
...input,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const imageValue = (id, id_image, src, size) => ({
|
|
123
|
+
id,
|
|
124
|
+
status: null,
|
|
125
|
+
id_image,
|
|
126
|
+
packing_type: null,
|
|
127
|
+
image_type: null,
|
|
128
|
+
id_article: ARTICLE_ID,
|
|
129
|
+
src: `id-${ARTICLE_ID}/${VERSION}/${src}`,
|
|
130
|
+
timestamp: "2026-02-11T19:12:38.000Z",
|
|
131
|
+
id_version: VERSION,
|
|
132
|
+
width: size,
|
|
133
|
+
height: size,
|
|
134
|
+
scene_type: null,
|
|
135
|
+
image_shot_type: null,
|
|
136
|
+
last_modified: "2026-08-25T14:31:27.000Z",
|
|
137
|
+
has_leyend: 0,
|
|
138
|
+
onb_status: 1,
|
|
139
|
+
onb_comments: "",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const upcAttribute = {
|
|
143
|
+
id: 683,
|
|
144
|
+
name: "UPC",
|
|
145
|
+
is_datasheet: 1,
|
|
146
|
+
is_new_attribute: 0,
|
|
147
|
+
last_modified_by: 275,
|
|
148
|
+
created_at: "2026-07-01T13:56:44.000Z",
|
|
149
|
+
updated_at: "2026-07-01T13:56:44.000Z",
|
|
150
|
+
is_active: 1,
|
|
151
|
+
value: "400080029609",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const users = (role, count) => ({
|
|
155
|
+
data: Array.from({ length: count }, (_, i) => ({
|
|
156
|
+
name: `${role} ${i + 1}`,
|
|
157
|
+
last_name: null,
|
|
158
|
+
id_user: 9000 + i,
|
|
159
|
+
})),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const member = (articleId, name, upc, isCurrent = false) => ({ articleId, name, upc, isCurrent });
|
|
163
|
+
|
|
164
|
+
const buildFamily = (familia) => {
|
|
165
|
+
if (familia === "padre") {
|
|
166
|
+
return {
|
|
167
|
+
articleId: ARTICLE_ID,
|
|
168
|
+
isVariant: false,
|
|
169
|
+
parent: null,
|
|
170
|
+
variants: [
|
|
171
|
+
member(144821, "Yoghurt Frutas Selectas durazno 120 g", "400080029630"),
|
|
172
|
+
member(144822, "Yoghurt Frutas Selectas mango 120 g con un nombre bastante largo", "400080029647"),
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (familia === "hijo") {
|
|
177
|
+
return {
|
|
178
|
+
articleId: ARTICLE_ID,
|
|
179
|
+
isVariant: true,
|
|
180
|
+
parent: member(144800, "Yoghurt Frutas Selectas (línea)", "400080029500"),
|
|
181
|
+
variants: [
|
|
182
|
+
member(ARTICLE_ID, "Producto 300080", "400080029609", true),
|
|
183
|
+
member(144821, "Yoghurt Frutas Selectas durazno 120 g", "400080029630"),
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { articleId: ARTICLE_ID, isVariant: false, parent: null, variants: [] };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Crea las respuestas.
|
|
192
|
+
* - `images: false` simula un producto sin imágenes.
|
|
193
|
+
* - `delSolStatus` cambia el estatus de los tres servicios de Del Sol; con
|
|
194
|
+
* "AC" el auditor asignado puede validar o rechazar.
|
|
195
|
+
* - `familia`: "padre" (este producto con dos hijos) o "hijo" (este producto
|
|
196
|
+
* es hijo de otro, con un hermano).
|
|
197
|
+
* - `grupo`: el producto está en un grupo de variantes con otros dos.
|
|
198
|
+
*/
|
|
199
|
+
export const buildFixtures = ({ images = true, delSolStatus, familia, grupo = false } = {}) => ({
|
|
200
|
+
comments: {
|
|
201
|
+
description: lambda({ message: "OK", data: [comment(15886, "description", "Revisar la longitud del header.")], order: null }),
|
|
202
|
+
datasheet: lambda({ message: "OK", data: [comment(15885, "datasheet", "Rechazo")], order: null }),
|
|
203
|
+
images: lambda({ message: "OK", data: [], order: null }),
|
|
204
|
+
},
|
|
205
|
+
datasheets: lambda({
|
|
206
|
+
message: "ok",
|
|
207
|
+
data: {
|
|
208
|
+
36: { retailer: RETAILERS[36], data: {} },
|
|
209
|
+
38: {
|
|
210
|
+
retailer: RETAILERS[38],
|
|
211
|
+
data: { 6: { groupId: "6", dataGroup: "Información Básica del Producto", inputs: ["683_38"] } },
|
|
212
|
+
},
|
|
213
|
+
inputs: {
|
|
214
|
+
"683_38": {
|
|
215
|
+
id: 683,
|
|
216
|
+
name: "UPC",
|
|
217
|
+
min_chars: null,
|
|
218
|
+
max_chars: 14,
|
|
219
|
+
type: "Numérico",
|
|
220
|
+
id_group: 6,
|
|
221
|
+
required: true,
|
|
222
|
+
description: "Código de Barras del Producto, GTIN, UPC",
|
|
223
|
+
option_list: null,
|
|
224
|
+
value: "400080029609",
|
|
225
|
+
onb_status: 1,
|
|
226
|
+
onb_comments: null,
|
|
227
|
+
ai_generated: 0,
|
|
228
|
+
isOverridden: false,
|
|
229
|
+
inheritable: false,
|
|
230
|
+
active: 1,
|
|
231
|
+
commercial_model: null,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
variant: { isVariant: false, parentArticleId: null, parent: null },
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
descriptions: lambda({
|
|
238
|
+
message: "OK",
|
|
239
|
+
data: [
|
|
240
|
+
{ ...RETAILERS[36], inputs: [] },
|
|
241
|
+
{ ...RETAILERS[38], inputs: descriptionInputs },
|
|
242
|
+
],
|
|
243
|
+
inputsGeneral: Object.fromEntries(descriptionInputs.map((input) => [input.id, input])),
|
|
244
|
+
variant: { isVariant: false, parentArticleId: null, parent: null },
|
|
245
|
+
}),
|
|
246
|
+
users: {
|
|
247
|
+
4: { data: [{ name: "Coordinador Textos", last_name: null, id_user: 423 }] },
|
|
248
|
+
5: { data: [{ name: "Coordinador Imágenes", last_name: null, id_user: 424 }] },
|
|
249
|
+
6: { data: [{ name: "Auditor QA", last_name: null, id_user: 425 }, ...users("Auditor", 2).data] },
|
|
250
|
+
7: { data: [{ name: "Especialista Textos", last_name: null, id_user: 426 }, ...users("Especialista textos", 3).data] },
|
|
251
|
+
8: { data: [{ name: "Especialista Edición", last_name: null, id_user: 427 }, ...users("Especialista edición", 3).data] },
|
|
252
|
+
},
|
|
253
|
+
family: lambda(buildFamily(familia)),
|
|
254
|
+
group: lambda(
|
|
255
|
+
grupo
|
|
256
|
+
? {
|
|
257
|
+
articleId: ARTICLE_ID,
|
|
258
|
+
group: "YOG-FRESA",
|
|
259
|
+
members: [
|
|
260
|
+
member(ARTICLE_ID, "Producto 300080", "400080029609", true),
|
|
261
|
+
member(144815, "Yoghurt Frutas Selectas fresa 4 pack", "400080029616"),
|
|
262
|
+
member(144816, "Yoghurt Frutas Selectas fresa 1 kg", "400080029623"),
|
|
263
|
+
],
|
|
264
|
+
}
|
|
265
|
+
: { articleId: ARTICLE_ID, group: null, members: [] },
|
|
266
|
+
),
|
|
267
|
+
services: lambda({
|
|
268
|
+
message: "OK",
|
|
269
|
+
data: [
|
|
270
|
+
service(36, "datasheet", "RA/AS", 2),
|
|
271
|
+
service(36, "description", "RA/AS", 2),
|
|
272
|
+
service(36, "images", "AA", 5),
|
|
273
|
+
service(38, "datasheet", delSolStatus ?? "RA/AS", 2),
|
|
274
|
+
service(38, "description", delSolStatus ?? "RA/AS", 2),
|
|
275
|
+
service(38, "images", delSolStatus ?? "AA", 5),
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
percentages: lambda([
|
|
279
|
+
{
|
|
280
|
+
id_article: ARTICLE_ID,
|
|
281
|
+
version: VERSION,
|
|
282
|
+
relations: [
|
|
283
|
+
{
|
|
284
|
+
id_retailer: 38,
|
|
285
|
+
id_category: 3513,
|
|
286
|
+
datasheet: { required: 100, general: 100 },
|
|
287
|
+
descriptions: { required: 100, general: 100 },
|
|
288
|
+
images: {
|
|
289
|
+
required: 100,
|
|
290
|
+
general: 80.01,
|
|
291
|
+
details: {
|
|
292
|
+
mandatoryShots: { total: 1, completed: 1, percentage: 100 },
|
|
293
|
+
optionalShots: { total: 5, completed: 2, percentage: 40 },
|
|
294
|
+
namingAttributes: { total: 1, completed: 1, percentage: 100 },
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
percentagesGeneral: { required: 100, general: 69.43 },
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
summary: { totalRelations: 1, averageRequired: 100, averageGeneral: 69.43 },
|
|
301
|
+
},
|
|
302
|
+
]),
|
|
303
|
+
images: lambda({
|
|
304
|
+
message: "OK",
|
|
305
|
+
data: {
|
|
306
|
+
retailers: [{ ...RETAILERS[38], min: 2, max: 5 }],
|
|
307
|
+
attrForImgs: { 38: [upcAttribute], general: [upcAttribute] },
|
|
308
|
+
imagePackagingType: [
|
|
309
|
+
{ id: 1, name: "Embalaje adicional", deliverable_value: "0", status: 1 },
|
|
310
|
+
{ id: 2, name: "En paquete", deliverable_value: "1", status: 1 },
|
|
311
|
+
{ id: 3, name: "Producto expuesto", deliverable_value: "2", status: 1 },
|
|
312
|
+
{ id: 4, name: "Embalaje multiforme", deliverable_value: "3", status: 1 },
|
|
313
|
+
{ id: 5, name: "Si no es una imagen / no corresponde", deliverable_value: "X", status: 1 },
|
|
314
|
+
],
|
|
315
|
+
imageType: [
|
|
316
|
+
{ id: 1, name: "Fotografía del producto", deliverable_value: "A", status: 1 },
|
|
317
|
+
{ id: 2, name: "Render", deliverable_value: "R", status: 1 },
|
|
318
|
+
],
|
|
319
|
+
imageShotType: [
|
|
320
|
+
{ id: 1, type_shot: "SKU", status: 1 },
|
|
321
|
+
{ id: 2, type_shot: "SKU.1", status: 1 },
|
|
322
|
+
{ id: 3, type_shot: "SKU.2", status: 1 },
|
|
323
|
+
],
|
|
324
|
+
inputs: IMAGE_INPUTS,
|
|
325
|
+
retailerMandatories: [
|
|
326
|
+
[{ id_retailer: 38, id_category: 3506, id_image: 1, order: 1, priority: 1, mandatory: 1 }],
|
|
327
|
+
],
|
|
328
|
+
inputsByRetailer: [
|
|
329
|
+
IMAGE_INPUTS.map((input, index) => ({
|
|
330
|
+
id_retailer: 38,
|
|
331
|
+
id_category: 3513,
|
|
332
|
+
id_image: input.id,
|
|
333
|
+
order: index + 1,
|
|
334
|
+
priority: 1,
|
|
335
|
+
mandatory: input.required,
|
|
336
|
+
})),
|
|
337
|
+
],
|
|
338
|
+
values: images
|
|
339
|
+
? [
|
|
340
|
+
imageValue(1070155, 1, "1-4f5e3461-395f-4a66-b676-c0a4fb9a3069.jpg", 250),
|
|
341
|
+
imageValue(1070366, 11, "11-b522d019-e845-4921-b28f-5ce4e3c135ba.jpg", 3000),
|
|
342
|
+
imageValue(1070367, 3, "3-37a15352-c6e9-47db-a0fa-ca277c571b1c.jpg", 3000),
|
|
343
|
+
]
|
|
344
|
+
: [],
|
|
345
|
+
assets: [],
|
|
346
|
+
},
|
|
347
|
+
retailerWithoutInputs: [],
|
|
348
|
+
}),
|
|
349
|
+
// Historial de versiones del producto (`REACT_APP_VERSIONS_ENDPOINT`).
|
|
350
|
+
versions: lambda({
|
|
351
|
+
data: [
|
|
352
|
+
{ version: 1, article_status: null, timestamp: null },
|
|
353
|
+
{ version: 2, article_status: "AP", timestamp: "2026-05-14T17:20:00.000Z" },
|
|
354
|
+
{ version: VERSION, article_status: null, timestamp: "2026-08-02T15:05:00.000Z" },
|
|
355
|
+
],
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
4
|
+
import { buildFixtures } from "./api.fixtures";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
API falsa para las historias: reemplaza el adaptador de axios mientras la
|
|
8
|
+
historia está montada. Toda la pantalla (contexto, utils, modales) usa la
|
|
9
|
+
instancia por omisión de axios y nadie usa `fetch`, así que con esto basta y
|
|
10
|
+
no hace falta ninguna dependencia nueva.
|
|
11
|
+
|
|
12
|
+
Lo que no reconoce responde 200 vacío y lo avisa en la consola: así una
|
|
13
|
+
llamada nueva no rompe la historia y se nota qué falta simular.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const reply = (config, data, status = 200) =>
|
|
17
|
+
new Promise((resolve) =>
|
|
18
|
+
// Un poco de latencia para ver el skeleton y los estados de carga.
|
|
19
|
+
setTimeout(
|
|
20
|
+
() => resolve({ data, status, statusText: "OK", headers: {}, config, request: {} }),
|
|
21
|
+
150,
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const pathOf = (url = "") => {
|
|
26
|
+
try {
|
|
27
|
+
const { pathname, searchParams } = new URL(url, "http://mock");
|
|
28
|
+
// `/dev/articles-data/images` → `articles-data/images`
|
|
29
|
+
return { path: pathname.replace(/^\/[^/]+\//, ""), params: searchParams };
|
|
30
|
+
} catch {
|
|
31
|
+
return { path: url, params: new URLSearchParams() };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const ok = { statusCode: 200, body: JSON.stringify({ message: "OK" }) };
|
|
36
|
+
|
|
37
|
+
export const createMockAdapter = ({
|
|
38
|
+
images = true,
|
|
39
|
+
failLoad = false,
|
|
40
|
+
delSolStatus,
|
|
41
|
+
familia,
|
|
42
|
+
grupo,
|
|
43
|
+
} = {}) => {
|
|
44
|
+
const fx = buildFixtures({ images, delSolStatus, familia, grupo });
|
|
45
|
+
|
|
46
|
+
return (config) => {
|
|
47
|
+
const method = (config.method || "get").toUpperCase();
|
|
48
|
+
const { path, params } = pathOf(config.url);
|
|
49
|
+
const query = { ...Object.fromEntries(params), ...(config.params || {}) };
|
|
50
|
+
|
|
51
|
+
if (failLoad && path === "services") {
|
|
52
|
+
return reply(config, { statusCode: 500, body: JSON.stringify({ message: "Error simulado" }) }, 500).then(
|
|
53
|
+
(response) => Promise.reject(Object.assign(new Error("Request failed with status code 500"), { response })),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (method === "GET" && path === "comments") return reply(config, fx.comments[query.concept] ?? fx.comments.images);
|
|
58
|
+
if (path === "articles-data/datasheets") return reply(config, fx.datasheets);
|
|
59
|
+
if (method === "GET" && path === "articles-data/descriptions") return reply(config, fx.descriptions);
|
|
60
|
+
if (path === "articles-data/images") return reply(config, fx.images);
|
|
61
|
+
if (method === "GET" && path === "users") return reply(config, fx.users[query.fetchRole] ?? { data: [] });
|
|
62
|
+
if (path === "variants/family") return reply(config, fx.family);
|
|
63
|
+
if (path === "variants/group") return reply(config, fx.group);
|
|
64
|
+
if (method === "GET" && path === "services") return reply(config, fx.services);
|
|
65
|
+
if (method === "GET" && path === "versions") return reply(config, fx.versions);
|
|
66
|
+
if (path === "percentages") return reply(config, fx.percentages);
|
|
67
|
+
// Sin versión auditable ni observación: con la API real de dev estas dos
|
|
68
|
+
// fallan por CORS, así que la pantalla ya sabe vivir sin ellas.
|
|
69
|
+
if (method === "GET" && path === "version-manager") {
|
|
70
|
+
return reply(config, { statusCode: 200, body: JSON.stringify({ data: null }) });
|
|
71
|
+
}
|
|
72
|
+
if (method === "GET" && path === "articles-observations/read-observation") {
|
|
73
|
+
return reply(config, { statusCode: 200, body: JSON.stringify({ data: [{ observations: "" }] }) });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Guardados, evaluaciones y demás escrituras: se aceptan sin hacer nada.
|
|
77
|
+
if (method !== "GET") {
|
|
78
|
+
console.info(`[mock API] ${method} ${path}`, config.data);
|
|
79
|
+
return reply(config, ok);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.warn(`[mock API] sin simular: ${method} ${path}`, query);
|
|
83
|
+
return reply(config, { statusCode: 200, body: JSON.stringify({ message: "OK", data: [] }) });
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const realAdapter = () => {
|
|
88
|
+
if (!axios.defaults.__realAdapter) axios.defaults.__realAdapter = axios.defaults.adapter;
|
|
89
|
+
return axios.defaults.__realAdapter;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let activeMock = null;
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
Monta la API falsa ANTES que la pantalla: el adaptador se cambia durante el
|
|
96
|
+
render, y los efectos de los hijos —que son los que piden los datos— corren
|
|
97
|
+
después.
|
|
98
|
+
|
|
99
|
+
Dos detalles que no son adorno:
|
|
100
|
+
- Se aplica en CADA render, no solo al montar. Al cambiar de historia desde
|
|
101
|
+
el menú, React reutiliza este mismo componente con otras `options` (por
|
|
102
|
+
ejemplo, de "Error al cargar" a otra): si solo se aplicara al montar, la
|
|
103
|
+
historia nueva seguiría con la API de la anterior.
|
|
104
|
+
- Al desmontarse solo restaura si nadie más tomó el relevo. El render de la
|
|
105
|
+
historia nueva ocurre ANTES que la limpieza de la vieja; restaurar a ciegas
|
|
106
|
+
le quitaría la API falsa a la historia que acaba de entrar.
|
|
107
|
+
*/
|
|
108
|
+
const MockApi = ({ options, children }) => {
|
|
109
|
+
const [id] = useState(() => Symbol("mock-api"));
|
|
110
|
+
const key = JSON.stringify(options ?? {});
|
|
111
|
+
realAdapter();
|
|
112
|
+
if (activeMock?.id !== id || activeMock.key !== key) {
|
|
113
|
+
axios.defaults.adapter = createMockAdapter(options);
|
|
114
|
+
activeMock = { id, key };
|
|
115
|
+
}
|
|
116
|
+
useEffect(
|
|
117
|
+
() => () => {
|
|
118
|
+
if (activeMock?.id === id) {
|
|
119
|
+
axios.defaults.adapter = realAdapter();
|
|
120
|
+
activeMock = null;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[id],
|
|
124
|
+
);
|
|
125
|
+
return children;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Decorador de Storybook que envuelve la historia en la API falsa. La `key`
|
|
130
|
+
* obliga a remontar la pantalla cuando cambian las opciones, para que vuelva a
|
|
131
|
+
* pedir los datos con la API nueva.
|
|
132
|
+
*/
|
|
133
|
+
export const withMockApi = (options) => (Story) => (
|
|
134
|
+
<MockApi key={JSON.stringify(options ?? {})} options={options}>
|
|
135
|
+
<Story />
|
|
136
|
+
</MockApi>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/** Decorador para las historias contra dev: garantiza el adaptador real. */
|
|
140
|
+
export const withRealApi = (Story) => {
|
|
141
|
+
axios.defaults.adapter = realAdapter();
|
|
142
|
+
activeMock = null;
|
|
143
|
+
return <Story />;
|
|
144
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Box, Button, Paper, Stack, TextField, Typography } from "@mui/material";
|
|
3
|
+
|
|
4
|
+
import successIcon from "../../../../assets/images/genericModal/genericModalCheck.svg";
|
|
5
|
+
import errorIcon from "../../../../assets/images/genericModal/errorModal.svg";
|
|
6
|
+
|
|
7
|
+
import { COLORS, FONTS } from "./tokens";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Caja de comentario y botones de acción ("Cambio de Estatus", "Enviar
|
|
11
|
+
evaluación").
|
|
12
|
+
|
|
13
|
+
Reemplaza a TagAndInput (Quill) + Commentary + los botones de la vista
|
|
14
|
+
original. El comentario se guarda con el mismo `createComment` y los mismos
|
|
15
|
+
avisos que `handleOnSubmitComment`, pero leyendo el campo controlado en lugar
|
|
16
|
+
del DOM de Quill. Con eso se va un defecto del original: solo guardaba el
|
|
17
|
+
primer párrafo (`.ql-editor > p`), así que un comentario de dos párrafos
|
|
18
|
+
perdía el segundo.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const MAX_CHARS = 999;
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
Se guarda el texto tal cual, con sus saltos de línea: los comentarios se
|
|
25
|
+
pintan como texto (`white-space: pre-wrap`), no como HTML, y es el mismo
|
|
26
|
+
formato que ya usa el mensaje de rechazo (convierte los <br> de Quill a \n).
|
|
27
|
+
*/
|
|
28
|
+
const toCommentText = (text) => text.trim();
|
|
29
|
+
|
|
30
|
+
const pill = {
|
|
31
|
+
borderRadius: "30px",
|
|
32
|
+
px: "25px",
|
|
33
|
+
py: "10px",
|
|
34
|
+
minHeight: 40,
|
|
35
|
+
textTransform: "none",
|
|
36
|
+
fontSize: 18,
|
|
37
|
+
lineHeight: "22px",
|
|
38
|
+
boxShadow: "none",
|
|
39
|
+
whiteSpace: "nowrap",
|
|
40
|
+
"&:hover": { boxShadow: "none", opacity: 0.7 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CommentBox = ({
|
|
44
|
+
comment,
|
|
45
|
+
crossComment,
|
|
46
|
+
createComment,
|
|
47
|
+
token,
|
|
48
|
+
dispatch,
|
|
49
|
+
showChangeStatus,
|
|
50
|
+
onOpenChangeStatus,
|
|
51
|
+
showSendEvaluation,
|
|
52
|
+
canSendEvaluation,
|
|
53
|
+
onSendEvaluation,
|
|
54
|
+
}) => {
|
|
55
|
+
const [text, setText] = useState("");
|
|
56
|
+
const [sending, setSending] = useState(false);
|
|
57
|
+
|
|
58
|
+
const notify = (payload) => dispatch({ type: "SET_MODAL", payload: { show: true, ...payload } });
|
|
59
|
+
|
|
60
|
+
const submit = async () => {
|
|
61
|
+
if (!text.trim()) return;
|
|
62
|
+
setSending(true);
|
|
63
|
+
try {
|
|
64
|
+
await createComment(toCommentText(text), token);
|
|
65
|
+
setText("");
|
|
66
|
+
notify({ title: "", message: "Comentario guardado con éxito", image: successIcon });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Error saving comment:", error);
|
|
69
|
+
notify({ title: "Error", message: "Hubo un error al guardar el comentario", image: errorIcon });
|
|
70
|
+
} finally {
|
|
71
|
+
setSending(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Stack
|
|
77
|
+
direction={{ xs: "column", sm: "row" }}
|
|
78
|
+
alignItems={{ xs: "stretch", sm: "flex-end" }}
|
|
79
|
+
justifyContent="space-between"
|
|
80
|
+
flexWrap="wrap"
|
|
81
|
+
gap={1.5}
|
|
82
|
+
sx={{ py: "10px", minWidth: 0 }}
|
|
83
|
+
>
|
|
84
|
+
{!comment ? (
|
|
85
|
+
<Stack
|
|
86
|
+
direction={{ xs: "column", md: "row" }}
|
|
87
|
+
alignItems={{ xs: "stretch", md: "flex-end" }}
|
|
88
|
+
gap={1}
|
|
89
|
+
sx={{ flex: 1, minWidth: 0 }}
|
|
90
|
+
>
|
|
91
|
+
<Box sx={{ width: { xs: "100%", md: "min(500px, 100%)" } }}>
|
|
92
|
+
<Typography
|
|
93
|
+
component="label"
|
|
94
|
+
htmlFor="commentary-box-input"
|
|
95
|
+
sx={{ display: "block", mb: 0.5, fontFamily: FONTS.Raleway, fontSize: 14, color: COLORS.color_gray }}
|
|
96
|
+
>
|
|
97
|
+
Caja de Comentario
|
|
98
|
+
</Typography>
|
|
99
|
+
<TextField
|
|
100
|
+
id="commentary-box-input"
|
|
101
|
+
multiline
|
|
102
|
+
minRows={4}
|
|
103
|
+
maxRows={8}
|
|
104
|
+
fullWidth
|
|
105
|
+
value={text}
|
|
106
|
+
onChange={(e) => setText(e.target.value.slice(0, MAX_CHARS))}
|
|
107
|
+
helperText={`${text.length}/${MAX_CHARS}`}
|
|
108
|
+
FormHelperTextProps={{ sx: { textAlign: "right", mx: 0 } }}
|
|
109
|
+
InputProps={{ sx: { fontFamily: FONTS.AvenirNext, fontSize: 13, borderRadius: "3px" } }}
|
|
110
|
+
/>
|
|
111
|
+
</Box>
|
|
112
|
+
<Button
|
|
113
|
+
variant="outlined"
|
|
114
|
+
disabled={!text.trim() || sending}
|
|
115
|
+
onClick={submit}
|
|
116
|
+
sx={{
|
|
117
|
+
...pill,
|
|
118
|
+
mb: { md: "23px" },
|
|
119
|
+
fontSize: 16,
|
|
120
|
+
borderColor: COLORS.s5,
|
|
121
|
+
color: COLORS.s5,
|
|
122
|
+
"&:hover": { ...pill["&:hover"], borderColor: COLORS.s5 },
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{sending ? "Enviando…" : "Enviar comentario"}
|
|
126
|
+
</Button>
|
|
127
|
+
</Stack>
|
|
128
|
+
) : (
|
|
129
|
+
<Paper
|
|
130
|
+
variant="outlined"
|
|
131
|
+
sx={{ flex: 1, minWidth: 0, maxWidth: { md: 500 }, minHeight: 80, p: "10px", borderColor: COLORS.s2 }}
|
|
132
|
+
>
|
|
133
|
+
<Typography
|
|
134
|
+
sx={{
|
|
135
|
+
whiteSpace: "pre-wrap",
|
|
136
|
+
overflowWrap: "anywhere",
|
|
137
|
+
fontFamily: FONTS.AvenirNext,
|
|
138
|
+
fontSize: 12,
|
|
139
|
+
lineHeight: "15px",
|
|
140
|
+
color: COLORS.deep_gray,
|
|
141
|
+
textDecoration: crossComment ? "line-through" : "none",
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{comment?.message}
|
|
145
|
+
</Typography>
|
|
146
|
+
</Paper>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{(showChangeStatus || showSendEvaluation) && (
|
|
150
|
+
<Stack gap={2} sx={{ flexShrink: 0 }}>
|
|
151
|
+
{showChangeStatus && (
|
|
152
|
+
<Button
|
|
153
|
+
id="button-change-status"
|
|
154
|
+
variant="contained"
|
|
155
|
+
onClick={onOpenChangeStatus}
|
|
156
|
+
sx={{ ...pill, bgcolor: "#E33AA9", "&:hover": { ...pill["&:hover"], bgcolor: "#E33AA9" } }}
|
|
157
|
+
>
|
|
158
|
+
Cambio de Estatus
|
|
159
|
+
</Button>
|
|
160
|
+
)}
|
|
161
|
+
{showSendEvaluation && (
|
|
162
|
+
// Igual que el original: gris y deshabilitado mientras no se pueda
|
|
163
|
+
// enviar (evaluación sin terminar o faltan requeridos).
|
|
164
|
+
<Button
|
|
165
|
+
variant="contained"
|
|
166
|
+
disabled={!canSendEvaluation}
|
|
167
|
+
onClick={onSendEvaluation}
|
|
168
|
+
sx={{
|
|
169
|
+
...pill,
|
|
170
|
+
bgcolor: "#71de56",
|
|
171
|
+
color: COLORS.white,
|
|
172
|
+
"&:hover": { ...pill["&:hover"], bgcolor: "#71de56" },
|
|
173
|
+
"&.Mui-disabled": { bgcolor: "grey", color: COLORS.white },
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
Enviar evaluación
|
|
177
|
+
</Button>
|
|
178
|
+
)}
|
|
179
|
+
</Stack>
|
|
180
|
+
)}
|
|
181
|
+
</Stack>
|
|
182
|
+
);
|
|
183
|
+
};
|