contentoh-components-library 21.6.30 → 21.6.32

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-radius: 4px;\n text-align: left;\n font-family: ", ";\n font-size: 12px;\n color: ", ";\n }\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, 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.32",
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,13 @@ 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 [pendingPropagation, setPendingPropagation] = useState(null);
189
+ const [preparandoPropagacion, setPreparandoPropagacion] = useState(false);
190
+
181
191
  const [auditServices, setAuditServices] = useState([]);
182
192
  const [auditDatasheets, setAuditDatasheets] = useState([]);
183
193
  const [auditDescriptions, setAuditDescriptions] = useState([]);
@@ -771,16 +781,175 @@ const RetailerProductEditionView = ({
771
781
  }
772
782
  };
773
783
 
784
+ /*
785
+ Trae la familia: las variantes del producto, o sus hermanas si el abierto es
786
+ una de ellas. Se pide al abrir la sección y no al montar la pantalla: la
787
+ mayoría de los productos no son familia.
788
+ */
789
+ const loadFamily = async (forzar = false) => {
790
+ if (family && !forzar) return family;
791
+ try {
792
+ const res = await axios.post(
793
+ `${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
794
+ { articleId: state.product?.id_article },
795
+ { headers: { Authorization: token } }
796
+ );
797
+ const data = readPayload(res);
798
+ setFamily(data);
799
+ return data;
800
+ } catch (error) {
801
+ console.error("loadFamily: ", error);
802
+ setFamily({ variants: [], parent: null, error: true });
803
+ return null;
804
+ }
805
+ };
806
+
807
+ /*
808
+ En Creators el parentesco se pregunta, no viene con el producto.
809
+
810
+ Su listado sale del endpoint de reportes, que no trae `variantsCount` ni
811
+ `parentArticleId`: esas columnas se agregaron a las vistas pero DynamoDB no
812
+ las tiene hasta que se corra el recorrido de datos. Y aunque las tuviera,
813
+ dependería de lo actualizado que esté ese recorrido.
814
+
815
+ Preguntar a `family` cuesta una llamada por ficha y da el dato de la base, no
816
+ de una copia. Además aplica el alcance del perfil (§5.1): al equipo interno le
817
+ devuelve los productos solicitados a ContentOh, que es justo lo que puede
818
+ tocar.
819
+ */
820
+ useEffect(() => {
821
+ if (!state.product?.id_article) return;
822
+ loadFamily(true);
823
+ // eslint-disable-next-line react-hooks/exhaustive-deps
824
+ }, [state.product?.id_article]);
825
+
826
+ // El conteo sale de la familia recién consultada, no del producto.
827
+ const variantesDelProducto = family?.error
828
+ ? 0
829
+ : (family?.variants ?? []).filter((v) => !v.isCurrent).length;
830
+
831
+ const esVarianteDeOtro = family?.error
832
+ ? !!state.product?.parentArticleId
833
+ : !!family?.parent;
834
+
835
+ /*
836
+ En Creators la lista de familia es INFORMATIVA: no se salta a otra variante.
837
+
838
+ Saltar exige precargar el producto destino, y el endpoint que hace eso en la
839
+ aplicación de clientes —read-articles-v2— filtra por la compañía del usuario:
840
+ `AND a.company_id = <la del usuario>`. El equipo interno pertenece a ContentOh
841
+ y trabaja productos de sus clientes, así que ahí no devuelve nada.
842
+
843
+ Navegar sin precargar sería peor que no ofrecerlo: esta pantalla lee el
844
+ producto de sessionStorage, así que cambiaría la URL y seguiría mostrando el
845
+ producto anterior, dando a entender que se cambió de variante cuando no.
846
+
847
+ Para habilitarlo hace falta una forma de traer un producto por id sin filtro
848
+ de compañía, respetando el alcance del perfil interno (§5.1 del contrato).
849
+ */
850
+
851
+ /*
852
+ Refresca tras ligar, sin recargar: el producto sale de sessionStorage, así que
853
+ un refresh devolvería el mismo objeto y el contador seguiría igual.
854
+ */
855
+ const handleOnLinkedVariants = (result) => {
856
+ if (!result) return;
857
+
858
+ const updatedProduct =
859
+ result.mode === "children"
860
+ ? {
861
+ ...state.product,
862
+ variantsCount:
863
+ (state.product?.variantsCount ?? 0) + (result.linked ?? []).length,
864
+ }
865
+ : {
866
+ ...state.product,
867
+ parentArticleId: result.parentArticleId ?? null,
868
+ variantsCount: 0,
869
+ };
870
+
871
+ dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
872
+ setFamily(null);
873
+
874
+ try {
875
+ sessionStorage.setItem("productSelected", JSON.stringify(updatedProduct));
876
+ } catch (error) {
877
+ console.error("linkVariants/sessionStorage: ", error);
878
+ }
879
+ };
880
+
881
+ /*
882
+ Acción "Volver a heredar" de los chips. Repinta en sitio con lo que devuelve
883
+ el endpoint: recargar la ficha se llevaría lo que haya sin guardar.
884
+ */
885
+ const handleOnRestoreInheritance = async (attributeId) => {
886
+ if (!attributeId || !state.product?.id_article) return;
887
+
888
+ try {
889
+ const res = await axios.post(
890
+ `${process.env.REACT_APP_VARIANTS_ENDPOINT}/restore`,
891
+ { articleId: state.product.id_article, attributes: [Number(attributeId)] },
892
+ { headers: { Authorization: token } }
893
+ );
894
+
895
+ const data = readPayload(res);
896
+
897
+ dispatch({
898
+ type: "UPDATE_DATASHEETS_INPUTS",
899
+ payload: (data.values || []).map((item) => ({
900
+ attributeId: item.id,
901
+ value: item.value,
902
+ isOverridden: 0,
903
+ })),
904
+ });
905
+ } catch (error) {
906
+ console.error("restoreInheritance: ", error);
907
+ }
908
+ };
909
+
910
+ /*
911
+ Los atributos recién tocados, con el nombre que ve el usuario:
912
+ `updated_datasheets_inputs` solo trae { attributeId, value }.
913
+ */
914
+ const cambiosDeFichaTecnica = () => {
915
+ const retailerId = state.active_retailer?.id_retailer;
916
+ const inputs = state.services?.datasheets?.inputs ?? {};
917
+
918
+ return (state.updated_datasheets_inputs ?? []).map((item) => {
919
+ const input = inputs[`${item.attributeId}_${retailerId}`];
920
+ return {
921
+ id: Number(item.attributeId),
922
+ name: input?.name ?? `Atributo ${item.attributeId}`,
923
+ value: item.value,
924
+ };
925
+ });
926
+ };
927
+
774
928
  const handleOnClickSave = async () => {
775
- if (state.saving) return;
929
+ if (state.saving || preparandoPropagacion) return;
776
930
 
777
931
  switch (state.active_tab) {
778
932
  case "Descripción":
779
933
  await saveDescriptions(token);
780
934
  break;
781
- case "Ficha técnica":
935
+ case "Ficha técnica": {
936
+ // Guardar un padre con variantes abre la decisión de propagar. Sin este
937
+ // paso el cambio se queda solo en el padre y las variantes se van
938
+ // separando de él sin que nadie lo note.
939
+ const esPadre = variantesDelProducto > 0;
940
+ const cambios = cambiosDeFichaTecnica();
941
+
942
+ if (esPadre && cambios.length > 0) {
943
+ setPreparandoPropagacion(true);
944
+ loadFamily()
945
+ .then(() => setPendingPropagation({ cambios }))
946
+ .finally(() => setPreparandoPropagacion(false));
947
+ return;
948
+ }
949
+
782
950
  await saveDatasheets(token);
783
951
  break;
952
+ }
784
953
  case "Imágenes":
785
954
  await updateImages(token);
786
955
  break;
@@ -1534,6 +1703,29 @@ const RetailerProductEditionView = ({
1534
1703
  state={state}
1535
1704
  >
1536
1705
  <Container headerTop={headerTop}>
1706
+ {pendingPropagation && (
1707
+ <PropagateChangesModal
1708
+ changedAttributes={pendingPropagation.cambios}
1709
+ variants={(family?.variants ?? []).filter((v) => !v.isCurrent)}
1710
+ saving={state.saving}
1711
+ onCancel={() => setPendingPropagation(null)}
1712
+ onConfirm={(propagation) => {
1713
+ setPendingPropagation(null);
1714
+ saveDatasheets(token, propagation ?? undefined);
1715
+ }}
1716
+ />
1717
+ )}
1718
+ {showLinkVariants && (
1719
+ <LinkVariantsModal
1720
+ articleId={state.product?.id_article}
1721
+ articleName={state.product?.name}
1722
+ articleUpc={state.product?.upc}
1723
+ activeRetailerId={state.active_retailer?.id_retailer}
1724
+ token={token}
1725
+ onClose={() => setShowLinkVariants(false)}
1726
+ onLinked={handleOnLinkedVariants}
1727
+ />
1728
+ )}
1537
1729
  {showRejectModal && (
1538
1730
  <Modal
1539
1731
  title={
@@ -1738,6 +1930,89 @@ const RetailerProductEditionView = ({
1738
1930
  </div>
1739
1931
 
1740
1932
  <div className="product-information">
1933
+ {/* Solo si el producto NO es ya variante de alguien: ligarlo otra vez
1934
+ devolvería VARIANT_ALREADY_LINKED sin excepción. */}
1935
+ {!esVarianteDeOtro && (
1936
+ <div className="variants-actions">
1937
+ <button
1938
+ type="button"
1939
+ className="link-variants"
1940
+ onClick={() => setShowLinkVariants(true)}
1941
+ >
1942
+ Ligar productos
1943
+ </button>
1944
+ {variantesDelProducto > 0 && (
1945
+ <button
1946
+ type="button"
1947
+ className="variants-count"
1948
+ aria-expanded={showFamily}
1949
+ onClick={() => {
1950
+ setShowFamily((abierto) => !abierto);
1951
+ loadFamily();
1952
+ }}
1953
+ >
1954
+ {variantesDelProducto} variante
1955
+ {variantesDelProducto === 1 ? "" : "s"}
1956
+ <span aria-hidden="true">{showFamily ? " ▴" : " ▾"}</span>
1957
+ </button>
1958
+ )}
1959
+ </div>
1960
+ )}
1961
+
1962
+ {esVarianteDeOtro && (
1963
+ <div className="variants-actions">
1964
+ <button
1965
+ type="button"
1966
+ className="variants-count"
1967
+ aria-expanded={showFamily}
1968
+ onClick={() => {
1969
+ setShowFamily((abierto) => !abierto);
1970
+ loadFamily();
1971
+ }}
1972
+ >
1973
+ Variante de otro producto
1974
+ <span aria-hidden="true">{showFamily ? " ▴" : " ▾"}</span>
1975
+ </button>
1976
+ </div>
1977
+ )}
1978
+
1979
+ {showFamily && (
1980
+ <div className="variants-family">
1981
+ {!family && <p className="loading">Cargando familia…</p>}
1982
+
1983
+ {family?.error && (
1984
+ <p className="loading">
1985
+ No se pudo cargar la familia de este producto.
1986
+ </p>
1987
+ )}
1988
+
1989
+ {family?.parent && (
1990
+ <div className="family-row parent">
1991
+ <span className="role">Producto padre</span>
1992
+ <span className="name">{family.parent.name}</span>
1993
+ <span className="upc">{family.parent.upc}</span>
1994
+ </div>
1995
+ )}
1996
+
1997
+ {family?.variants?.map((variant) => (
1998
+ <div
1999
+ key={variant.articleId}
2000
+ className={`family-row${variant.isCurrent ? " current" : ""}`}
2001
+ >
2002
+ <span className="role">
2003
+ {variant.isCurrent ? "Estás aquí" : "Variante"}
2004
+ </span>
2005
+ <span className="name">{variant.name}</span>
2006
+ <span className="upc">{variant.upc}</span>
2007
+ </div>
2008
+ ))}
2009
+
2010
+ {family && !family.error && !family.variants?.length && (
2011
+ <p className="loading">Este producto no tiene variantes.</p>
2012
+ )}
2013
+ </div>
2014
+ )}
2015
+
1741
2016
  <FullProductNameHeader
1742
2017
  // states
1743
2018
  headerData={state.product}
@@ -1845,6 +2120,8 @@ const RetailerProductEditionView = ({
1845
2120
  activeSection={state.active_tab}
1846
2121
  inputGroup={dataGroup}
1847
2122
  dataInputs={state.datasheets_inputs[1]}
2123
+ variant={state.services?.datasheets?.variant}
2124
+ onRestoreInheritance={handleOnRestoreInheritance}
1848
2125
  auditInputs={auditDatasheets[1]}
1849
2126
  updatedDatasheets={
1850
2127
  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,86 @@ 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-radius: 4px;
224
+ text-align: left;
225
+ font-family: ${FontFamily.Raleway_500};
226
+ font-size: 12px;
227
+ color: ${GlobalColors.deep_gray};
228
+ }
229
+
230
+
231
+ .family-row .role {
232
+ min-width: 96px;
233
+ font-size: 10px;
234
+ color: ${PINK};
235
+ }
236
+
237
+ .family-row.current .role {
238
+ color: ${GlobalColors.gray};
239
+ }
240
+
241
+ .family-row .name {
242
+ flex: 1;
243
+ overflow: hidden;
244
+ text-overflow: ellipsis;
245
+ white-space: nowrap;
246
+ }
247
+
248
+ .family-row .upc {
249
+ font-size: 11px;
250
+ color: ${GlobalColors.gray};
251
+ }
252
+ }
169
253
  `;
254
+
@@ -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,