feat(propuesta): pasarela paso a paso con autodetección de ubicación y FAB
- Campo universal de ubicación (Google Maps/Waze/OSM/coordenadas/dirección) sobre proveedores extensibles (src/utils/ubicacion) con resolver e inferencia inversa Nominatim (nombre/provincia pre-rellenados, alias cooficiales) - FAB flotante abajo a la derecha que abre la pasarela en un modal accesible (Escape, foco contenido, responsive); sin descripción ni puntuación; avisos de duplicado y filtro de palabras dentro del modal - Aviso PWA reposicionado: tarjeta abajo a la izquierda en móvil, botón compacto arriba a la izquierda en PC (no tapa el FAB) - Suite de pruebas: 88 unitarias/componente (vitest + RTL, offline) y 10 de integración (supertest + SQLite :memory: y Nominatim real opt-in) - CI: workflow de Gitea Actions con npm test + build en push/PR e integración manual
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { CATEGORIAS } from "../constants/categorias.js";
|
||||
import { ETIQUETAS_PROVEEDOR } from "../utils/ubicacion/index.js";
|
||||
|
||||
const fila = { display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 10, padding: "6px 0", borderBottom: "0.5px solid var(--color-border-tertiary)", fontSize: 13 };
|
||||
const clave = { color: "var(--color-text-secondary)", flexShrink: 0 };
|
||||
const valor = { textAlign: "right", fontWeight: 500, wordBreak: "break-word" };
|
||||
|
||||
// Paso 6 (final) de la pasarela: resumen legible de todo lo introducido,
|
||||
// aceptación obligatoria de la política de privacidad y envío de la
|
||||
// propuesta. Los avisos de palabra prohibida y de posible duplicado se
|
||||
// muestran dentro del propio modal.
|
||||
export default function PasoResumen({
|
||||
form, aceptaPrivacidad, setAceptaPrivacidad, onAbrirPrivacidad,
|
||||
onEnviar, onConfirmarDuplicado, onDescartarDuplicado,
|
||||
errorFiltro, posibleDuplicado, comprobandoDuplicado, saving,
|
||||
}) {
|
||||
const categoria = form.categoria ? CATEGORIAS[form.categoria] : null;
|
||||
const enviando = comprobandoDuplicado || saving;
|
||||
const etiquetaProveedor = form.proveedorUbicacion ? ETIQUETAS_PROVEEDOR[form.proveedorUbicacion] || form.proveedorUbicacion : "";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p style={{ margin: "0 0 8px", fontSize: 13, color: "var(--color-text-secondary)" }}>
|
||||
Comprueba los datos antes de enviar. El administrador revisará la propuesta antes de publicarla.
|
||||
</p>
|
||||
|
||||
<div style={{ background: "var(--color-background-secondary)", borderRadius: "var(--border-radius-md)", padding: "4px 12px", marginBottom: 12 }}>
|
||||
<div style={fila}>
|
||||
<span style={clave}>📍 Ubicación</span>
|
||||
<span style={valor}>
|
||||
{etiquetaProveedor && <>{etiquetaProveedor}<br /></>}
|
||||
{form.lat != null && form.lng != null && <span style={{ fontWeight: 400 }}>{form.lat.toFixed(4)}, {form.lng.toFixed(4)}</span>}
|
||||
{form.direccion && <span style={{ fontWeight: 400, display: "block" }}>{form.direccion}</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div style={fila}>
|
||||
<span style={clave}>🏷️ Nombre</span>
|
||||
<span style={valor}>{form.nombre}</span>
|
||||
</div>
|
||||
<div style={fila}>
|
||||
<span style={clave}>🗺️ Provincia</span>
|
||||
<span style={valor}>{form.provincia}</span>
|
||||
</div>
|
||||
<div style={fila}>
|
||||
<span style={clave}>📂 Categoría</span>
|
||||
<span style={valor}>{categoria ? `${categoria.emoji} ${form.categoria}` : ""}</span>
|
||||
</div>
|
||||
<div style={{ ...fila, borderBottom: "none" }}>
|
||||
<span style={clave}>🔖 Tipo específico</span>
|
||||
<span style={{ ...valor, fontWeight: form.subcategoria ? 500 : 400, color: form.subcategoria ? "inherit" : "var(--color-text-tertiary)" }}>
|
||||
{form.subcategoria || "—"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label style={{ display: "flex", alignItems: "flex-start", gap: 8, marginBottom: 12, fontSize: 12, color: "var(--color-text-secondary)", cursor: "pointer" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aceptaPrivacidad}
|
||||
onChange={(e) => setAceptaPrivacidad(e.target.checked)}
|
||||
style={{ marginTop: 2 }}
|
||||
/>
|
||||
<span>
|
||||
He leído y acepto la{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.preventDefault(); onAbrirPrivacidad(); }}
|
||||
style={{ background: "none", border: "none", padding: 0, color: "#8B0000", textDecoration: "underline", cursor: "pointer", fontSize: 12 }}
|
||||
>
|
||||
política de privacidad
|
||||
</button>
|
||||
{" "}sobre cómo se usan estos datos. <span style={{ color: "#8B0000" }}>*</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Aviso de palabra prohibida */}
|
||||
{errorFiltro && (
|
||||
<div role="alert" style={{ marginBottom: 12, background: "#FDEDEC", border: "1px solid #F5B7B1", borderRadius: "var(--border-radius-md)", padding: "10px 14px", fontSize: 13, color: "#922B21", display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<i className="ti ti-ban" aria-hidden="true"></i> {errorFiltro}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aviso de posible duplicado */}
|
||||
{posibleDuplicado && (
|
||||
<div role="alert" style={{ marginBottom: 12, background: "#FDF2E9", border: "1px solid #F5CBA7", borderRadius: "var(--border-radius-md)", padding: "10px 14px", fontSize: 13, color: "#784212" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 500 }}>
|
||||
<i className="ti ti-alert-triangle" aria-hidden="true"></i> Ya existe un local parecido
|
||||
</div>
|
||||
<ul style={{ margin: "6px 0", paddingLeft: 20 }}>
|
||||
{posibleDuplicado.coincidencias.map((c) => (
|
||||
<li key={c.id}>{c.nombre} · {c.provincia}{c.direccion ? ` · ${c.direccion}` : ""} ({c.estado === "pendiente" ? "pendiente de revisión" : "ya publicado"})</li>
|
||||
))}
|
||||
</ul>
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 4 }}>
|
||||
<button onClick={onConfirmarDuplicado} disabled={saving}
|
||||
style={{ background: "#8B0000", color: "white", border: "none", borderRadius: "var(--border-radius-md)", padding: "6px 14px", fontSize: 12, fontWeight: 500, cursor: "pointer" }}>
|
||||
{saving ? "Enviando..." : "Enviar de todos modos"}
|
||||
</button>
|
||||
<button onClick={onDescartarDuplicado} style={{ background: "none", border: "0.5px solid var(--color-border-secondary)", borderRadius: "var(--border-radius-md)", padding: "6px 14px", fontSize: 12, cursor: "pointer", color: "var(--color-text-secondary)" }}>Revisar datos</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onEnviar}
|
||||
disabled={!aceptaPrivacidad || enviando}
|
||||
style={{
|
||||
background: aceptaPrivacidad ? "#8B0000" : "var(--color-background-secondary)",
|
||||
color: aceptaPrivacidad ? "white" : "var(--color-text-tertiary)",
|
||||
border: "none", borderRadius: "var(--border-radius-md)",
|
||||
padding: "10px 20px", fontSize: 14, fontWeight: 500, width: "100%",
|
||||
cursor: aceptaPrivacidad && !enviando ? "pointer" : "not-allowed",
|
||||
}}
|
||||
>
|
||||
{comprobandoDuplicado ? "Comprobando..." : saving ? "Enviando..." : "📨 Enviar propuesta"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user