diff --git a/server.js b/server.js index 98f0aec..ff1b1d9 100644 --- a/server.js +++ b/server.js @@ -29,14 +29,14 @@ const pool = new Pool({ }); // ========================================== -// 🧠 AUTO-ACTUALIZACIÓN DB +// 🧠 AUTO-ACTUALIZACIÓN DB Y CARGA DE DATOS GEOGRÁFICOS // ========================================== async function autoUpdateDB() { const client = await pool.connect(); try { console.log("🔄 Verificando salud de la base de datos..."); - // 1. CREAR TABLAS (Si no existen) + // 1. ESTRUCTURA BÁSICA await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, @@ -76,10 +76,6 @@ async function autoUpdateDB() { id SERIAL PRIMARY KEY, owner_id INT REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, - cif TEXT, - email TEXT, - phone TEXT, - address TEXT, created_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS clients ( @@ -137,31 +133,80 @@ async function autoUpdateDB() { ); `); - // 2. PARCHE DE REPARACIÓN + // 2. NUEVAS TABLAS GEOGRÁFICAS (PROVINCIAS Y ZONAS) + await client.query(` + CREATE TABLE IF NOT EXISTS provinces ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL + ); + + CREATE TABLE IF NOT EXISTS zones ( + id SERIAL PRIMARY KEY, + province_id INT REFERENCES provinces(id) ON DELETE CASCADE, + name TEXT NOT NULL, -- Ej: Madrid Capital, Zona Sur, etc. + owner_id INT, -- NULL = Zona del sistema, ID = Zona personalizada + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS user_zones ( + user_id INT REFERENCES users(id) ON DELETE CASCADE, + zone_id INT REFERENCES zones(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, zone_id) + ); + `); + + // 3. SEEDING (POBLAR DATOS DE ESPAÑA SI ESTÁ VACÍO) + const provCheck = await client.query("SELECT COUNT(*) FROM provinces"); + if (parseInt(provCheck.rows[0].count) === 0) { + console.log("🇪🇸 Poblando base de datos con Provincias y Zonas de España..."); + + const spainProvinces = [ + "Álava", "Albacete", "Alicante", "Almería", "Asturias", "Ávila", "Badajoz", "Barcelona", "Burgos", "Cáceres", + "Cádiz", "Cantabria", "Castellón", "Ciudad Real", "Córdoba", "Cuenca", "Girona", "Granada", "Guadalajara", + "Guipúzcoa", "Huelva", "Huesca", "Illes Balears", "Jaén", "La Coruña", "La Rioja", "Las Palmas", "León", + "Lleida", "Lugo", "Madrid", "Málaga", "Murcia", "Navarra", "Ourense", "Palencia", "Pontevedra", "Salamanca", + "Santa Cruz de Tenerife", "Segovia", "Sevilla", "Soria", "Tarragona", "Teruel", "Toledo", "Valencia", + "Valladolid", "Vizcaya", "Zamora", "Zaragoza", "Ceuta", "Melilla" + ]; + + for (const provName of spainProvinces) { + const resProv = await client.query("INSERT INTO provinces (name) VALUES ($1) RETURNING id", [provName]); + const provId = resProv.rows[0].id; + + // Zonas automáticas básicas para seguros + const zones = ["Capital", "Provincia (Resto)"]; + + // Zonas detalladas para grandes capitales + if (["Madrid", "Barcelona", "Valencia", "Sevilla", "Málaga", "Alicante", "Vizcaya"].includes(provName)) { + zones.push("Área Metropolitana", "Zona Norte", "Zona Sur", "Zona Este", "Zona Oeste"); + } + + for (const z of zones) { + await client.query("INSERT INTO zones (province_id, name, owner_id) VALUES ($1, $2, NULL)", [provId, z]); + } + } + console.log("✅ Datos geográficos cargados."); + } + + // 4. PARCHE DE REPARACIÓN await client.query(` DO $$ BEGIN - -- Client ID + -- Columnas faltantes previas IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='client_id') THEN ALTER TABLE services ADD COLUMN client_id INT REFERENCES clients(id) ON DELETE SET NULL; END IF; - -- Status ID IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='status_id') THEN ALTER TABLE services ADD COLUMN status_id INT REFERENCES service_statuses(id) ON DELETE SET NULL; END IF; - -- Guild & Assigned IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='guild_id') THEN ALTER TABLE services ADD COLUMN guild_id INT REFERENCES guilds(id) ON DELETE SET NULL; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='assigned_to') THEN ALTER TABLE services ADD COLUMN assigned_to INT REFERENCES users(id) ON DELETE SET NULL; END IF; - -- Contact IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='contact_phone') THEN ALTER TABLE services ADD COLUMN contact_phone TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='contact_name') THEN ALTER TABLE services ADD COLUMN contact_name TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='address') THEN ALTER TABLE services ADD COLUMN address TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='email') THEN ALTER TABLE services ADD COLUMN email TEXT; END IF; - -- Details IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='title') THEN ALTER TABLE services ADD COLUMN title TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='description') THEN ALTER TABLE services ADD COLUMN description TEXT; END IF; - -- Schedule IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='scheduled_date') THEN ALTER TABLE services ADD COLUMN scheduled_date DATE DEFAULT CURRENT_DATE; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='scheduled_time') THEN ALTER TABLE services ADD COLUMN scheduled_time TIME DEFAULT CURRENT_TIME; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='duration_minutes') THEN ALTER TABLE services ADD COLUMN duration_minutes INT DEFAULT 30; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='is_urgent') THEN ALTER TABLE services ADD COLUMN is_urgent BOOLEAN DEFAULT FALSE; END IF; - -- Company & Notes IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='company_id') THEN ALTER TABLE services ADD COLUMN company_id INT REFERENCES companies(id) ON DELETE SET NULL; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='is_company') THEN ALTER TABLE services ADD COLUMN is_company BOOLEAN DEFAULT FALSE; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='company_ref') THEN ALTER TABLE services ADD COLUMN company_ref TEXT; END IF; @@ -173,7 +218,7 @@ async function autoUpdateDB() { END $$; `); - console.log("✅ DB Sincronizada y Reparada."); + console.log("✅ DB Sincronizada."); } catch (e) { console.error("❌ Error DB:", e); } finally { @@ -237,6 +282,25 @@ app.post("/auth/reset-password", async (req, res) => { try { const { phone, code, newPassword } = req.body; const p = normalizePhone(phone); const q = await client.query(`SELECT lc.*, u.id as uid FROM login_codes lc JOIN users u ON lc.user_id=u.id WHERE lc.phone=$1 AND lc.purpose='password_reset' AND lc.consumed_at IS NULL AND lc.expires_at>NOW() ORDER BY lc.created_at DESC LIMIT 1`, [p]); if(q.rowCount===0) return res.status(400).json({ok:false}); const row=q.rows[0]; if(!(await bcrypt.compare(String(code), row.code_hash))) return res.status(400).json({ok:false}); const hash=await bcrypt.hash(newPassword, 10); await client.query('BEGIN'); await client.query("UPDATE users SET password_hash=$1 WHERE id=$2",[hash, row.uid]); await client.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1",[row.id]); await client.query('COMMIT'); res.json({ok:true}); } catch(e){await client.query('ROLLBACK'); res.status(500).json({ok:false});} finally{client.release();} }); +// ========================= +// NUEVAS RUTAS GEOGRÁFICAS +// ========================= +app.get("/provinces", authMiddleware, async (req, res) => { + try { const q = await pool.query("SELECT * FROM provinces ORDER BY name ASC"); res.json({ ok: true, provinces: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } +}); + +app.get("/zones", authMiddleware, async (req, res) => { + try { + const { province_id } = req.query; + let query = "SELECT * FROM zones WHERE (owner_id IS NULL OR owner_id=$1)"; + const params = [req.user.accountId]; + if(province_id) { query += " AND province_id=$2"; params.push(province_id); } + query += " ORDER BY name ASC"; + const q = await pool.query(query, params); + res.json({ ok: true, zones: q.rows }); + } catch (e) { res.status(500).json({ ok: false }); } +}); + // DATOS MAESTROS app.get("/statuses", authMiddleware, async (req, res) => { try { @@ -255,6 +319,7 @@ app.get("/clients/search", authMiddleware, async (req, res) => { app.get("/companies", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT * FROM companies WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]); res.json({ ok: true, companies: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/companies", authMiddleware, async (req, res) => { try { const { name } = req.body; await pool.query("INSERT INTO companies (name, owner_id) VALUES ($1, $2)", [name, req.user.accountId]); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } }); +// OBTENER OPERARIOS POR GREMIO app.get("/operators", authMiddleware, async (req, res) => { try { const { guild_id } = req.query; @@ -313,7 +378,7 @@ app.post("/services", authMiddleware, async (req, res) => { const { phone, name, address, email, description, scheduled_date, scheduled_time, duration, is_urgent, is_company, company_id, company_ref, internal_notes, client_notes, status_id, guild_id, assigned_to } = req.body; const p = normalizePhone(phone); - // SANITIZAR ENTEROS (Evitar error invalid input syntax for type integer: "") + // SANITIZAR const safeDuration = (duration === "" || duration === null) ? 30 : duration; const safeCompanyId = (company_id === "" || company_id === null) ? null : company_id; const safeGuildId = (guild_id === "" || guild_id === null) ? null : guild_id; @@ -372,7 +437,6 @@ app.put("/services/:id", authMiddleware, async (req, res) => { guild_id, assigned_to } = req.body; - // SANITIZAR ENTEROS const safeDuration = (duration === "" || duration === null) ? 30 : duration; const safeCompanyId = (company_id === "" || company_id === null) ? null : company_id; const safeGuildId = (guild_id === "" || guild_id === null) ? null : guild_id;