feat: directorio más fiable y más fácil de usar
deploy-branch / deploy (push) Successful in 2m34s
deploy-branch / teardown (push) Skipped

El objetivo de estos cambios es, por un lado, mantener los datos
actualizados y, por otro, mejorar la experiencia para quienes lo usan:

- Fiabilidad: los locales se pueden confirmar ("Sigue aquí") y
  reportar (cerrado, datos incorrectos...), y se avisa de duplicados
  antes de publicar, para que el contenido no se quede obsoleto ni
  repetido.

- Usabilidad: autocompletado al proponer, favoritos, "cerca de mí",
  compartir fichas, enlaces a Waze y una política de privacidad
  visible, para que sea más sencillo aportar y encontrar locales.
This commit is contained in:
2026-08-10 22:23:35 +02:00
parent 257fb9c622
commit b36867225c
14 changed files with 826 additions and 58 deletions
+14
View File
@@ -43,6 +43,15 @@ db.exec(`
datos TEXT
);
CREATE TABLE IF NOT EXISTS reportes (
id TEXT PRIMARY KEY,
localId TEXT NOT NULL,
motivo TEXT,
comentario TEXT,
fecha TEXT DEFAULT CURRENT_TIMESTAMP,
estado TEXT DEFAULT 'pendiente'
);
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
@@ -69,5 +78,10 @@ for (const table of ["locales", "pendientes"]) {
ensureColumn(table, "enlaceGoogleMaps", "TEXT");
ensureColumn(table, "puntuacion", "INTEGER DEFAULT 0");
}
// fechaConfirmacion: última vez que alguien confirmó que el local sigue activo.
// Al aprobarse por primera vez se inicializa igual a "fecha"; luego se actualiza
// con /api/locales/:id/confirmar cada vez que un usuario pulsa "Sigue aquí".
ensureColumn("locales", "fechaConfirmacion", "TEXT");
db.prepare("UPDATE locales SET fechaConfirmacion = fecha WHERE fechaConfirmacion IS NULL").run();
export default db;
+81 -3
View File
@@ -22,6 +22,41 @@ app.get("/api/pendientes", (_req, res) => {
res.json(pendientes);
});
// Utilidades para detectar duplicados
function normaliza(s) {
return (s || "").toString().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim();
}
function distanciaMetros(lat1, lng1, lat2, lng2) {
const R = 6371000;
const toRad = (d) => (d * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// Comprueba si ya existe un local con el mismo nombre (en la misma provincia o muy
// cerca geográficamente) entre los ya publicados y los pendientes de revisión.
app.post("/api/check-duplicado", (req, res) => {
const { nombre, provincia, lat, lng } = req.body;
const nombreN = normaliza(nombre);
if (!nombreN) return res.json({ duplicado: false, coincidencias: [] });
const candidatos = [
...db.prepare("SELECT id, nombre, provincia, direccion, categoria, lat, lng FROM locales").all().map((l) => ({ ...l, estado: "publicado" })),
...db.prepare("SELECT id, nombre, provincia, direccion, categoria, lat, lng FROM pendientes").all().map((l) => ({ ...l, estado: "pendiente" })),
];
const coincidencias = candidatos.filter((l) => {
if (normaliza(l.nombre) !== nombreN) return false;
const mismaProvincia = provincia && normaliza(l.provincia) === normaliza(provincia);
const cerca = lat && lng && l.lat && l.lng && distanciaMetros(lat, lng, l.lat, l.lng) < 200;
return mismaProvincia || cerca;
});
res.json({ duplicado: coincidencias.length > 0, coincidencias });
});
// Enviar propuesta
app.post("/api/propuestas", (req, res) => {
const { nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, lat, lng } = req.body;
@@ -47,9 +82,9 @@ app.post("/api/aprobar/:id", (req, res) => {
const fecha = new Date().toISOString();
db.prepare(`
INSERT INTO locales (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, fecha, lat, lng)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(localeId, propuesta.nombre, propuesta.descripcion, propuesta.provincia, propuesta.subcategoria, propuesta.direccion, propuesta.enlaceGoogleMaps, propuesta.puntuacion ?? 0, propuesta.categoria, fecha, propuesta.lat, propuesta.lng);
INSERT INTO locales (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, fecha, lat, lng, fechaConfirmacion)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(localeId, propuesta.nombre, propuesta.descripcion, propuesta.provincia, propuesta.subcategoria, propuesta.direccion, propuesta.enlaceGoogleMaps, propuesta.puntuacion ?? 0, propuesta.categoria, fecha, propuesta.lat, propuesta.lng, fecha);
db.prepare("DELETE FROM pendientes WHERE id = ?").run(id);
@@ -67,6 +102,49 @@ app.post("/api/rechazar/:id", (req, res) => {
app.delete("/api/locales/:id", (req, res) => {
const { id } = req.params;
db.prepare("DELETE FROM locales WHERE id = ?").run(id);
db.prepare("DELETE FROM reportes WHERE localId = ?").run(id);
res.json({ ok: true });
});
// Confirmar que un local sigue activo (renueva fechaConfirmacion)
app.post("/api/locales/:id/confirmar", (req, res) => {
const { id } = req.params;
const local = db.prepare("SELECT id FROM locales WHERE id = ?").get(id);
if (!local) return res.status(404).json({ error: "Local no encontrado" });
const ahora = new Date().toISOString();
db.prepare("UPDATE locales SET fechaConfirmacion = ? WHERE id = ?").run(ahora, id);
res.json({ ok: true, fechaConfirmacion: ahora });
});
// Reportar un local (cerrado / datos incorrectos / inapropiado / otro)
app.post("/api/locales/:id/reportar", (req, res) => {
const { id } = req.params;
const { motivo, comentario } = req.body;
const local = db.prepare("SELECT id FROM locales WHERE id = ?").get(id);
if (!local) return res.status(404).json({ error: "Local no encontrado" });
const reporteId = randomUUID();
db.prepare(`
INSERT INTO reportes (id, localId, motivo, comentario, fecha, estado)
VALUES (?, ?, ?, ?, ?, 'pendiente')
`).run(reporteId, id, motivo || "otro", comentario || "", new Date().toISOString());
res.status(201).json({ ok: true, id: reporteId });
});
// Reportes (admin)
app.get("/api/reportes", (_req, res) => {
const reportes = db.prepare(`
SELECT r.*, l.nombre AS localNombre, l.provincia AS localProvincia, l.direccion AS localDireccion
FROM reportes r LEFT JOIN locales l ON l.id = r.localId
WHERE r.estado = 'pendiente'
ORDER BY r.fecha DESC
`).all();
res.json(reportes);
});
// Descartar un reporte (el admin revisó y no hace falta actuar)
app.post("/api/reportes/:id/descartar", (req, res) => {
const { id } = req.params;
db.prepare("UPDATE reportes SET estado = 'descartado' WHERE id = ?").run(id);
res.json({ ok: true });
});