contentoh-components-library 21.6.30 → 21.6.31

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.
@@ -15,9 +15,12 @@ var _variables = require("../../../global-files/variables");
15
15
 
16
16
  var _templateObject;
17
17
 
18
- var Container = _styledComponents.default.div(_templateObject || (_templateObject = (0, _taggedTemplateLiteral2.default)(["\n display: flex;\n flex-direction: column;\n height: 100%;\n flex: 1;\n overflow: auto;\n\n .data-container {\n display: flex;\n flex: 0%;\n height: calc(100% - ", "px);\n .image-data-panel {\n width: 340px;\n\n & + * {\n margin-left: 10px;\n }\n }\n\n .product-information {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n\n .services-information-container {\n height: 100%;\n overflow: auto;\n }\n\n .image-services {\n aside {\n display: grid;\n grid-template-columns: repeat(auto-fill, 179px);\n column-gap: 15px;\n row-gap: 15px;\n padding: 20px;\n }\n }\n\n .commentary-box {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n padding: 10px;\n padding-left: 0;\n\n .action-buttons{\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\n\n .commentary {\n display: flex;\n align-items: flex-end;\n\n .input-container {\n width: 500px;\n\n .quill {\n height: 100px;\n }\n\n & + * {\n margin-left: 5px;\n }\n }\n\n .buttons-box {\n display: flex;\n width: 210px;\n flex-wrap: wrap;\n\n .general-transparent-button {\n & + * {\n margin-top: 5px;\n }\n }\n\n .general-transparent-button,\n .general-green-button,\n .general-button-disabled {\n width: fit-content;\n min-width: 201px;\n height: 40px;\n }\n }\n }\n }\n\n .feedback-box {\n display: flex;\n }\n\n .required-inputs-message {\n font-family: ", ";\n font-size: 13px;\n color: ", ";\n padding: 10px;\n display: flex;\n align-items: center;\n\n p + * {\n margin-top: 10px;\n }\n\n button {\n min-width: fit-content;\n }\n }\n }\n }\n #modal-message-box {\n width: 400px;\n height: 100px;\n }\n .container {\n width: 100%;\n height: 90%;\n .dropzone {\n height: 100%;\n width: 100%;\n position: relative;\n transition: all 0.5s ease;\n\n &.drag-active {\n background-color: rgba(", ", 0.05);\n border: 2px dashed ", ";\n border-radius: 8px;\n\n aside {\n opacity: 0.3;\n pointer-events: none;\n }\n }\n\n .drag-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(", ", 0.1);\n border-radius: 8px;\n pointer-events: none;\n z-index: 10;\n\n p {\n font-family: ", ";\n font-size: 24px;\n font-weight: 600;\n color: ", ";\n text-align: center;\n padding: 20px;\n background-color: ", ";\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n }\n }\n }\n"])), function (_ref) {
18
+ // Rosa ContentOh, el mismo del modal de ligar productos.
19
+ var PINK = "#E13AA8";
20
+
21
+ var Container = _styledComponents.default.div(_templateObject || (_templateObject = (0, _taggedTemplateLiteral2.default)(["\n display: flex;\n flex-direction: column;\n height: 100%;\n flex: 1;\n overflow: auto;\n\n .data-container {\n display: flex;\n flex: 0%;\n height: calc(100% - ", "px);\n .image-data-panel {\n width: 340px;\n\n & + * {\n margin-left: 10px;\n }\n }\n\n .product-information {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n\n .services-information-container {\n height: 100%;\n overflow: auto;\n }\n\n .image-services {\n aside {\n display: grid;\n grid-template-columns: repeat(auto-fill, 179px);\n column-gap: 15px;\n row-gap: 15px;\n padding: 20px;\n }\n }\n\n .commentary-box {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n padding: 10px;\n padding-left: 0;\n\n .action-buttons{\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\n\n .commentary {\n display: flex;\n align-items: flex-end;\n\n .input-container {\n width: 500px;\n\n .quill {\n height: 100px;\n }\n\n & + * {\n margin-left: 5px;\n }\n }\n\n .buttons-box {\n display: flex;\n width: 210px;\n flex-wrap: wrap;\n\n .general-transparent-button {\n & + * {\n margin-top: 5px;\n }\n }\n\n .general-transparent-button,\n .general-green-button,\n .general-button-disabled {\n width: fit-content;\n min-width: 201px;\n height: 40px;\n }\n }\n }\n }\n\n .feedback-box {\n display: flex;\n }\n\n .required-inputs-message {\n font-family: ", ";\n font-size: 13px;\n color: ", ";\n padding: 10px;\n display: flex;\n align-items: center;\n\n p + * {\n margin-top: 10px;\n }\n\n button {\n min-width: fit-content;\n }\n }\n }\n }\n #modal-message-box {\n width: 400px;\n height: 100px;\n }\n .container {\n width: 100%;\n height: 90%;\n .dropzone {\n height: 100%;\n width: 100%;\n position: relative;\n transition: all 0.5s ease;\n\n &.drag-active {\n background-color: rgba(", ", 0.05);\n border: 2px dashed ", ";\n border-radius: 8px;\n\n aside {\n opacity: 0.3;\n pointer-events: none;\n }\n }\n\n .drag-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(", ", 0.1);\n border-radius: 8px;\n pointer-events: none;\n z-index: 10;\n\n p {\n font-family: ", ";\n font-size: 24px;\n font-weight: 600;\n color: ", ";\n text-align: center;\n padding: 20px;\n background-color: ", ";\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n }\n }\n }\n\n .variants-actions {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 8px;\n\n .link-variants {\n border: 1px solid ", ";\n border-radius: 6px;\n background: none;\n padding: 6px 12px;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n cursor: pointer;\n }\n\n .variants-count {\n border: none;\n background: none;\n padding: 0;\n cursor: pointer;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n text-decoration: underline;\n }\n }\n\n .variants-family {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n padding: 8px;\n border: 1px solid ", ";\n border-radius: 6px;\n\n .loading {\n margin: 0;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n }\n\n .family-row {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n text-align: left;\n font-family: ", ";\n font-size: 12px;\n color: ", ";\n }\n\n .family-row:hover:not(:disabled) {\n background-color: ", ";\n }\n\n .family-row:disabled {\n cursor: default;\n opacity: 0.7;\n }\n\n .family-row .role {\n min-width: 96px;\n font-size: 10px;\n color: ", ";\n }\n\n .family-row.current .role {\n color: ", ";\n }\n\n .family-row .name {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .family-row .upc {\n font-size: 11px;\n color: ", ";\n }\n }\n"])), function (_ref) {
19
22
  var headerTop = _ref.headerTop;
20
23
  return headerTop;
21
- }, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_gray, _variables.GlobalColors.rgb_color_pink, _variables.GlobalColors.color_pink, _variables.GlobalColors.rgb_color_pink, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_pink, _variables.GlobalColors.white);
24
+ }, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_gray, _variables.GlobalColors.rgb_color_pink, _variables.GlobalColors.color_pink, _variables.GlobalColors.rgb_color_pink, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_pink, _variables.GlobalColors.white, PINK, _variables.FontFamily.Raleway_600, PINK, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, _variables.GlobalColors.s2, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, _variables.FontFamily.Raleway_500, _variables.GlobalColors.deep_gray, _variables.GlobalColors.s2, PINK, _variables.GlobalColors.gray, _variables.GlobalColors.gray);
22
25
 
23
26
  exports.Container = Container;
@@ -126,6 +126,8 @@ var sendMessage = /*#__PURE__*/function () {
126
126
  exports.sendMessage = sendMessage;
127
127
 
128
128
  var normalizeProduct = function normalizeProduct(product) {
129
+ var _ref5, _productData$parentAr, _ref6, _productData$parent, _ref7, _productData$variants;
130
+
129
131
  // Handle case where product might be wrapped in a product property
130
132
  var productData = product.product || product;
131
133
  var article = productData.article || {};
@@ -161,6 +163,12 @@ var normalizeProduct = function normalizeProduct(product) {
161
163
  percentages: productData.percentages || null,
162
164
  summary: productData.summary || null,
163
165
  version: productData.version || 0,
166
+ // Parentesco. read-articles-v2 ya lo devuelve; sin arrastrarlo hasta aquí la
167
+ // pantalla no puede saber si el producto pertenece a una familia.
168
+ // `?? null` en vez de `|| null`: variantsCount 0 es un dato válido.
169
+ parentArticleId: (_ref5 = (_productData$parentAr = productData.parentArticleId) !== null && _productData$parentAr !== void 0 ? _productData$parentAr : article.parentArticleId) !== null && _ref5 !== void 0 ? _ref5 : null,
170
+ parent: (_ref6 = (_productData$parent = productData.parent) !== null && _productData$parent !== void 0 ? _productData$parent : article.parent) !== null && _ref6 !== void 0 ? _ref6 : null,
171
+ variantsCount: (_ref7 = (_productData$variants = productData.variantsCount) !== null && _productData$variants !== void 0 ? _productData$variants : article.variantsCount) !== null && _ref7 !== void 0 ? _ref7 : 0,
164
172
  id_order: productData.orderId || productData.id_order || 0,
165
173
  status: productData.status || null,
166
174
  datasheet_status: productData.datasheet_status || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.6.30",
3
+ "version": "21.6.31",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -137,13 +137,20 @@ export const ProviderProductEditionProvider = ({ children }) => {
137
137
  }
138
138
  };
139
139
 
140
- const saveDatasheets = async (token) => {
140
+ /*
141
+ `propagation` es opcional y solo lo manda la ficha de un producto padre, con
142
+ lo que el usuario eligió en el diálogo. Sin ese bloque el backend no propaga
143
+ nada: bajar un cambio a las variantes es una decisión, no un efecto
144
+ secundario de guardar.
145
+ */
146
+ const saveDatasheets = async (token, propagation) => {
141
147
  if (state.updated_datasheets_inputs.length === 0) return;
142
148
  dispatch({ type: "SET_SAVING", payload: true });
143
149
 
144
150
  const dataObject = {
145
151
  articleId: state.product?.id_article,
146
152
  articleData: state.updated_datasheets_inputs,
153
+ ...(propagation ? { propagation } : {}),
147
154
  };
148
155
 
149
156
  if (state.product?.id_order) {
@@ -174,9 +181,22 @@ export const ProviderProductEditionProvider = ({ children }) => {
174
181
  type: "SET_UPDATED_DATASHEETS_INPUTS",
175
182
  payload: [],
176
183
  });
184
+ // En una variante, todo atributo que el hijo toca deja de ser heredado:
185
+ // update-articles-data le pone is_overridden = 1 en la base. Sin
186
+ // reflejarlo aquí, el chip sigue diciendo "Heredado" hasta la siguiente
187
+ // carga de la ficha, y el usuario ve una marca que ya es falsa.
188
+ const esVariante =
189
+ state.services?.datasheets?.variant?.isVariant ??
190
+ !!state.product?.parentArticleId;
191
+
177
192
  dispatch({
178
193
  type: "UPDATE_DATASHEETS_INPUTS",
179
- payload: state.updated_datasheets_inputs,
194
+ payload: esVariante
195
+ ? state.updated_datasheets_inputs.map((item) => ({
196
+ ...item,
197
+ isOverridden: 1,
198
+ }))
199
+ : state.updated_datasheets_inputs,
180
200
  });
181
201
 
182
202
  // Contar cuántos campos requeridos que antes estaban vacíos ahora tienen valor
@@ -50,6 +50,9 @@ import { InputGroup } from "../../organisms/InputGroup";
50
50
  import { ProductImageModal } from "../../organisms/ProductImageModal";
51
51
  import { VersionSelector } from "../../organisms/VersionSelector";
52
52
  import { Modal } from "../../organisms/Modal";
53
+ import { LinkVariantsModal } from "../../organisms/LinkVariantsModal";
54
+ import { PropagateChangesModal } from "../../organisms/PropagateChangesModal";
55
+ import { readPayload } from "../../organisms/LinkVariantsModal/api";
53
56
 
54
57
  import successIcon from "../../../assets/images/genericModal/genericModalCheck.svg";
55
58
  import errorIcon from "../../../assets/images/genericModal/errorModal.svg";
@@ -178,6 +181,14 @@ const RetailerProductEditionView = ({
178
181
  const [statusArray, setStatusArray] = useState([]);
179
182
  const [rejectAll, setRejectAll] = useState(false);
180
183
 
184
+ // Variantes: mismo comportamiento que la ficha de la aplicación de clientes.
185
+ const [showLinkVariants, setShowLinkVariants] = useState(false);
186
+ const [family, setFamily] = useState(null);
187
+ const [showFamily, setShowFamily] = useState(false);
188
+ const [loadingVariant, setLoadingVariant] = useState(null);
189
+ const [pendingPropagation, setPendingPropagation] = useState(null);
190
+ const [preparandoPropagacion, setPreparandoPropagacion] = useState(false);
191
+
181
192
  const [auditServices, setAuditServices] = useState([]);
182
193
  const [auditDatasheets, setAuditDatasheets] = useState([]);
183
194
  const [auditDescriptions, setAuditDescriptions] = useState([]);
@@ -771,16 +782,170 @@ const RetailerProductEditionView = ({
771
782
  }
772
783
  };
773
784
 
785
+ /*
786
+ Trae la familia: las variantes del producto, o sus hermanas si el abierto es
787
+ una de ellas. Se pide al abrir la sección y no al montar la pantalla: la
788
+ mayoría de los productos no son familia.
789
+ */
790
+ const loadFamily = async () => {
791
+ if (family) return family;
792
+ try {
793
+ const res = await axios.post(
794
+ `${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
795
+ { articleId: state.product?.id_article },
796
+ { headers: { Authorization: token } }
797
+ );
798
+ const data = readPayload(res);
799
+ setFamily(data);
800
+ return data;
801
+ } catch (error) {
802
+ console.error("loadFamily: ", error);
803
+ setFamily({ variants: [], parent: null, error: true });
804
+ return null;
805
+ }
806
+ };
807
+
808
+ /*
809
+ Abre otra variante. Esta pantalla lee el producto de sessionStorage, no de la
810
+ URL, así que hay que dejarlo ahí antes de navegar; si no, cambia la dirección
811
+ y sigue mostrando el producto anterior.
812
+
813
+ Se pide por ID y no por UPC: hay 12,024 UPC repetidos entre artículos activos
814
+ y 5,579 de esos casos caen dentro de la misma compañía.
815
+ */
816
+ const openVariant = async (articleId) => {
817
+ setLoadingVariant(articleId);
818
+ try {
819
+ const res = await axios.get(`${process.env.REACT_APP_NEW_ARTICLES_ENDPOINT}`, {
820
+ params: { listType: "general", filterByArticleId: articleId },
821
+ headers: { Authorization: token },
822
+ });
823
+
824
+ const { data } = readPayload(res);
825
+ const encontrado = (data ?? []).find(
826
+ (item) => Number(item.id_article) === Number(articleId)
827
+ );
828
+
829
+ if (encontrado) {
830
+ sessionStorage.setItem(
831
+ "productSelected",
832
+ JSON.stringify({
833
+ ...encontrado,
834
+ article: { id_article: encontrado.id_article },
835
+ })
836
+ );
837
+ }
838
+ } catch (error) {
839
+ // Se navega igual: dejar el clic sin efecto parecería que no responde.
840
+ console.error("openVariant: ", error);
841
+ } finally {
842
+ setLoadingVariant(null);
843
+ window.location.assign("/EditProducts");
844
+ }
845
+ };
846
+
847
+ /*
848
+ Refresca tras ligar, sin recargar: el producto sale de sessionStorage, así que
849
+ un refresh devolvería el mismo objeto y el contador seguiría igual.
850
+ */
851
+ const handleOnLinkedVariants = (result) => {
852
+ if (!result) return;
853
+
854
+ const updatedProduct =
855
+ result.mode === "children"
856
+ ? {
857
+ ...state.product,
858
+ variantsCount:
859
+ (state.product?.variantsCount ?? 0) + (result.linked ?? []).length,
860
+ }
861
+ : {
862
+ ...state.product,
863
+ parentArticleId: result.parentArticleId ?? null,
864
+ variantsCount: 0,
865
+ };
866
+
867
+ dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
868
+ setFamily(null);
869
+
870
+ try {
871
+ sessionStorage.setItem("productSelected", JSON.stringify(updatedProduct));
872
+ } catch (error) {
873
+ console.error("linkVariants/sessionStorage: ", error);
874
+ }
875
+ };
876
+
877
+ /*
878
+ Acción "Volver a heredar" de los chips. Repinta en sitio con lo que devuelve
879
+ el endpoint: recargar la ficha se llevaría lo que haya sin guardar.
880
+ */
881
+ const handleOnRestoreInheritance = async (attributeId) => {
882
+ if (!attributeId || !state.product?.id_article) return;
883
+
884
+ try {
885
+ const res = await axios.post(
886
+ `${process.env.REACT_APP_VARIANTS_ENDPOINT}/restore`,
887
+ { articleId: state.product.id_article, attributes: [Number(attributeId)] },
888
+ { headers: { Authorization: token } }
889
+ );
890
+
891
+ const data = readPayload(res);
892
+
893
+ dispatch({
894
+ type: "UPDATE_DATASHEETS_INPUTS",
895
+ payload: (data.values || []).map((item) => ({
896
+ attributeId: item.id,
897
+ value: item.value,
898
+ isOverridden: 0,
899
+ })),
900
+ });
901
+ } catch (error) {
902
+ console.error("restoreInheritance: ", error);
903
+ }
904
+ };
905
+
906
+ /*
907
+ Los atributos recién tocados, con el nombre que ve el usuario:
908
+ `updated_datasheets_inputs` solo trae { attributeId, value }.
909
+ */
910
+ const cambiosDeFichaTecnica = () => {
911
+ const retailerId = state.active_retailer?.id_retailer;
912
+ const inputs = state.services?.datasheets?.inputs ?? {};
913
+
914
+ return (state.updated_datasheets_inputs ?? []).map((item) => {
915
+ const input = inputs[`${item.attributeId}_${retailerId}`];
916
+ return {
917
+ id: Number(item.attributeId),
918
+ name: input?.name ?? `Atributo ${item.attributeId}`,
919
+ value: item.value,
920
+ };
921
+ });
922
+ };
923
+
774
924
  const handleOnClickSave = async () => {
775
- if (state.saving) return;
925
+ if (state.saving || preparandoPropagacion) return;
776
926
 
777
927
  switch (state.active_tab) {
778
928
  case "Descripción":
779
929
  await saveDescriptions(token);
780
930
  break;
781
- case "Ficha técnica":
931
+ case "Ficha técnica": {
932
+ // Guardar un padre con variantes abre la decisión de propagar. Sin este
933
+ // paso el cambio se queda solo en el padre y las variantes se van
934
+ // separando de él sin que nadie lo note.
935
+ const esPadre = state.product?.variantsCount > 0;
936
+ const cambios = cambiosDeFichaTecnica();
937
+
938
+ if (esPadre && cambios.length > 0) {
939
+ setPreparandoPropagacion(true);
940
+ loadFamily()
941
+ .then(() => setPendingPropagation({ cambios }))
942
+ .finally(() => setPreparandoPropagacion(false));
943
+ return;
944
+ }
945
+
782
946
  await saveDatasheets(token);
783
947
  break;
948
+ }
784
949
  case "Imágenes":
785
950
  await updateImages(token);
786
951
  break;
@@ -1534,6 +1699,29 @@ const RetailerProductEditionView = ({
1534
1699
  state={state}
1535
1700
  >
1536
1701
  <Container headerTop={headerTop}>
1702
+ {pendingPropagation && (
1703
+ <PropagateChangesModal
1704
+ changedAttributes={pendingPropagation.cambios}
1705
+ variants={(family?.variants ?? []).filter((v) => !v.isCurrent)}
1706
+ saving={state.saving}
1707
+ onCancel={() => setPendingPropagation(null)}
1708
+ onConfirm={(propagation) => {
1709
+ setPendingPropagation(null);
1710
+ saveDatasheets(token, propagation ?? undefined);
1711
+ }}
1712
+ />
1713
+ )}
1714
+ {showLinkVariants && (
1715
+ <LinkVariantsModal
1716
+ articleId={state.product?.id_article}
1717
+ articleName={state.product?.name}
1718
+ articleUpc={state.product?.upc}
1719
+ activeRetailerId={state.active_retailer?.id_retailer}
1720
+ token={token}
1721
+ onClose={() => setShowLinkVariants(false)}
1722
+ onLinked={handleOnLinkedVariants}
1723
+ />
1724
+ )}
1537
1725
  {showRejectModal && (
1538
1726
  <Modal
1539
1727
  title={
@@ -1738,6 +1926,105 @@ const RetailerProductEditionView = ({
1738
1926
  </div>
1739
1927
 
1740
1928
  <div className="product-information">
1929
+ {/* Solo si el producto NO es ya variante de alguien: ligarlo otra vez
1930
+ devolvería VARIANT_ALREADY_LINKED sin excepción. */}
1931
+ {!state.product?.parentArticleId && (
1932
+ <div className="variants-actions">
1933
+ <button
1934
+ type="button"
1935
+ className="link-variants"
1936
+ onClick={() => setShowLinkVariants(true)}
1937
+ >
1938
+ Ligar productos
1939
+ </button>
1940
+ {state.product?.variantsCount > 0 && (
1941
+ <button
1942
+ type="button"
1943
+ className="variants-count"
1944
+ aria-expanded={showFamily}
1945
+ onClick={() => {
1946
+ setShowFamily((abierto) => !abierto);
1947
+ loadFamily();
1948
+ }}
1949
+ >
1950
+ {state.product.variantsCount} variante
1951
+ {state.product.variantsCount === 1 ? "" : "s"}
1952
+ <span aria-hidden="true">{showFamily ? " ▴" : " ▾"}</span>
1953
+ </button>
1954
+ )}
1955
+ </div>
1956
+ )}
1957
+
1958
+ {state.product?.parentArticleId && (
1959
+ <div className="variants-actions">
1960
+ <button
1961
+ type="button"
1962
+ className="variants-count"
1963
+ aria-expanded={showFamily}
1964
+ onClick={() => {
1965
+ setShowFamily((abierto) => !abierto);
1966
+ loadFamily();
1967
+ }}
1968
+ >
1969
+ Variante de otro producto
1970
+ <span aria-hidden="true">{showFamily ? " ▴" : " ▾"}</span>
1971
+ </button>
1972
+ </div>
1973
+ )}
1974
+
1975
+ {showFamily && (
1976
+ <div className="variants-family">
1977
+ {!family && <p className="loading">Cargando familia…</p>}
1978
+
1979
+ {family?.error && (
1980
+ <p className="loading">
1981
+ No se pudo cargar la familia de este producto.
1982
+ </p>
1983
+ )}
1984
+
1985
+ {family?.parent && (
1986
+ <button
1987
+ type="button"
1988
+ className="family-row parent"
1989
+ disabled={loadingVariant !== null}
1990
+ onClick={() => openVariant(family.parent.articleId)}
1991
+ >
1992
+ <span className="role">
1993
+ {loadingVariant === family.parent.articleId
1994
+ ? "Abriendo…"
1995
+ : "Producto padre"}
1996
+ </span>
1997
+ <span className="name">{family.parent.name}</span>
1998
+ <span className="upc">{family.parent.upc}</span>
1999
+ </button>
2000
+ )}
2001
+
2002
+ {family?.variants?.map((variant) => (
2003
+ <button
2004
+ key={variant.articleId}
2005
+ type="button"
2006
+ className={`family-row${variant.isCurrent ? " current" : ""}`}
2007
+ disabled={variant.isCurrent || loadingVariant !== null}
2008
+ onClick={() => openVariant(variant.articleId)}
2009
+ >
2010
+ <span className="role">
2011
+ {loadingVariant === variant.articleId
2012
+ ? "Abriendo…"
2013
+ : variant.isCurrent
2014
+ ? "Estás aquí"
2015
+ : "Variante"}
2016
+ </span>
2017
+ <span className="name">{variant.name}</span>
2018
+ <span className="upc">{variant.upc}</span>
2019
+ </button>
2020
+ ))}
2021
+
2022
+ {family && !family.error && !family.variants?.length && (
2023
+ <p className="loading">Este producto no tiene variantes.</p>
2024
+ )}
2025
+ </div>
2026
+ )}
2027
+
1741
2028
  <FullProductNameHeader
1742
2029
  // states
1743
2030
  headerData={state.product}
@@ -1845,6 +2132,8 @@ const RetailerProductEditionView = ({
1845
2132
  activeSection={state.active_tab}
1846
2133
  inputGroup={dataGroup}
1847
2134
  dataInputs={state.datasheets_inputs[1]}
2135
+ variant={state.services?.datasheets?.variant}
2136
+ onRestoreInheritance={handleOnRestoreInheritance}
1848
2137
  auditInputs={auditDatasheets[1]}
1849
2138
  updatedDatasheets={
1850
2139
  state.updated_datasheets_inputs
@@ -1,6 +1,9 @@
1
1
  import styled from "styled-components";
2
2
  import { FontFamily, GlobalColors } from "../../../global-files/variables";
3
3
 
4
+ // Rosa ContentOh, el mismo del modal de ligar productos.
5
+ const PINK = "#E13AA8";
6
+
4
7
  export const Container = styled.div`
5
8
  display: flex;
6
9
  flex-direction: column;
@@ -166,4 +169,97 @@ export const Container = styled.div`
166
169
  }
167
170
  }
168
171
  }
172
+
173
+ .variants-actions {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 10px;
177
+ margin-bottom: 8px;
178
+
179
+ .link-variants {
180
+ border: 1px solid ${PINK};
181
+ border-radius: 6px;
182
+ background: none;
183
+ padding: 6px 12px;
184
+ font-family: ${FontFamily.Raleway_600};
185
+ font-size: 11px;
186
+ color: ${PINK};
187
+ cursor: pointer;
188
+ }
189
+
190
+ .variants-count {
191
+ border: none;
192
+ background: none;
193
+ padding: 0;
194
+ cursor: pointer;
195
+ font-family: ${FontFamily.Raleway_500};
196
+ font-size: 11px;
197
+ color: ${GlobalColors.gray};
198
+ text-decoration: underline;
199
+ }
200
+ }
201
+
202
+ .variants-family {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 4px;
206
+ margin-bottom: 10px;
207
+ padding: 8px;
208
+ border: 1px solid ${GlobalColors.s2};
209
+ border-radius: 6px;
210
+
211
+ .loading {
212
+ margin: 0;
213
+ font-family: ${FontFamily.Raleway_500};
214
+ font-size: 11px;
215
+ color: ${GlobalColors.gray};
216
+ }
217
+
218
+ .family-row {
219
+ display: flex;
220
+ align-items: baseline;
221
+ gap: 8px;
222
+ padding: 6px 8px;
223
+ border: none;
224
+ border-radius: 4px;
225
+ background: none;
226
+ cursor: pointer;
227
+ text-align: left;
228
+ font-family: ${FontFamily.Raleway_500};
229
+ font-size: 12px;
230
+ color: ${GlobalColors.deep_gray};
231
+ }
232
+
233
+ .family-row:hover:not(:disabled) {
234
+ background-color: ${GlobalColors.s2};
235
+ }
236
+
237
+ .family-row:disabled {
238
+ cursor: default;
239
+ opacity: 0.7;
240
+ }
241
+
242
+ .family-row .role {
243
+ min-width: 96px;
244
+ font-size: 10px;
245
+ color: ${PINK};
246
+ }
247
+
248
+ .family-row.current .role {
249
+ color: ${GlobalColors.gray};
250
+ }
251
+
252
+ .family-row .name {
253
+ flex: 1;
254
+ overflow: hidden;
255
+ text-overflow: ellipsis;
256
+ white-space: nowrap;
257
+ }
258
+
259
+ .family-row .upc {
260
+ font-size: 11px;
261
+ color: ${GlobalColors.gray};
262
+ }
263
+ }
169
264
  `;
265
+
@@ -114,6 +114,14 @@ export const normalizeProduct = (product) => {
114
114
  percentages: productData.percentages || null,
115
115
  summary: productData.summary || null,
116
116
  version: productData.version || 0,
117
+
118
+ // Parentesco. read-articles-v2 ya lo devuelve; sin arrastrarlo hasta aquí la
119
+ // pantalla no puede saber si el producto pertenece a una familia.
120
+ // `?? null` en vez de `|| null`: variantsCount 0 es un dato válido.
121
+ parentArticleId:
122
+ productData.parentArticleId ?? article.parentArticleId ?? null,
123
+ parent: productData.parent ?? article.parent ?? null,
124
+ variantsCount: productData.variantsCount ?? article.variantsCount ?? 0,
117
125
  id_order: productData.orderId || productData.id_order || 0,
118
126
  status: productData.status || null,
119
127
  datasheet_status: productData.datasheet_status || null,