import express from "express"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import db from "./db.js"; import { randomUUID } from "crypto"; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); app.use(express.json()); app.use(express.static(join(__dirname, "../dist"))); // Locales (público, solo lectura) app.get("/api/locales", (_req, res) => { const locales = db.prepare("SELECT * FROM locales ORDER BY fecha DESC").all(); res.json(locales); }); // Pendientes (admin) app.get("/api/pendientes", (_req, res) => { const pendientes = db.prepare("SELECT * FROM pendientes ORDER BY fechaPropuesta DESC").all(); 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; const id = randomUUID(); const fechaPropuesta = new Date().toISOString(); db.prepare(` INSERT INTO pendientes (id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion, categoria, estado, fechaPropuesta, lat, lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pendiente', ?, ?, ?) `).run(id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion ?? 0, categoria, fechaPropuesta, lat, lng); res.status(201).json({ id, nombre, descripcion, provincia, subcategoria, direccion, enlaceGoogleMaps, puntuacion: puntuacion ?? 0, categoria, estado: "pendiente", fechaPropuesta, lat, lng }); }); // Aprobar propuesta app.post("/api/aprobar/:id", (req, res) => { const { id } = req.params; const propuesta = db.prepare("SELECT * FROM pendientes WHERE id = ?").get(id); if (!propuesta) return res.status(404).json({ error: "Propuesta no encontrada" }); const localeId = randomUUID(); const fecha = new Date().toISOString(); db.prepare(` 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); res.json({ ok: true }); }); // Rechazar propuesta app.post("/api/rechazar/:id", (req, res) => { const { id } = req.params; db.prepare("DELETE FROM pendientes WHERE id = ?").run(id); res.json({ ok: true }); }); // Eliminar local 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 }); }); // Config (palabras filtradas) app.get("/api/config/filtro_palabras", (_req, res) => { const config = db.prepare("SELECT datos FROM config WHERE id = ?").get("filtro_palabras"); if (config) { res.json({ palabras: JSON.parse(config.datos) }); } else { res.json({ palabras: [] }); } }); app.post("/api/config/filtro_palabras", (req, res) => { const { palabras } = req.body; db.prepare("INSERT OR REPLACE INTO config (id, datos) VALUES (?, ?)").run("filtro_palabras", JSON.stringify(palabras)); res.json({ ok: true }); }); // SPA fallback app.get(/^\/(?!api\/).*/, (_req, res) => { res.sendFile(join(__dirname, "../dist/index.html")); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`Server listening on http://localhost:${PORT}`));