contentoh-components-library 21.6.27 → 21.6.29

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.
@@ -0,0 +1,209 @@
1
+ import { useState } from "react";
2
+
3
+ import {
4
+ Overlay,
5
+ Panel,
6
+ Header,
7
+ Body,
8
+ List,
9
+ Choices,
10
+ Note,
11
+ Footer,
12
+ } from "./styles";
13
+
14
+ /*
15
+ Diálogo de propagación al guardar un producto padre.
16
+
17
+ Aparece cuando se guardan cambios de ficha técnica en un producto que tiene
18
+ variantes. El backend NO propaga nada por su cuenta: solo baja a los hijos lo
19
+ que venga en `propagation`, y esa lista la arma esta pantalla. Guardar sin
20
+ abrirla, o cerrarla sin propagar, deja al padre modificado y a las variantes
21
+ como estaban.
22
+
23
+ props (* requerido):
24
+ - *changedAttributes: [{ id, name, value }] lo que se acaba de editar
25
+ - *variants: [{ articleId, name, upc }] las variantes activas del padre
26
+ - *onCancel: cerrar sin guardar nada
27
+ - *onConfirm: (propagation | null) => void
28
+ null → guardar solo el padre
29
+ { attributes, targetChildren } → guardar y propagar
30
+ - saving: deshabilita los botones mientras el guardado corre
31
+ */
32
+
33
+ // Por encima de este número el backend delega la propagación a una invocación
34
+ // en segundo plano. Se avisa aquí porque cambia lo que el usuario debe esperar:
35
+ // el guardado responde antes de que los hijos estén actualizados.
36
+ const UMBRAL_SEGUNDO_PLANO = 20;
37
+
38
+ export const PropagateChangesModal = ({
39
+ changedAttributes = [],
40
+ variants = [],
41
+ onCancel,
42
+ onConfirm,
43
+ saving = false,
44
+ }) => {
45
+ const [chosenAttributes, setChosenAttributes] = useState([]);
46
+ const [scope, setScope] = useState("all");
47
+ const [chosenChildren, setChosenChildren] = useState([]);
48
+
49
+ const destinatarios = scope === "all" ? variants.length : chosenChildren.length;
50
+ const puedePropagar = chosenAttributes.length > 0 && destinatarios > 0;
51
+ const enSegundoPlano = destinatarios > UMBRAL_SEGUNDO_PLANO;
52
+
53
+ const toggleAttribute = (id) => {
54
+ setChosenAttributes((current) =>
55
+ current.includes(id)
56
+ ? current.filter((item) => item !== id)
57
+ : [...current, id]
58
+ );
59
+ };
60
+
61
+ const toggleChild = (id) => {
62
+ setChosenChildren((current) =>
63
+ current.includes(id)
64
+ ? current.filter((item) => item !== id)
65
+ : [...current, id]
66
+ );
67
+ };
68
+
69
+ const confirmar = () => {
70
+ onConfirm?.({
71
+ attributes: chosenAttributes,
72
+ targetChildren: scope === "all" ? "all" : chosenChildren,
73
+ });
74
+ };
75
+
76
+ return (
77
+ <Overlay>
78
+ <Panel onClick={(event) => event.stopPropagation()}>
79
+ <Header>
80
+ <h2>Este producto tiene {variants.length} variante{variants.length === 1 ? "" : "s"}</h2>
81
+ <p>
82
+ Cambiaste {changedAttributes.length} atributo
83
+ {changedAttributes.length === 1 ? "" : "s"}. Elige cuáles bajan a las
84
+ variantes; el resto se guarda solo en este producto.
85
+ </p>
86
+ </Header>
87
+
88
+ <Body>
89
+ <h3>¿Qué se baja a las variantes?</h3>
90
+ <List>
91
+ {changedAttributes.map((attribute) => (
92
+ <li key={attribute.id}>
93
+ <label>
94
+ <input
95
+ type="checkbox"
96
+ checked={chosenAttributes.includes(attribute.id)}
97
+ onChange={() => toggleAttribute(attribute.id)}
98
+ />
99
+ <span>{attribute.name}</span>
100
+ <span className="valor" title={attribute.value}>
101
+ {attribute.value}
102
+ </span>
103
+ </label>
104
+ </li>
105
+ ))}
106
+ </List>
107
+
108
+ <h3>¿A cuáles?</h3>
109
+ <Choices>
110
+ <label>
111
+ <input
112
+ type="radio"
113
+ name="propagation-scope"
114
+ checked={scope === "all"}
115
+ onChange={() => setScope("all")}
116
+ />
117
+ <span>
118
+ A todas
119
+ <span className="hint">
120
+ {variants.length} variante{variants.length === 1 ? "" : "s"} activa
121
+ {variants.length === 1 ? "" : "s"}.
122
+ </span>
123
+ </span>
124
+ </label>
125
+ <label>
126
+ <input
127
+ type="radio"
128
+ name="propagation-scope"
129
+ checked={scope === "some"}
130
+ onChange={() => setScope("some")}
131
+ />
132
+ <span>Elegir cuáles</span>
133
+ </label>
134
+ </Choices>
135
+
136
+ {scope === "some" && (
137
+ <List>
138
+ {variants.map((variant) => (
139
+ <li key={variant.articleId}>
140
+ <label>
141
+ <input
142
+ type="checkbox"
143
+ checked={chosenChildren.includes(variant.articleId)}
144
+ onChange={() => toggleChild(variant.articleId)}
145
+ />
146
+ <span>
147
+ {variant.name}
148
+ <span className="meta">UPC {variant.upc || "—"}</span>
149
+ </span>
150
+ </label>
151
+ </li>
152
+ ))}
153
+ </List>
154
+ )}
155
+
156
+ {/* La diferencia con "ligar", y la que sorprende si no se dice: aquí
157
+ lo que la variante editó a mano NO se pisa. */}
158
+ <Note>
159
+ Una variante que haya editado alguno de estos atributos por su cuenta
160
+ conserva su valor. Para devolverlo al del padre está la acción
161
+ «Volver a heredar» en la ficha de esa variante.
162
+ </Note>
163
+
164
+ {enSegundoPlano && (
165
+ <Note className="accent">
166
+ Son más de {UMBRAL_SEGUNDO_PLANO} variantes, así que la propagación
167
+ corre en segundo plano: el guardado termina antes de que todas estén
168
+ actualizadas.
169
+ </Note>
170
+ )}
171
+ </Body>
172
+
173
+ <Footer>
174
+ <span className="resumen">
175
+ {puedePropagar
176
+ ? `${chosenAttributes.length} atributo(s) a ${destinatarios} variante(s).`
177
+ : "No se bajará nada a las variantes."}
178
+ </span>
179
+ <button
180
+ type="button"
181
+ className="secondary"
182
+ onClick={onCancel}
183
+ disabled={saving}
184
+ >
185
+ Cancelar
186
+ </button>
187
+ {/* Guardar sin propagar es una salida legítima y visible: el usuario
188
+ puede querer cambiar solo el padre. */}
189
+ <button
190
+ type="button"
191
+ className="secondary"
192
+ onClick={() => onConfirm?.(null)}
193
+ disabled={saving}
194
+ >
195
+ Solo este producto
196
+ </button>
197
+ <button
198
+ type="button"
199
+ className="primary"
200
+ onClick={confirmar}
201
+ disabled={saving || !puedePropagar}
202
+ >
203
+ {saving ? "Guardando…" : "Guardar y propagar"}
204
+ </button>
205
+ </Footer>
206
+ </Panel>
207
+ </Overlay>
208
+ );
209
+ };
@@ -0,0 +1,184 @@
1
+ import styled from "styled-components";
2
+ import "../../../global-files/global-styles.css";
3
+ import { GlobalColors, FontFamily } from "../../../global-files/variables";
4
+
5
+ const PINK = "#E13AA8";
6
+ const PINK_RGB = "225, 58, 168";
7
+
8
+ export const Overlay = styled.div`
9
+ position: fixed;
10
+ inset: 0;
11
+ z-index: 110;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ background-color: rgba(40, 31, 51, 0.45);
16
+ `;
17
+
18
+ export const Panel = styled.div`
19
+ display: flex;
20
+ flex-direction: column;
21
+ width: min(560px, 92vw);
22
+ max-height: 86vh;
23
+ border-radius: 10px;
24
+ background-color: ${GlobalColors.white};
25
+ font-family: ${FontFamily.Raleway};
26
+ overflow: hidden;
27
+
28
+ input[type="checkbox"],
29
+ input[type="radio"] {
30
+ accent-color: ${PINK};
31
+ width: 15px;
32
+ height: 15px;
33
+ cursor: pointer;
34
+ }
35
+ `;
36
+
37
+ export const Header = styled.div`
38
+ padding: 20px 24px 8px;
39
+
40
+ h2 {
41
+ margin: 0;
42
+ font-family: ${FontFamily.Raleway_700};
43
+ font-size: 17px;
44
+ color: ${GlobalColors.deep_gray};
45
+ }
46
+
47
+ p {
48
+ margin: 6px 0 0;
49
+ font-size: 13px;
50
+ line-height: 1.4;
51
+ color: ${GlobalColors.gray};
52
+ }
53
+ `;
54
+
55
+ export const Body = styled.div`
56
+ flex: 1;
57
+ overflow-y: auto;
58
+ padding: 12px 24px 0;
59
+
60
+ h3 {
61
+ margin: 0 0 8px;
62
+ font-family: ${FontFamily.Raleway_600};
63
+ font-size: 13px;
64
+ color: ${GlobalColors.deep_gray};
65
+ }
66
+ `;
67
+
68
+ export const List = styled.ul`
69
+ list-style: none;
70
+ margin: 0 0 18px;
71
+ padding: 0;
72
+ border: 1px solid ${GlobalColors.s2};
73
+ border-radius: 6px;
74
+ max-height: 190px;
75
+ overflow-y: auto;
76
+
77
+ li + li {
78
+ border-top: 1px solid ${GlobalColors.s2};
79
+ }
80
+
81
+ label {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 10px;
85
+ padding: 9px 12px;
86
+ cursor: pointer;
87
+ font-size: 13px;
88
+ color: ${GlobalColors.deep_gray};
89
+ }
90
+
91
+ .valor {
92
+ margin-left: auto;
93
+ max-width: 45%;
94
+ font-size: 12px;
95
+ color: ${GlobalColors.gray};
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ white-space: nowrap;
99
+ }
100
+
101
+ .meta {
102
+ display: block;
103
+ font-size: 12px;
104
+ color: ${GlobalColors.gray};
105
+ }
106
+ `;
107
+
108
+ export const Choices = styled.div`
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 2px;
112
+ margin-bottom: 10px;
113
+
114
+ label {
115
+ display: flex;
116
+ align-items: flex-start;
117
+ gap: 8px;
118
+ padding: 4px 0;
119
+ font-size: 13px;
120
+ color: ${GlobalColors.deep_gray};
121
+ cursor: pointer;
122
+ }
123
+
124
+ .hint {
125
+ display: block;
126
+ font-size: 12px;
127
+ color: ${GlobalColors.gray};
128
+ }
129
+ `;
130
+
131
+ export const Note = styled.p`
132
+ margin: 0 0 16px;
133
+ padding: 10px 12px;
134
+ border-radius: 6px;
135
+ font-size: 12px;
136
+ line-height: 1.45;
137
+ background-color: ${GlobalColors.s2};
138
+ color: ${GlobalColors.deep_gray};
139
+
140
+ &.accent {
141
+ background-color: rgba(${PINK_RGB}, 0.1);
142
+ color: ${PINK};
143
+ }
144
+ `;
145
+
146
+ export const Footer = styled.div`
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 10px;
150
+ padding: 16px 24px;
151
+ border-top: 1px solid ${GlobalColors.s2};
152
+
153
+ .resumen {
154
+ flex: 1;
155
+ font-size: 12px;
156
+ color: ${GlobalColors.gray};
157
+ }
158
+
159
+ button {
160
+ height: 38px;
161
+ padding: 0 16px;
162
+ border-radius: 6px;
163
+ font-family: ${FontFamily.Raleway_600};
164
+ font-size: 13px;
165
+ cursor: pointer;
166
+ }
167
+
168
+ button.secondary {
169
+ border: 1px solid ${GlobalColors.s2};
170
+ background-color: ${GlobalColors.white};
171
+ color: ${GlobalColors.gray};
172
+ }
173
+
174
+ button.primary {
175
+ border: none;
176
+ background-color: ${PINK};
177
+ color: ${GlobalColors.white};
178
+ }
179
+
180
+ button:disabled {
181
+ opacity: 0.5;
182
+ cursor: default;
183
+ }
184
+ `;
@@ -118,13 +118,21 @@ export const ProviderProductEditionProvider = ({ children }) => {
118
118
  }
119
119
  };
120
120
 
121
- const saveDatasheets = async (token) => {
121
+ /*
122
+ `propagation` es opcional y solo lo manda la ficha de un producto padre, con
123
+ lo que el usuario eligió en el diálogo: { attributes: [...], targetChildren }.
124
+
125
+ Sin ese bloque el backend no propaga nada. Es deliberado: bajar un cambio a
126
+ las variantes tiene que ser una decisión, no un efecto secundario de guardar.
127
+ */
128
+ const saveDatasheets = async (token, propagation) => {
122
129
  if (state.updated_datasheets_inputs.length === 0) return;
123
130
  dispatch({ type: "SET_SAVING", payload: true });
124
131
 
125
132
  const dataObject = {
126
133
  articleId: state.product?.id_article,
127
134
  articleData: state.updated_datasheets_inputs,
135
+ ...(propagation ? { propagation } : {}),
128
136
  };
129
137
 
130
138
  // Marcar todos los items como aprobados