contentoh-components-library 21.6.19 → 21.6.21
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/.env.test +2 -1
- package/dist/components/atoms/InheritanceTag/index.js +8 -0
- package/dist/components/molecules/TagAndInput/index.js +1 -0
- package/dist/components/molecules/TagAndInputv2/index.js +1 -0
- package/dist/components/organisms/LinkVariantsModal/api.js +102 -0
- package/dist/components/organisms/LinkVariantsModal/index.js +620 -0
- package/dist/components/organisms/LinkVariantsModal/styles.js +60 -0
- package/dist/components/pages/ProviderProductEdition/context/reducers/inputs.js +8 -2
- package/dist/components/pages/ProviderProductEdition/index.js +198 -89
- package/dist/components/pages/ProviderProductEdition/styles.js +2 -2
- package/dist/components/pages/ProviderProductEdition/utils.js +8 -0
- package/dist/index.js +25 -12
- package/package.json +1 -1
- package/src/components/atoms/InheritanceTag/index.js +7 -0
- package/src/components/molecules/TagAndInput/index.js +1 -0
- package/src/components/molecules/TagAndInputv2/index.js +1 -0
- package/src/components/organisms/LinkVariantsModal/api.js +61 -0
- package/src/components/organisms/LinkVariantsModal/index.js +479 -0
- package/src/components/organisms/LinkVariantsModal/styles.js +264 -0
- package/src/components/pages/ProviderProductEdition/context/reducers/inputs.js +21 -13
- package/src/components/pages/ProviderProductEdition/index.js +76 -0
- package/src/components/pages/ProviderProductEdition/styles.js +23 -0
- package/src/components/pages/ProviderProductEdition/utils.js +8 -0
- package/src/index.js +1 -0
- package/dist/ai/prompts/attribute.prompt.js +0 -28
- package/dist/ai/prompts/description.prompt.js +0 -33
- package/dist/ai/schemas/attribute.schema.js +0 -35
- package/dist/ai/schemas/description.schema.js +0 -27
package/dist/index.js
CHANGED
|
@@ -1083,7 +1083,7 @@ Object.keys(_index82).forEach(function (key) {
|
|
|
1083
1083
|
});
|
|
1084
1084
|
});
|
|
1085
1085
|
|
|
1086
|
-
var _index83 = require("./components/organisms/
|
|
1086
|
+
var _index83 = require("./components/organisms/LinkVariantsModal/index");
|
|
1087
1087
|
|
|
1088
1088
|
Object.keys(_index83).forEach(function (key) {
|
|
1089
1089
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -1096,7 +1096,7 @@ Object.keys(_index83).forEach(function (key) {
|
|
|
1096
1096
|
});
|
|
1097
1097
|
});
|
|
1098
1098
|
|
|
1099
|
-
var _index84 = require("./components/organisms/
|
|
1099
|
+
var _index84 = require("./components/organisms/ProductImageModal/index");
|
|
1100
1100
|
|
|
1101
1101
|
Object.keys(_index84).forEach(function (key) {
|
|
1102
1102
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -1109,7 +1109,7 @@ Object.keys(_index84).forEach(function (key) {
|
|
|
1109
1109
|
});
|
|
1110
1110
|
});
|
|
1111
1111
|
|
|
1112
|
-
var _index85 = require("./components/organisms/
|
|
1112
|
+
var _index85 = require("./components/organisms/Chat/index");
|
|
1113
1113
|
|
|
1114
1114
|
Object.keys(_index85).forEach(function (key) {
|
|
1115
1115
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -1122,7 +1122,7 @@ Object.keys(_index85).forEach(function (key) {
|
|
|
1122
1122
|
});
|
|
1123
1123
|
});
|
|
1124
1124
|
|
|
1125
|
-
var _index86 = require("./components/organisms/
|
|
1125
|
+
var _index86 = require("./components/organisms/Modal/index");
|
|
1126
1126
|
|
|
1127
1127
|
Object.keys(_index86).forEach(function (key) {
|
|
1128
1128
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -1135,6 +1135,19 @@ Object.keys(_index86).forEach(function (key) {
|
|
|
1135
1135
|
});
|
|
1136
1136
|
});
|
|
1137
1137
|
|
|
1138
|
+
var _index87 = require("./components/organisms/OrderDetail/index");
|
|
1139
|
+
|
|
1140
|
+
Object.keys(_index87).forEach(function (key) {
|
|
1141
|
+
if (key === "default" || key === "__esModule") return;
|
|
1142
|
+
if (key in exports && exports[key] === _index87[key]) return;
|
|
1143
|
+
Object.defineProperty(exports, key, {
|
|
1144
|
+
enumerable: true,
|
|
1145
|
+
get: function get() {
|
|
1146
|
+
return _index87[key];
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1138
1151
|
var _RangeCalendar = require("./components/organisms/RangeCalendar");
|
|
1139
1152
|
|
|
1140
1153
|
Object.keys(_RangeCalendar).forEach(function (key) {
|
|
@@ -1161,28 +1174,28 @@ Object.keys(_TableResizable).forEach(function (key) {
|
|
|
1161
1174
|
});
|
|
1162
1175
|
});
|
|
1163
1176
|
|
|
1164
|
-
var
|
|
1177
|
+
var _index88 = require("./components/organisms/SideModal/index");
|
|
1165
1178
|
|
|
1166
|
-
Object.keys(
|
|
1179
|
+
Object.keys(_index88).forEach(function (key) {
|
|
1167
1180
|
if (key === "default" || key === "__esModule") return;
|
|
1168
|
-
if (key in exports && exports[key] ===
|
|
1181
|
+
if (key in exports && exports[key] === _index88[key]) return;
|
|
1169
1182
|
Object.defineProperty(exports, key, {
|
|
1170
1183
|
enumerable: true,
|
|
1171
1184
|
get: function get() {
|
|
1172
|
-
return
|
|
1185
|
+
return _index88[key];
|
|
1173
1186
|
}
|
|
1174
1187
|
});
|
|
1175
1188
|
});
|
|
1176
1189
|
|
|
1177
|
-
var
|
|
1190
|
+
var _index89 = require("./components/organisms/GridProducts/index");
|
|
1178
1191
|
|
|
1179
|
-
Object.keys(
|
|
1192
|
+
Object.keys(_index89).forEach(function (key) {
|
|
1180
1193
|
if (key === "default" || key === "__esModule") return;
|
|
1181
|
-
if (key in exports && exports[key] ===
|
|
1194
|
+
if (key in exports && exports[key] === _index89[key]) return;
|
|
1182
1195
|
Object.defineProperty(exports, key, {
|
|
1183
1196
|
enumerable: true,
|
|
1184
1197
|
get: function get() {
|
|
1185
|
-
return
|
|
1198
|
+
return _index89[key];
|
|
1186
1199
|
}
|
|
1187
1200
|
});
|
|
1188
1201
|
});
|
package/package.json
CHANGED
|
@@ -9,16 +9,23 @@ import { Container } from "./styles";
|
|
|
9
9
|
* así que es el que lleva acento y el que ofrece la acción de volver a heredar.
|
|
10
10
|
* "Heredado" es el caso normal y se mantiene discreto: si gritara en cada campo,
|
|
11
11
|
* la ficha entera se volvería ruido.
|
|
12
|
+
*
|
|
13
|
+
* Un campo VACÍO no lleva marca: no heredó nada y tampoco tiene valor propio, así
|
|
14
|
+
* que llamarlo "Heredado" es falso además de ruidoso. Sin esto la ficha de una
|
|
15
|
+
* variante sale con el chip repetido en cada campo, incluidos los obligatorios en
|
|
16
|
+
* rojo que están sin capturar, y la marca deja de distinguir nada.
|
|
12
17
|
*/
|
|
13
18
|
export const InheritanceTag = ({
|
|
14
19
|
isVariant = false,
|
|
15
20
|
isOverridden = false,
|
|
21
|
+
hasValue = true,
|
|
16
22
|
parentUpc,
|
|
17
23
|
onRestore,
|
|
18
24
|
restoreLabel = "Volver a heredar",
|
|
19
25
|
className = "",
|
|
20
26
|
}) => {
|
|
21
27
|
if (!isVariant) return null;
|
|
28
|
+
if (!hasValue) return null;
|
|
22
29
|
|
|
23
30
|
const state = isOverridden ? "own" : "inherited";
|
|
24
31
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Las lambdas de este API están declaradas con `integration: lambda`, no con
|
|
3
|
+
lambda-proxy. Eso tiene dos consecuencias que se pagan caras si se ignoran:
|
|
4
|
+
|
|
5
|
+
1. La respuesta llega ENVUELTA: axios entrega { statusCode, headers, body },
|
|
6
|
+
con `body` en texto. Leer `res.data.candidates` da undefined.
|
|
7
|
+
2. El HTTP siempre es 200, incluso cuando la lambda respondió 400 o 500. Un
|
|
8
|
+
try/catch alrededor de axios NUNCA se dispara por un error de negocio: el
|
|
9
|
+
error viaja dentro del cuerpo y hay que mirar `statusCode` a mano.
|
|
10
|
+
|
|
11
|
+
El repo ya vive con esto —provider-product-edition.context.jsx hace
|
|
12
|
+
JSON.parse(res.data.body)— pero sin un lugar común cada llamada nueva vuelve a
|
|
13
|
+
tropezar. Esto lo centraliza.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const unwrap = (res) => {
|
|
17
|
+
const payload = res?.data;
|
|
18
|
+
|
|
19
|
+
const envuelto = payload
|
|
20
|
+
&& typeof payload === "object"
|
|
21
|
+
&& "statusCode" in payload
|
|
22
|
+
&& "body" in payload;
|
|
23
|
+
|
|
24
|
+
if (!envuelto) return { status: res?.status ?? 200, body: payload };
|
|
25
|
+
|
|
26
|
+
const body =
|
|
27
|
+
typeof payload.body === "string" ? safeParse(payload.body) : payload.body;
|
|
28
|
+
|
|
29
|
+
return { status: Number(payload.statusCode), body };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const safeParse = (text) => {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(text);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return { message: text };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
Error de negocio con el código estable que manda el backend
|
|
42
|
+
(VARIANT_CROSS_COMPANY, VARIANT_ALREADY_LINKED, ...). Se lanza para que el
|
|
43
|
+
llamador use un try/catch normal, aunque el transporte haya dicho 200.
|
|
44
|
+
*/
|
|
45
|
+
export class ApiError extends Error {
|
|
46
|
+
constructor(code, message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "ApiError";
|
|
49
|
+
this.code = code;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const readPayload = (res) => {
|
|
54
|
+
const { status, body } = unwrap(res);
|
|
55
|
+
|
|
56
|
+
if (status >= 400) {
|
|
57
|
+
throw new ApiError(body?.code, body?.message || "La operación falló.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return body ?? {};
|
|
61
|
+
};
|
|
@@ -0,0 +1,479 @@
|
|
|
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
|
+
// Solo estorba cuando hay algo que elegir y no se eligió nada.
|
|
464
|
+
// Con el padre sin atributos heredables, ligar sigue siendo
|
|
465
|
+
// válido: se heredan imágenes, descripciones y cadenas.
|
|
466
|
+
(inheritMode === "selected" &&
|
|
467
|
+
attributes.length > 0 &&
|
|
468
|
+
!chosenAttributes.length)
|
|
469
|
+
}
|
|
470
|
+
>
|
|
471
|
+
{loading ? "Ligando…" : "Ligar"}
|
|
472
|
+
</button>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
</Footer>
|
|
476
|
+
</Panel>
|
|
477
|
+
</Overlay>
|
|
478
|
+
);
|
|
479
|
+
};
|