Actualizar server.js

This commit is contained in:
2026-02-08 14:54:18 +00:00
parent 8e013f7a7c
commit 9e2ac9e072

144
server.js
View File

@@ -106,6 +106,8 @@ async function autoUpdateDB() {
owner_id INT REFERENCES users(id) ON DELETE CASCADE, owner_id INT REFERENCES users(id) ON DELETE CASCADE,
client_id INT REFERENCES clients(id) ON DELETE SET NULL, client_id INT REFERENCES clients(id) ON DELETE SET NULL,
status_id INT REFERENCES service_statuses(id) ON DELETE SET NULL, status_id INT REFERENCES service_statuses(id) ON DELETE SET NULL,
guild_id INT REFERENCES guilds(id) ON DELETE SET NULL, -- NUEVO
assigned_to INT REFERENCES users(id) ON DELETE SET NULL, -- NUEVO
title TEXT, title TEXT,
description TEXT, description TEXT,
contact_phone TEXT, contact_phone TEXT,
@@ -135,11 +137,15 @@ async function autoUpdateDB() {
); );
`); `);
// 2. PARCHE DE REPARACIÓN (Columnas faltantes) // 2. PARCHE DE REPARACIÓN
await client.query(` await client.query(`
DO $$ DO $$
BEGIN BEGIN
-- Services -- Nuevas columnas Asignación
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;
-- Reparaciones anteriores
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; 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;
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='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='description') THEN ALTER TABLE services ADD COLUMN description 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;
@@ -158,7 +164,6 @@ async function autoUpdateDB() {
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='client_notes') THEN ALTER TABLE services ADD COLUMN client_notes TEXT; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='client_notes') THEN ALTER TABLE services ADD COLUMN client_notes TEXT; 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; 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;
-- Limpieza Constraints
BEGIN ALTER TABLE users DROP CONSTRAINT IF EXISTS users_phone_key; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER TABLE users DROP CONSTRAINT IF EXISTS users_phone_key; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key; EXCEPTION WHEN OTHERS THEN NULL; END; BEGIN ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key; EXCEPTION WHEN OTHERS THEN NULL; END;
END $$; END $$;
@@ -193,11 +198,7 @@ async function sendWhatsAppCode(phone, code) {
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, body: JSON.stringify({ number: phone.replace("+", ""), text: `🔐 Código: *${code}*` }) }).catch(console.error); await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, body: JSON.stringify({ number: phone.replace("+", ""), text: `🔐 Código: *${code}*` }) }).catch(console.error);
} }
// ========================= // RUTAS AUTH
// RUTAS
// =========================
// AUTH
app.post("/auth/register", async (req, res) => { app.post("/auth/register", async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
@@ -228,7 +229,7 @@ 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();} 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();}
}); });
// ESTADOS, CLIENTES, COMPAÑIAS // DATOS MAESTROS
app.get("/statuses", authMiddleware, async (req, res) => { app.get("/statuses", authMiddleware, async (req, res) => {
try { try {
let q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); let q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]);
@@ -246,10 +247,40 @@ 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.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 }); } }); 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 }); } });
// --- SERVICIOS CRUD COMPLETO --- // NUEVO: OBTENER OPERARIOS FILTRADOS POR GREMIO
app.get("/operators", authMiddleware, async (req, res) => {
try {
const { guild_id } = req.query;
let query = `
SELECT u.id, u.full_name
FROM users u
JOIN user_guilds ug ON u.id = ug.user_id
WHERE u.owner_id = $1 AND u.role = 'operario'
`;
const params = [req.user.accountId];
if (guild_id) { query += ` AND ug.guild_id = $2`; params.push(guild_id); }
query += ` GROUP BY u.id ORDER BY u.full_name ASC`;
const q = await pool.query(query, params);
res.json({ ok: true, operators: q.rows });
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
});
// SERVICIOS CRUD
app.get("/services", authMiddleware, async (req, res) => { app.get("/services", authMiddleware, async (req, res) => {
try { const q = await pool.query(`SELECT s.*, st.name as status_name, st.color as status_color, c.name as company_name FROM services s LEFT JOIN service_statuses st ON s.status_id=st.id LEFT JOIN companies c ON s.company_id=c.id WHERE s.owner_id=$1 ORDER BY s.created_at DESC`, [req.user.accountId]); res.json({ ok: true, services: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } try {
const q = await pool.query(`
SELECT s.*, st.name as status_name, st.color as status_color, c.name as company_name,
g.name as guild_name, u.full_name as assigned_name
FROM services s
LEFT JOIN service_statuses st ON s.status_id = st.id
LEFT JOIN companies c ON s.company_id = c.id
LEFT JOIN guilds g ON s.guild_id = g.id
LEFT JOIN users u ON s.assigned_to = u.id
WHERE s.owner_id=$1 ORDER BY s.created_at DESC`,
[req.user.accountId]
);
res.json({ ok: true, services: q.rows });
} catch (e) { res.status(500).json({ ok: false }); }
}); });
app.get("/services/:id", authMiddleware, async (req, res) => { app.get("/services/:id", authMiddleware, async (req, res) => {
@@ -273,39 +304,17 @@ app.put("/services/:id/status", authMiddleware, async (req, res) => {
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
}); });
// NUEVO: EDITAR SERVICIO COMPLETO
app.put("/services/:id", authMiddleware, async (req, res) => {
const client = await pool.connect();
try {
const { name, address, email, description, scheduled_date, scheduled_time, duration, is_urgent, is_company, company_id, company_ref, internal_notes, client_notes } = req.body;
await client.query('BEGIN');
await client.query(`
UPDATE services SET
contact_name=$1, address=$2, email=$3, description=$4,
scheduled_date=$5, scheduled_time=$6, duration_minutes=$7, is_urgent=$8,
is_company=$9, company_id=$10, company_ref=$11,
internal_notes=$12, client_notes=$13
WHERE id=$14 AND owner_id=$15
`, [name, address, email, description, scheduled_date, scheduled_time, duration, is_urgent, is_company, company_id, company_ref, internal_notes, client_notes, req.params.id, req.user.accountId]);
await client.query("INSERT INTO service_logs (service_id, user_id, new_status_id, comment) VALUES ($1, $2, (SELECT status_id FROM services WHERE id=$1), 'Datos del servicio editados')", [req.params.id, req.user.sub]);
await client.query('COMMIT'); res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); console.error(e); res.status(500).json({ ok: false }); } finally { client.release(); }
});
// NUEVO: BORRAR SERVICIO
app.delete("/services/:id", authMiddleware, async (req, res) => {
try {
// Al borrar el servicio, la FK con CASCADE borrará logs automáticamente
await pool.query("DELETE FROM services WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]);
res.json({ ok: true });
} catch (e) { res.status(500).json({ ok: false }); }
});
app.post("/services", authMiddleware, async (req, res) => { app.post("/services", authMiddleware, async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
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 } = req.body; 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 // NUEVOS CAMPOS
} = req.body;
const p = normalizePhone(phone); const p = normalizePhone(phone);
await client.query('BEGIN'); await client.query('BEGIN');
@@ -327,9 +336,19 @@ app.post("/services", authMiddleware, async (req, res) => {
} }
const insert = await client.query(` const insert = await client.query(`
INSERT INTO services (owner_id, client_id, status_id, contact_phone, contact_name, address, email, description, scheduled_date, scheduled_time, duration_minutes, is_urgent, is_company, company_id, company_ref, internal_notes, client_notes, title) INSERT INTO services (
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id owner_id, client_id, status_id, contact_phone, contact_name, address, email,
`, [req.user.accountId, clientId, finalStatus, p, name, address, email, description, scheduled_date || 'NOW()', scheduled_time || 'NOW()', duration || 30, is_urgent || false, is_company || false, company_id || null, company_ref, internal_notes, client_notes, name + " - Svc"]); description, scheduled_date, scheduled_time, duration_minutes, is_urgent,
is_company, company_id, company_ref, internal_notes, client_notes, title,
guild_id, assigned_to
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
RETURNING id
`, [
req.user.accountId, clientId, finalStatus, p, name, address, email,
description, scheduled_date || 'NOW()', scheduled_time || 'NOW()', duration || 30, is_urgent || false,
is_company || false, company_id || null, company_ref, internal_notes, client_notes, name + " - Svc",
guild_id || null, assigned_to || null
]);
await client.query("INSERT INTO service_logs (service_id, user_id, new_status_id, comment) VALUES ($1, $2, $3, 'Servicio Creado')", [insert.rows[0].id, req.user.sub, finalStatus]); await client.query("INSERT INTO service_logs (service_id, user_id, new_status_id, comment) VALUES ($1, $2, $3, 'Servicio Creado')", [insert.rows[0].id, req.user.sub, finalStatus]);
@@ -338,6 +357,43 @@ app.post("/services", authMiddleware, async (req, res) => {
} catch (e) { await client.query('ROLLBACK'); console.error(e); res.status(500).json({ ok: false, error: e.message }); } finally { client.release(); } } catch (e) { await client.query('ROLLBACK'); console.error(e); res.status(500).json({ ok: false, error: e.message }); } finally { client.release(); }
}); });
// EDITAR (Con asignación)
app.put("/services/:id", authMiddleware, async (req, res) => {
const client = await pool.connect();
try {
const {
name, address, email, description,
scheduled_date, scheduled_time, duration, is_urgent,
is_company, company_id, company_ref,
internal_notes, client_notes,
guild_id, assigned_to // NUEVOS CAMPOS
} = req.body;
await client.query('BEGIN');
await client.query(`
UPDATE services SET
contact_name=$1, address=$2, email=$3, description=$4,
scheduled_date=$5, scheduled_time=$6, duration_minutes=$7, is_urgent=$8,
is_company=$9, company_id=$10, company_ref=$11,
internal_notes=$12, client_notes=$13,
guild_id=$14, assigned_to=$15
WHERE id=$16 AND owner_id=$17
`, [
name, address, email, description,
scheduled_date, scheduled_time, duration, is_urgent,
is_company, company_id, company_ref,
internal_notes, client_notes,
guild_id, assigned_to,
req.params.id, req.user.accountId
]);
await client.query("INSERT INTO service_logs (service_id, user_id, new_status_id, comment) VALUES ($1, $2, (SELECT status_id FROM services WHERE id=$1), 'Datos editados')", [req.params.id, req.user.sub]);
await client.query('COMMIT'); res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); console.error(e); res.status(500).json({ ok: false }); } finally { client.release(); }
});
app.delete("/services/:id", authMiddleware, async (req, res) => { try { await pool.query("DELETE FROM services WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } });
// RUTAS USERS/GREMIOS // RUTAS USERS/GREMIOS
app.get("/guilds", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT * FROM guilds WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]); res.json({ ok: true, guilds: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } }); app.get("/guilds", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT * FROM guilds WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]); res.json({ ok: true, guilds: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } });
app.post("/guilds", authMiddleware, async (req, res) => { try { const { name } = req.body; await pool.query("INSERT INTO guilds (name, owner_id) VALUES ($1, $2)", [name, req.user.accountId]); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/guilds", authMiddleware, async (req, res) => { try { const { name } = req.body; await pool.query("INSERT INTO guilds (name, owner_id) VALUES ($1, $2)", [name, req.user.accountId]); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } });