contentoh-components-library 21.6.18 → 21.6.20

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 (28) hide show
  1. package/.env.test +2 -1
  2. package/dist/components/molecules/TagAndInput/index.js +14 -1
  3. package/dist/components/organisms/InputGroup/index.js +23 -17
  4. package/dist/components/organisms/LinkVariantsModal/api.js +102 -0
  5. package/dist/components/organisms/LinkVariantsModal/index.js +617 -0
  6. package/dist/components/organisms/LinkVariantsModal/styles.js +60 -0
  7. package/dist/components/pages/ProductEditionv2/index.js +3 -0
  8. package/dist/components/pages/ProviderProductEdition/context/reducers/inputs.js +8 -2
  9. package/dist/components/pages/ProviderProductEdition/index.js +201 -91
  10. package/dist/components/pages/ProviderProductEdition/styles.js +2 -2
  11. package/dist/components/pages/ProviderProductEdition/utils.js +8 -0
  12. package/dist/index.js +25 -12
  13. package/package.json +1 -1
  14. package/src/components/molecules/TagAndInput/index.js +15 -0
  15. package/src/components/organisms/InputGroup/index.js +6 -0
  16. package/src/components/organisms/LinkVariantsModal/api.js +61 -0
  17. package/src/components/organisms/LinkVariantsModal/index.js +474 -0
  18. package/src/components/organisms/LinkVariantsModal/styles.js +264 -0
  19. package/src/components/pages/ProductEditionv2/index.js +1 -0
  20. package/src/components/pages/ProviderProductEdition/context/reducers/inputs.js +21 -13
  21. package/src/components/pages/ProviderProductEdition/index.js +77 -0
  22. package/src/components/pages/ProviderProductEdition/styles.js +23 -0
  23. package/src/components/pages/ProviderProductEdition/utils.js +8 -0
  24. package/src/index.js +1 -0
  25. package/dist/ai/prompts/attribute.prompt.js +0 -28
  26. package/dist/ai/prompts/description.prompt.js +0 -33
  27. package/dist/ai/schemas/attribute.schema.js +0 -35
  28. package/dist/ai/schemas/description.schema.js +0 -27
@@ -0,0 +1,474 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import axios from "axios";
3
+
4
+ import { readPayload, ApiError } from "./api";
5
+
6
+ import {
7
+ Overlay,
8
+ Panel,
9
+ Header,
10
+ ModeSwitch,
11
+ Body,
12
+ SearchRow,
13
+ List,
14
+ AttributeList,
15
+ Section,
16
+ Message,
17
+ Footer,
18
+ } from "./styles";
19
+
20
+ /*
21
+ Modal para ligar productos como padre/variante.
22
+
23
+ props (* requerido):
24
+ - *articleId: producto desde el que se liga (el que está en edición)
25
+ - articleName / articleUpc: para que el usuario sepa sobre qué está trabajando
26
+ - *token: JWT que va en el header Authorization
27
+ - endpoint: base del API de variantes. Por defecto REACT_APP_VARIANTS_ENDPOINT
28
+ - *onClose: cerrar sin cambios
29
+ - onLinked: se llama con la respuesta del backend cuando la liga tuvo éxito
30
+
31
+ Los dos modos son los de POST /variants/link:
32
+ parent → este producto pasa a ser variante del que se elija
33
+ children → los elegidos pasan a ser variantes de este producto
34
+
35
+ En ambos el padre baja al hijo, nunca al revés. Lo que cambia es quién es quién.
36
+ */
37
+
38
+ // El backend devuelve códigos estables justamente para no mandar textos de UI
39
+ // desde el servidor. La traducción a lenguaje humano vive aquí.
40
+ const ERROR_TEXT = {
41
+ VARIANT_SELF_LINK: "Un producto no puede ser variante de sí mismo.",
42
+ VARIANT_PARENT_IS_CHILD:
43
+ "Ese producto ya es variante de otro. Las familias son de un solo nivel.",
44
+ VARIANT_CHILD_IS_PARENT:
45
+ "Ese producto ya tiene variantes propias, así que no puede colgar de otro.",
46
+ VARIANT_ALREADY_LINKED:
47
+ "Ese producto ya pertenece a otra familia. Desvincúlalo primero.",
48
+ VARIANT_NOT_OWNED_BY_USER: "Ese producto está fuera de tu alcance.",
49
+ VARIANT_CROSS_COMPANY: "Ese producto pertenece a otra compañía.",
50
+ VARIANT_INACTIVE_ARTICLE: "Ese producto está desactivado.",
51
+ VARIANT_NOT_FOUND: "No encontramos ese producto.",
52
+ VARIANT_PLAN_LIMIT_REACHED:
53
+ "Tu plan no tiene cupo disponible para más productos.",
54
+ };
55
+
56
+ const readError = (error) => {
57
+ if (error instanceof ApiError || error?.name === "ApiError") {
58
+ return ERROR_TEXT[error.code] || error.message;
59
+ }
60
+ // Falla de transporte de verdad: red caída, DNS, CORS.
61
+ return "No se pudo conectar con el servidor.";
62
+ };
63
+
64
+ export const LinkVariantsModal = ({
65
+ articleId,
66
+ articleName,
67
+ articleUpc,
68
+ activeRetailerId,
69
+ token,
70
+ endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
71
+ onClose,
72
+ onLinked,
73
+ }) => {
74
+ const [mode, setMode] = useState("parent");
75
+ const [step, setStep] = useState("search");
76
+
77
+ const [query, setQuery] = useState("");
78
+ const [candidates, setCandidates] = useState([]);
79
+ const [rejected, setRejected] = useState(null);
80
+ const [searched, setSearched] = useState(false);
81
+ const [selected, setSelected] = useState([]);
82
+
83
+ const [attributes, setAttributes] = useState([]);
84
+ const [nameScope, setNameScope] = useState(null);
85
+ const [inheritMode, setInheritMode] = useState("all-filled");
86
+ const [chosenAttributes, setChosenAttributes] = useState([]);
87
+ const [inheritImages, setInheritImages] = useState(true);
88
+ const [inheritDescriptions, setInheritDescriptions] = useState(true);
89
+ const [inheritCategoryRetailers, setInheritCategoryRetailers] = useState(true);
90
+
91
+ const [loading, setLoading] = useState(false);
92
+ const [error, setError] = useState(null);
93
+
94
+ const request = useCallback(
95
+ async (action, body) => {
96
+ const res = await axios.post(`${endpoint}/${action}`, body, {
97
+ headers: { Authorization: token },
98
+ });
99
+ // El transporte siempre responde 200; el resultado real va en el cuerpo.
100
+ return readPayload(res);
101
+ },
102
+ [endpoint, token]
103
+ );
104
+
105
+ // En modo children el padre es el producto en edición; en modo parent es el
106
+ // candidato que el usuario acaba de elegir. De ahí sale qué atributos ofrecer.
107
+ const parentArticleId = useMemo(
108
+ () => (mode === "children" ? articleId : selected[0]),
109
+ [mode, articleId, selected]
110
+ );
111
+
112
+ // Cambiar de dirección invalida la selección: los candidatos elegibles no son
113
+ // los mismos, y arrastrar la anterior llevaría a ligar algo que no se buscó.
114
+ useEffect(() => {
115
+ setCandidates([]);
116
+ setSelected([]);
117
+ setRejected(null);
118
+ setSearched(false);
119
+ setError(null);
120
+ setStep("search");
121
+ }, [mode]);
122
+
123
+ const search = async (event) => {
124
+ event?.preventDefault();
125
+ setLoading(true);
126
+ setError(null);
127
+ try {
128
+ const data = await request("search", {
129
+ mode,
130
+ articleId,
131
+ query: query.trim(),
132
+ });
133
+ setCandidates(data.candidates || []);
134
+ setRejected(data.rejected || null);
135
+ setSearched(true);
136
+ } catch (err) {
137
+ setError(readError(err));
138
+ } finally {
139
+ setLoading(false);
140
+ }
141
+ };
142
+
143
+ const toggle = (id) => {
144
+ setSelected((current) => {
145
+ if (mode === "parent") return current[0] === id ? [] : [id];
146
+ return current.includes(id)
147
+ ? current.filter((item) => item !== id)
148
+ : [...current, id];
149
+ });
150
+ };
151
+
152
+ const goToInheritance = async () => {
153
+ setLoading(true);
154
+ setError(null);
155
+ try {
156
+ // La cadena activa decide con qué nombre se ve cada atributo: el equipo
157
+ // interno y los proveedores los renombran por cadena en attribute_manager,
158
+ // y mostrar el nombre de otra cadena confundiría al elegir.
159
+ const data = await request("attributes", {
160
+ parentArticleId,
161
+ retailerId: activeRetailerId,
162
+ });
163
+ setAttributes(data.attributes || []);
164
+ setNameScope(data.nameScope ?? null);
165
+ setChosenAttributes((data.attributes || []).map((item) => item.id));
166
+ setStep("inherit");
167
+ } catch (err) {
168
+ setError(readError(err));
169
+ } finally {
170
+ setLoading(false);
171
+ }
172
+ };
173
+
174
+ const toggleAttribute = (id) => {
175
+ setChosenAttributes((current) =>
176
+ current.includes(id)
177
+ ? current.filter((item) => item !== id)
178
+ : [...current, id]
179
+ );
180
+ };
181
+
182
+ const submit = async () => {
183
+ setLoading(true);
184
+ setError(null);
185
+
186
+ const inherit = {
187
+ attributes: inheritMode === "all-filled" ? "all-filled" : chosenAttributes,
188
+ images: inheritImages,
189
+ descriptions: inheritDescriptions,
190
+ categoryRetailers: inheritCategoryRetailers,
191
+ };
192
+
193
+ const body =
194
+ mode === "parent"
195
+ ? { mode, articleId, parentArticleId: selected[0], inherit }
196
+ : { mode, parentArticleId: articleId, childrenArticleIds: selected, inherit };
197
+
198
+ try {
199
+ const data = await request("link", body);
200
+ onLinked?.(data);
201
+ onClose?.();
202
+ } catch (err) {
203
+ setError(readError(err));
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ };
208
+
209
+ const selectionLabel =
210
+ mode === "parent"
211
+ ? "Se elige un solo producto padre."
212
+ : `${selected.length} producto(s) pasarán a ser variantes.`;
213
+
214
+ return (
215
+ <Overlay onClick={onClose}>
216
+ <Panel onClick={(event) => event.stopPropagation()}>
217
+ <Header>
218
+ <div>
219
+ <h2>Ligar productos</h2>
220
+ <p>
221
+ {articleName || `Producto ${articleId}`}
222
+ {articleUpc ? ` · UPC ${articleUpc}` : ""}
223
+ </p>
224
+ </div>
225
+ <button type="button" className="close" onClick={onClose} aria-label="Cerrar">
226
+ ×
227
+ </button>
228
+ </Header>
229
+
230
+ {step === "search" && (
231
+ <ModeSwitch>
232
+ <button
233
+ type="button"
234
+ className={mode === "parent" ? "active" : ""}
235
+ onClick={() => setMode("parent")}
236
+ >
237
+ Este producto es variante de otro
238
+ </button>
239
+ <button
240
+ type="button"
241
+ className={mode === "children" ? "active" : ""}
242
+ onClick={() => setMode("children")}
243
+ >
244
+ Agregar variantes a este producto
245
+ </button>
246
+ </ModeSwitch>
247
+ )}
248
+
249
+ <Body>
250
+ {error && <Message className="error">{error}</Message>}
251
+
252
+ {step === "search" && (
253
+ <>
254
+ <SearchRow onSubmit={search}>
255
+ <input
256
+ type="text"
257
+ value={query}
258
+ placeholder="Busca por UPC, SKU o nombre"
259
+ onChange={(event) => setQuery(event.target.value)}
260
+ />
261
+ <button type="submit" disabled={loading}>
262
+ {loading ? "Buscando…" : "Buscar"}
263
+ </button>
264
+ </SearchRow>
265
+
266
+ {/* El backend explica por qué un UPC existente no es ligable. Sin
267
+ esto el usuario ve "sin resultados" y no sabe qué hacer. */}
268
+ {rejected && (
269
+ <Message className="error">
270
+ {rejected.name || rejected.upc}: {ERROR_TEXT[rejected.code]}
271
+ </Message>
272
+ )}
273
+
274
+ {searched && !candidates.length && !rejected && (
275
+ <Message>
276
+ No hay productos que puedas ligar con esa búsqueda. Solo
277
+ aparecen productos activos de la misma compañía que todavía no
278
+ pertenecen a otra familia.
279
+ </Message>
280
+ )}
281
+
282
+ {candidates.length > 0 && (
283
+ <List>
284
+ {candidates.map((candidate) => (
285
+ <li key={candidate.articleId}>
286
+ <label>
287
+ <input
288
+ type={mode === "parent" ? "radio" : "checkbox"}
289
+ name="candidate"
290
+ checked={selected.includes(candidate.articleId)}
291
+ onChange={() => toggle(candidate.articleId)}
292
+ />
293
+ <span>
294
+ {candidate.name || `Producto ${candidate.articleId}`}
295
+ <span className="meta">
296
+ UPC {candidate.upc || "—"}
297
+ {candidate.categoryName ? ` · ${candidate.categoryName}` : ""}
298
+ </span>
299
+ </span>
300
+ {candidate.variantsCount > 0 && (
301
+ <span className="count">
302
+ {candidate.variantsCount} variante
303
+ {candidate.variantsCount === 1 ? "" : "s"}
304
+ </span>
305
+ )}
306
+ </label>
307
+ </li>
308
+ ))}
309
+ </List>
310
+ )}
311
+ </>
312
+ )}
313
+
314
+ {step === "inherit" && (
315
+ <>
316
+ <Section>
317
+ <h3>¿Qué se hereda del producto padre?</h3>
318
+ <label className="choice">
319
+ <input
320
+ type="radio"
321
+ checked={inheritMode === "all-filled"}
322
+ onChange={() => setInheritMode("all-filled")}
323
+ />
324
+ <span>
325
+ Todos los atributos con valor
326
+ <span className="hint">
327
+ {attributes.length} atributo(s) con información en el padre.
328
+ </span>
329
+ </span>
330
+ </label>
331
+ <label className="choice">
332
+ <input
333
+ type="radio"
334
+ checked={inheritMode === "selected"}
335
+ onChange={() => setInheritMode("selected")}
336
+ />
337
+ <span>
338
+ Elegir cuáles
339
+ <span className="hint">
340
+ Lo que no elijas conserva el valor que ya tiene la variante.
341
+ </span>
342
+ </span>
343
+ </label>
344
+ </Section>
345
+
346
+ {inheritMode === "selected" && activeRetailerId && !nameScope && (
347
+ <Message>
348
+ El producto padre no comparte la cadena que tienes abierta, así
349
+ que los nombres de abajo son los de otra de sus cadenas.
350
+ </Message>
351
+ )}
352
+
353
+ {inheritMode === "selected" && (
354
+ <AttributeList>
355
+ {attributes.map((attribute) => (
356
+ <li key={attribute.id}>
357
+ <label>
358
+ <input
359
+ type="checkbox"
360
+ checked={chosenAttributes.includes(attribute.id)}
361
+ onChange={() => toggleAttribute(attribute.id)}
362
+ />
363
+ <span>
364
+ {attribute.name}
365
+ {attribute.unconfigured && (
366
+ <span className="note">
367
+ sin configuración en las cadenas del padre
368
+ </span>
369
+ )}
370
+ {attribute.ambiguousName && (
371
+ <span className="note">
372
+ se llama distinto en otra cadena
373
+ </span>
374
+ )}
375
+ </span>
376
+ <span className="value" title={attribute.value}>
377
+ {attribute.value}
378
+ </span>
379
+ </label>
380
+ </li>
381
+ ))}
382
+ </AttributeList>
383
+ )}
384
+
385
+ <Section>
386
+ <h3>También se hereda</h3>
387
+ <label className="choice">
388
+ <input
389
+ type="checkbox"
390
+ checked={inheritImages}
391
+ onChange={() => setInheritImages((value) => !value)}
392
+ />
393
+ <span>Imágenes</span>
394
+ </label>
395
+ <label className="choice">
396
+ <input
397
+ type="checkbox"
398
+ checked={inheritDescriptions}
399
+ onChange={() => setInheritDescriptions((value) => !value)}
400
+ />
401
+ <span>Descripciones</span>
402
+ </label>
403
+ <label className="choice">
404
+ <input
405
+ type="checkbox"
406
+ checked={inheritCategoryRetailers}
407
+ onChange={() => setInheritCategoryRetailers((value) => !value)}
408
+ />
409
+ <span>
410
+ Cadenas y categorías
411
+ <span className="hint">
412
+ Las parejas cadena-categoría del padre se copian a la variante.
413
+ </span>
414
+ </span>
415
+ </label>
416
+ </Section>
417
+
418
+ {/* Es la diferencia entre link y propagate, y es la que sorprende:
419
+ aquí lo elegido se escribe encima aunque la variante lo hubiera
420
+ editado a mano. Decirlo antes evita el reclamo después. */}
421
+ <Message>
422
+ Lo que elijas heredar reemplaza el valor actual de la variante,
423
+ incluso si alguien lo había capturado a mano.
424
+ </Message>
425
+ </>
426
+ )}
427
+ </Body>
428
+
429
+ <Footer>
430
+ <span className="summary">
431
+ {step === "search" ? selectionLabel : "Revisa la herencia antes de ligar."}
432
+ </span>
433
+ <div className="actions">
434
+ {step === "inherit" && (
435
+ <button
436
+ type="button"
437
+ className="secondary"
438
+ onClick={() => setStep("search")}
439
+ disabled={loading}
440
+ >
441
+ Atrás
442
+ </button>
443
+ )}
444
+ <button type="button" className="secondary" onClick={onClose} disabled={loading}>
445
+ Cancelar
446
+ </button>
447
+ {step === "search" ? (
448
+ <button
449
+ type="button"
450
+ className="primary"
451
+ onClick={goToInheritance}
452
+ disabled={loading || !selected.length}
453
+ >
454
+ Continuar
455
+ </button>
456
+ ) : (
457
+ <button
458
+ type="button"
459
+ className="primary"
460
+ onClick={submit}
461
+ disabled={
462
+ loading ||
463
+ (inheritMode === "selected" && !chosenAttributes.length)
464
+ }
465
+ >
466
+ {loading ? "Ligando…" : "Ligar"}
467
+ </button>
468
+ )}
469
+ </div>
470
+ </Footer>
471
+ </Panel>
472
+ </Overlay>
473
+ );
474
+ };