diff --git a/server.js b/server.js index 04cfa9b..a36964a 100644 --- a/server.js +++ b/server.js @@ -131,6 +131,7 @@ async function autoUpdateDB() { color TEXT DEFAULT 'gray', is_default BOOLEAN DEFAULT FALSE, is_final BOOLEAN DEFAULT FALSE, + is_system BOOLEAN DEFAULT FALSE, -- AÑADIDO: Identificador de estados imborrables created_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS message_templates ( @@ -258,6 +259,11 @@ async function autoUpdateDB() { ALTER TABLE guilds ADD COLUMN ia_keywords JSONB DEFAULT '[]'; END IF; + -- AÑADIDO: Columna para marcar estados imborrables del sistema + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='service_statuses' AND column_name='is_system') THEN + ALTER TABLE service_statuses ADD COLUMN is_system BOOLEAN DEFAULT FALSE; + END IF; + 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; 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; 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; @@ -831,18 +837,13 @@ app.get("/discovery/keys/:provider", authMiddleware, async (req, res) => { } catch (e) { res.status(500).json({ ok: false }); } }); -// AÑADIDO: Ruta para el Panel Operativo (Muestra TODOS los activos, incluidos los de Proveedores) +// AÑADIDO Y MEJORADO: Ruta para el Panel Operativo (Muestra TODOS los activos) app.get("/services/active", authMiddleware, async (req, res) => { try { const q = await pool.query(` SELECT s.*, - u.full_name as assigned_name, - CASE - WHEN s.assigned_to IS NULL THEN 'sin_asignar' - WHEN (s.raw_data->>'scheduled_date') IS NULL OR (s.raw_data->>'scheduled_date') = '' THEN 'asignado_operario' - ELSE 'citado' - END as estado_operativo + u.full_name as assigned_name FROM scraped_services s LEFT JOIN users u ON s.assigned_to = u.id WHERE s.owner_id = $1 @@ -869,7 +870,7 @@ app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => { ...extra, "scheduled_date": date || "", "scheduled_time": time || "", - "status_operativo": status_operativo || "citado" + "status_operativo": status_operativo }; // 3. Guardamos el JSON completo de vuelta @@ -976,9 +977,70 @@ app.put("/clients/:id", authMiddleware, async (req, res) => { } catch (e) { res.status(500).json({ ok: false }); } }); -app.get("/statuses", authMiddleware, async (req, res) => { try { let q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); if (q.rowCount === 0) { const defaults = [{name:'Pendiente',c:'gray',d:true,f:false},{name:'En Proceso',c:'blue',d:false,f:false},{name:'Terminado',c:'green',d:false,f:true},{name:'Cancelado',c:'red',d:false,f:true}]; for (const s of defaults) await pool.query("INSERT INTO service_statuses (owner_id,name,color,is_default,is_final) VALUES ($1,$2,$3,$4,$5)", [req.user.accountId,s.name,s.c,s.d,s.f]); q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); } res.json({ ok: true, statuses: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } }); -app.post("/statuses", authMiddleware, async (req, res) => { try { const { name, color } = req.body; await pool.query("INSERT INTO service_statuses (owner_id, name, color) VALUES ($1, $2, $3)", [req.user.accountId, name, color || 'gray']); res.json({ ok: true }); } catch(e) { res.status(500).json({ ok: false }); } }); -app.delete("/statuses/:id", authMiddleware, async (req, res) => { const client = await pool.connect(); try { const statusId = req.params.id; const check = await client.query("SELECT COUNT(*) FROM services WHERE status_id = $1 AND owner_id = $2", [statusId, req.user.accountId]); if (parseInt(check.rows[0].count) > 0) return res.status(400).json({ ok: false, error: "En uso" }); await client.query("DELETE FROM service_statuses WHERE id=$1 AND owner_id=$2", [statusId, req.user.accountId]); res.json({ ok: true }); } catch(e) { res.status(500).json({ ok: false }); } finally { client.release(); } }); +// ========================================== +// 🎨 RUTAS DE ESTADOS DEL SISTEMA (SAAS COMPLETO) +// ========================================== +app.get("/statuses", authMiddleware, async (req, res) => { + try { + let q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); + + // 🚀 MAGIA SAAS: Si el usuario es nuevo, inyectamos los 9 estados base obligatorios. + if (q.rowCount === 0) { + const defaults = [ + {name:'Pendiente de Asignar', c:'gray', d:true, f:false, sys:true}, + {name:'Asignado', c:'blue', d:false, f:false, sys:true}, + {name:'Pendiente de Cita', c:'amber', d:false, f:false, sys:true}, + {name:'Citado', c:'emerald', d:false, f:false, sys:true}, + {name:'De Camino', c:'indigo', d:false, f:false, sys:true}, + {name:'Trabajando', c:'amber', d:false, f:false, sys:true}, + {name:'Incidencia', c:'red', d:false, f:false, sys:true}, + {name:'Terminado', c:'purple', d:false, f:true, sys:true}, + {name:'Anulado', c:'gray', d:false, f:true, sys:true} + ]; + for (const s of defaults) { + await pool.query("INSERT INTO service_statuses (owner_id,name,color,is_default,is_final,is_system) VALUES ($1,$2,$3,$4,$5,$6)", [req.user.accountId,s.name,s.c,s.d,s.f,s.sys]); + } + q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); + } + res.json({ ok: true, statuses: q.rows }); + } catch (e) { res.status(500).json({ ok: false }); } +}); + +app.post("/statuses", authMiddleware, async (req, res) => { + try { + const { name, color } = req.body; + await pool.query("INSERT INTO service_statuses (owner_id, name, color, is_system) VALUES ($1, $2, $3, false)", [req.user.accountId, name, color || 'gray']); + res.json({ ok: true }); + } catch(e) { res.status(500).json({ ok: false }); } +}); + +app.delete("/statuses/:id", authMiddleware, async (req, res) => { + const client = await pool.connect(); + try { + const statusId = req.params.id; + + // 1. Comprobamos si es del sistema + const sysCheck = await client.query("SELECT is_system FROM service_statuses WHERE id = $1 AND owner_id = $2", [statusId, req.user.accountId]); + if (sysCheck.rowCount > 0 && sysCheck.rows[0].is_system) { + return res.status(400).json({ ok: false, error: "Este es un estado esencial del sistema y no se puede borrar." }); + } + + // 2. Comprobamos si está en uso (esto hay que cruzarlo con scraped_services ahora que guardamos el ID ahí) + const checkSvc = await client.query("SELECT COUNT(*) FROM services WHERE status_id = $1 AND owner_id = $2", [statusId, req.user.accountId]); + const checkScrap = await client.query("SELECT COUNT(*) FROM scraped_services WHERE raw_data->>'status_operativo' = $1 AND owner_id = $2", [statusId, req.user.accountId]); + + if (parseInt(checkSvc.rows[0].count) > 0 || parseInt(checkScrap.rows[0].count) > 0) { + return res.status(400).json({ ok: false, error: "No puedes borrar un estado que tiene servicios asignados." }); + } + + await client.query("DELETE FROM service_statuses WHERE id=$1 AND owner_id=$2", [statusId, req.user.accountId]); + res.json({ ok: true }); + } catch(e) { + res.status(500).json({ ok: false, error: e.message }); + } finally { + client.release(); + } +}); app.get("/clients/search", authMiddleware, async (req, res) => { try { const { phone } = req.query; const p = normalizePhone(phone); if(!p) return res.json({ok:true,client:null}); const q = await pool.query("SELECT * FROM clients WHERE phone=$1 AND owner_id=$2 LIMIT 1", [p, req.user.accountId]); res.json({ ok: true, client: q.rows[0] || null }); } catch (e) { res.status(500).json({ ok: false }); } }); 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 }); } }); @@ -988,33 +1050,14 @@ app.delete("/companies/:id", authMiddleware, async (req, res) => { try { await p // AÑADIDO: Filtro estricto para que solo devuelva operarios que estén en estado 'active' app.get("/operators", authMiddleware, async (req, res) => { try { - // Si nos pasan un guild_id, filtramos también por gremio const guildId = req.query.guild_id; - let query = ` - SELECT u.id, u.full_name, u.zones - FROM users u - WHERE u.owner_id=$1 AND u.role='operario' AND u.status='active' - `; + let query = `SELECT u.id, u.full_name, u.zones FROM users u WHERE u.owner_id=$1 AND u.role='operario' AND u.status='active'`; const params = [req.user.accountId]; - - if (guildId) { - query = ` - SELECT u.id, u.full_name, u.zones - FROM users u - JOIN user_guilds ug ON u.id = ug.user_id - WHERE u.owner_id=$1 AND u.role='operario' AND u.status='active' AND ug.guild_id=$2 - `; - params.push(guildId); - } - + if (guildId) { query = `SELECT u.id, u.full_name, u.zones FROM users u JOIN user_guilds ug ON u.id = ug.user_id WHERE u.owner_id=$1 AND u.role='operario' AND u.status='active' AND ug.guild_id=$2`; params.push(guildId); } query += ` ORDER BY u.full_name ASC`; - const q = await pool.query(query, params); res.json({ ok: true, operators: q.rows }); - } catch (e) { - console.error("Error al cargar operarios:", e); - res.status(500).json({ ok: false }); - } + } catch (e) { res.status(500).json({ ok: false }); } }); app.get("/zones", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT * FROM zones WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]); res.json({ ok: true, zones: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } });