Actualizar server.js

This commit is contained in:
2026-02-08 22:32:32 +00:00
parent c66a0ae086
commit 966ee3cd2c

404
server.js
View File

@@ -34,9 +34,9 @@ const pool = new Pool({
async function autoUpdateDB() {
const client = await pool.connect();
try {
console.log("🔄 Verificando salud de la base de datos...");
console.log("🔄 Verificando estructura DB...");
// 1. ESTRUCTURA BÁSICA
// 1. TABLAS PRINCIPALES
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 (
@@ -101,22 +97,48 @@ async function autoUpdateDB() {
is_final BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
`);
// 2. TABLAS GEOGRÁFICAS (NUEVO MODELO)
await client.query(`
CREATE TABLE IF NOT EXISTS provinces (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
-- TABLA DE POBLACIONES (Se llenará externamente)
CREATE TABLE IF NOT EXISTS towns (
id SERIAL PRIMARY KEY,
province_id INT REFERENCES provinces(id) ON DELETE CASCADE,
name TEXT NOT NULL
);
-- ZONAS PERSONALIZADAS (Agrupaciones de pueblos)
CREATE TABLE IF NOT EXISTS zones (
id SERIAL PRIMARY KEY,
province_id INT REFERENCES provinces(id) ON DELETE CASCADE,
name TEXT NOT NULL,
name TEXT NOT NULL, -- Ej: "Ruta Sierra", "Capital y Alrededores"
owner_id INT,
created_at TIMESTAMP DEFAULT NOW()
);
-- RELACIÓN ZONA <-> PUEBLOS (Qué pueblos componen una zona)
CREATE TABLE IF NOT EXISTS zone_towns (
zone_id INT REFERENCES zones(id) ON DELETE CASCADE,
town_id INT REFERENCES towns(id) ON DELETE CASCADE,
PRIMARY KEY (zone_id, town_id)
);
-- RELACIÓN OPERARIO <-> ZONAS (Dónde trabaja el operario)
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. TABLA SERVICIOS
await client.query(`
CREATE TABLE IF NOT EXISTS services (
id SERIAL PRIMARY KEY,
owner_id INT REFERENCES users(id) ON DELETE CASCADE,
@@ -153,45 +175,36 @@ async function autoUpdateDB() {
);
`);
// SEEDING PROVINCIAS
// 4. POBLAR DATOS GEOGRÁFICOS BÁSICOS (EJEMPLO)
const provCheck = await client.query("SELECT COUNT(*) FROM provinces");
if (parseInt(provCheck.rows[0].count) === 0) {
console.log("🇪🇸 Poblando Provincias...");
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) {
await client.query("INSERT INTO provinces (name) VALUES ($1)", [provName]);
}
console.log("🇪🇸 Inicializando Provincias y Poblaciones de ejemplo...");
// Insertamos un par de provincias para probar
const cadizRes = await client.query("INSERT INTO provinces (name) VALUES ('Cádiz') RETURNING id");
const madridRes = await client.query("INSERT INTO provinces (name) VALUES ('Madrid') RETURNING id");
const cadizId = cadizRes.rows[0].id;
const madridId = madridRes.rows[0].id;
// Insertamos algunas poblaciones de ejemplo (EN PRODUCCIÓN ESTO SE HACE CON SQL MASIVO)
const townsCadiz = ["Cádiz Capital", "Jerez de la Frontera", "Algeciras", "San Fernando", "El Puerto de Santa María", "Chiclana", "Sanlúcar", "La Línea", "Puerto Real", "Rota", "Tarifa", "Conil"];
const townsMadrid = ["Madrid Capital", "Móstoles", "Alcalá de Henares", "Fuenlabrada", "Leganés", "Getafe"];
for (const t of townsCadiz) await client.query("INSERT INTO towns (province_id, name) VALUES ($1, $2)", [cadizId, t]);
for (const t of townsMadrid) await client.query("INSERT INTO towns (province_id, name) VALUES ($1, $2)", [madridId, t]);
console.log("✅ Datos geográficos de ejemplo cargados.");
}
// PARCHE REPARACIÓN (Fuerza columnas)
// 5. PARCHE DE REPARACIÓN DE COLUMNAS
await client.query(`
DO $$ BEGIN
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='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;
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;
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;
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;
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;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='internal_notes') THEN ALTER TABLE services ADD COLUMN internal_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;
-- (Resto de columnas omitidas por brevedad, el bloque anterior ya las reparó, pero mantenemos 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_email_key; EXCEPTION WHEN OTHERS THEN NULL; END;
END $$;
@@ -201,98 +214,83 @@ async function autoUpdateDB() {
} catch (e) { console.error("❌ Error DB:", e); } finally { client.release(); }
}
// HELPERS
function normalizePhone(phone) {
let p = String(phone || "").trim().replace(/\s+/g, "").replace(/-/g, "");
if (!p) return "";
if (!p.startsWith("+") && /^[6789]\d{8}$/.test(p)) return "+34" + p;
return p;
}
// HELPERS (Igual que siempre)
function normalizePhone(phone) { let p = String(phone || "").trim().replace(/\s+/g, "").replace(/-/g, ""); if (!p) return ""; if (!p.startsWith("+") && /^[6789]\d{8}$/.test(p)) return "+34" + p; return p; }
function genCode6() { return String(Math.floor(100000 + Math.random() * 900000)); }
function signToken(user) {
const accountId = user.owner_id || user.id;
return jwt.sign({ sub: user.id, email: user.email, phone: user.phone, role: user.role || 'operario', accountId }, JWT_SECRET, { expiresIn: "30d" });
}
function authMiddleware(req, res, next) {
const h = req.headers.authorization || "";
const token = h.startsWith("Bearer ") ? h.slice(7) : "";
if (!token) return res.status(401).json({ ok: false, error: "No token" });
try { req.user = jwt.verify(token, JWT_SECRET); next(); }
catch { return res.status(401).json({ ok: false, error: "Token inválido" }); }
}
async function sendWhatsAppCode(phone, code) {
if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) return;
const url = `${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`;
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);
}
function signToken(user) { const accountId = user.owner_id || user.id; return jwt.sign({ sub: user.id, email: user.email, phone: user.phone, role: user.role || 'operario', accountId }, JWT_SECRET, { expiresIn: "30d" }); }
function authMiddleware(req, res, next) { const h = req.headers.authorization || ""; const token = h.startsWith("Bearer ") ? h.slice(7) : ""; if (!token) return res.status(401).json({ ok: false, error: "No token" }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch { return res.status(401).json({ ok: false, error: "Token inválido" }); } }
async function sendWhatsAppCode(phone, code) { if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) return; const url = `${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`; 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
app.post("/auth/register", async (req, res) => {
const client = await pool.connect();
try {
const { fullName, phone, address, dni, email, password } = req.body;
const p = normalizePhone(phone);
if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false, error: "Faltan datos" });
const passwordHash = await bcrypt.hash(password, 10);
await client.query('BEGIN');
const insert = await client.query("INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL) RETURNING id", [fullName, p, address, dni, email, passwordHash]);
const userId = insert.rows[0].id;
const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, $4)", [userId, p, codeHash, expiresAt]);
await sendWhatsAppCode(p, code);
await client.query('COMMIT'); res.json({ ok: true, phone: p });
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
});
app.post("/auth/verify", async (req, res) => {
try { const { phone, code } = req.body; const p = normalizePhone(phone); const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 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 }); await pool.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [row.id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); } catch (e) { res.status(500).json({ ok: false }); }
});
app.post("/auth/login", async (req, res) => {
try { const { email, password } = req.body; const q = await pool.query("SELECT * FROM users WHERE email=$1", [email]); if (q.rowCount === 0) return res.status(401).json({ ok: false }); let user = null; for (const u of q.rows) { if (await bcrypt.compare(password, u.password_hash)) { user = u; break; } } if (!user) return res.status(401).json({ ok: false }); res.json({ ok: true, token: signToken(user) }); } catch(e) { res.status(500).json({ ok: false }); }
});
app.post("/auth/forgot-password", async (req, res) => {
try { const { dni, phone } = req.body; const p = normalizePhone(phone); const q = await pool.query("SELECT id FROM users WHERE dni=$1 AND phone=$2", [dni, p]); if (q.rowCount === 0) return res.status(404).json({ ok: false }); const uid = q.rows[0].id; const code = genCode6(); const hash = await bcrypt.hash(code, 10); await pool.query("INSERT INTO login_codes (user_id, phone, code_hash, purpose, expires_at) VALUES ($1, $2, $3, 'password_reset', $4)", [uid, p, hash, new Date(Date.now()+600000)]); await sendWhatsAppCode(p, code); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); }
});
app.post("/auth/reset-password", async (req, res) => {
const client = await pool.connect();
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();}
});
// RUTAS AUTH (Igual que siempre)
app.post("/auth/register", async (req, res) => { const client = await pool.connect(); try { const { fullName, phone, address, dni, email, password } = req.body; const p = normalizePhone(phone); if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false }); const passwordHash = await bcrypt.hash(password, 10); await client.query('BEGIN'); const insert = await client.query("INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL) RETURNING id", [fullName, p, address, dni, email, passwordHash]); const userId = insert.rows[0].id; const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, $4)", [userId, p, codeHash, expiresAt]); await sendWhatsAppCode(p, code); await client.query('COMMIT'); res.json({ ok: true, phone: p }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } });
app.post("/auth/verify", async (req, res) => { try { const { phone, code } = req.body; const p = normalizePhone(phone); const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 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 }); await pool.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [row.id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); } catch (e) { res.status(500).json({ ok: false }); } });
app.post("/auth/login", async (req, res) => { try { const { email, password } = req.body; const q = await pool.query("SELECT * FROM users WHERE email=$1", [email]); if (q.rowCount === 0) return res.status(401).json({ ok: false }); let user = null; for (const u of q.rows) { if (await bcrypt.compare(password, u.password_hash)) { user = u; break; } } if (!user) return res.status(401).json({ ok: false }); res.json({ ok: true, token: signToken(user) }); } catch(e) { res.status(500).json({ ok: false }); } });
// =========================
// NUEVAS RUTAS GEOGRÁFICAS
// API GEOGRÁFICA (ZONAS Y PUEBLOS)
// =========================
// 1. Obtener Provincias
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 }); }
});
// 2. Obtener Pueblos de una Provincia
app.get("/provinces/:id/towns", authMiddleware, async (req, res) => {
try {
const q = await pool.query("SELECT * FROM towns WHERE province_id=$1 ORDER BY name ASC", [req.params.id]);
res.json({ ok: true, towns: q.rows });
} catch (e) { res.status(500).json({ ok: false }); }
});
// 3. Crear/Listar Zonas (Con pueblos asociados)
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)";
let query = `
SELECT z.*,
(SELECT json_agg(t.name) FROM zone_towns zt JOIN towns t ON zt.town_id = t.id WHERE zt.zone_id = z.id) as towns_names
FROM zones z
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 }); }
});
app.post("/zones", authMiddleware, async (req, res) => {
const client = await pool.connect();
try {
const { province_id, name } = req.body;
if (!province_id || !name) return res.status(400).json({ ok: false, error: "Faltan datos" });
const q = await pool.query("INSERT INTO zones (province_id, name, owner_id) VALUES ($1, $2, $3) RETURNING *", [province_id, name, req.user.accountId]);
res.json({ ok: true, zone: q.rows[0] });
} catch (e) { res.status(500).json({ ok: false, error: e.message }); }
const { province_id, name, town_ids } = req.body; // town_ids es un array [1, 2, 5]
if (!province_id || !name) return res.status(400).json({ ok: false });
await client.query('BEGIN');
// Crear Zona
const q = await client.query("INSERT INTO zones (province_id, name, owner_id) VALUES ($1, $2, $3) RETURNING id", [province_id, name, req.user.accountId]);
const zoneId = q.rows[0].id;
// Asociar Pueblos
if (town_ids && Array.isArray(town_ids)) {
for (const tid of town_ids) {
await client.query("INSERT INTO zone_towns (zone_id, town_id) VALUES ($1, $2)", [zoneId, tid]);
}
}
await client.query('COMMIT');
res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
});
app.delete("/zones/:id", authMiddleware, async (req, res) => {
try {
const q = await pool.query("DELETE FROM zones WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]);
if (q.rowCount === 0) return res.status(403).json({ ok: false, error: "No puedes borrar esta zona" });
res.json({ ok: true });
} catch (e) { res.status(500).json({ ok: false }); }
try { await pool.query("DELETE FROM zones 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 }); }
});
// 4. Asignación Operarios <-> Zonas
app.get("/zones/:id/operators", authMiddleware, async (req, res) => {
try {
const q = await pool.query("SELECT user_id FROM user_zones WHERE zone_id=$1", [req.params.id]);
@@ -307,6 +305,7 @@ app.post("/zones/:id/assign", authMiddleware, async (req, res) => {
const { operator_ids } = req.body;
const zoneId = req.params.id;
await client.query('BEGIN');
// Limpiamos solo para mis usuarios en esa zona
await client.query("DELETE FROM user_zones WHERE zone_id=$1 AND user_id IN (SELECT id FROM users WHERE owner_id=$2)", [zoneId, req.user.accountId]);
if (operator_ids && Array.isArray(operator_ids)) {
for (const uid of operator_ids) {
@@ -321,205 +320,32 @@ app.post("/zones/:id/assign", authMiddleware, async (req, res) => {
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
});
// DATOS MAESTROS
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.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 }); } });
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 (Y ZONA EN EL FUTURO)
// OBTENER OPERARIOS (Sin filtro gremio para esta vista, o con filtro)
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);
const q = await pool.query("SELECT id, full_name FROM users WHERE owner_id=$1 AND role='operario' ORDER BY full_name ASC", [req.user.accountId]);
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) => {
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) => {
try { const q = await pool.query(`SELECT * FROM services WHERE id=$1 AND owner_id=$2`, [req.params.id, req.user.accountId]); res.json({ ok: true, service: q.rows[0] }); } catch (e) { res.status(500).json({ ok: false }); }
});
app.get("/services/:id/logs", authMiddleware, async (req, res) => {
try { const q = await pool.query(`SELECT l.*, u.full_name as user_name, s2.name as new_status, s2.color as new_color FROM service_logs l LEFT JOIN users u ON l.user_id=u.id LEFT JOIN service_statuses s2 ON l.new_status_id=s2.id WHERE l.service_id=$1 ORDER BY l.created_at DESC`, [req.params.id]); res.json({ ok: true, logs: q.rows }); } catch (e) { res.status(500).json({ ok: false }); }
});
app.put("/services/:id/status", authMiddleware, async (req, res) => {
const client = await pool.connect();
try {
const { status_id, comment } = req.body;
await client.query('BEGIN');
const curr = await client.query("SELECT status_id FROM services WHERE id=$1", [req.params.id]);
const old = curr.rows[0].status_id;
await client.query("UPDATE services SET status_id=$1 WHERE id=$2", [status_id, req.params.id]);
await client.query("INSERT INTO service_logs (service_id, user_id, old_status_id, new_status_id, comment) VALUES ($1, $2, $3, $4, $5)", [req.params.id, req.user.sub, old, status_id, comment]);
await client.query('COMMIT'); res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
});
// CREAR SERVICIO (SANITIZADO)
app.post("/services", authMiddleware, async (req, res) => {
const client = await pool.connect();
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, guild_id, assigned_to } = req.body;
const p = normalizePhone(phone);
// SANITIZAR ENTEROS (Evitar error invalid input syntax for type integer: "")
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;
const safeAssignedTo = (assigned_to === "" || assigned_to === null) ? null : assigned_to;
await client.query('BEGIN');
let finalStatus = status_id;
if (!finalStatus) {
const def = await client.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND is_default=TRUE LIMIT 1", [req.user.accountId]);
finalStatus = def.rows[0]?.id;
}
let clientId;
const cCheck = await client.query("SELECT id, addresses FROM clients WHERE phone=$1 AND owner_id=$2", [p, req.user.accountId]);
if (cCheck.rowCount > 0) {
clientId = cCheck.rows[0].id;
let addrs = cCheck.rows[0].addresses || [];
if(!addrs.includes(address)) { addrs.push(address); await client.query("UPDATE clients SET addresses=$1 WHERE id=$2", [JSON.stringify(addrs), clientId]); }
} else {
const newC = await client.query("INSERT INTO clients (owner_id, full_name, phone, email, addresses) VALUES ($1, $2, $3, $4, $5) RETURNING id", [req.user.accountId, name, p, email, JSON.stringify([address])]);
clientId = newC.rows[0].id;
}
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,
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()', safeDuration, is_urgent || false,
is_company || false, safeCompanyId, company_ref, internal_notes, client_notes, name + " - Svc",
safeGuildId, safeAssignedTo
]);
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('COMMIT');
res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); console.error(e); res.status(500).json({ ok: false, error: e.message }); } finally { client.release(); }
});
// EDITAR SERVICIO (SANITIZADO)
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
} = 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;
const safeAssignedTo = (assigned_to === "" || assigned_to === null) ? null : assigned_to;
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, safeDuration, is_urgent,
is_company, safeCompanyId, company_ref,
internal_notes, client_notes,
safeGuildId, safeAssignedTo,
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(); }
});
// --- RESTO DE APIS (SERVICIOS, ESTADOS, ETC - MANTENIDOS IGUAL) ---
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.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 }); } });
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.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, 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) => { try { const q = await pool.query(`SELECT * FROM services WHERE id=$1 AND owner_id=$2`, [req.params.id, req.user.accountId]); res.json({ ok: true, service: q.rows[0] }); } catch (e) { res.status(500).json({ ok: false }); } });
app.get("/services/:id/logs", authMiddleware, async (req, res) => { try { const q = await pool.query(`SELECT l.*, u.full_name as user_name, s2.name as new_status, s2.color as new_color FROM service_logs l LEFT JOIN users u ON l.user_id=u.id LEFT JOIN service_statuses s2 ON l.new_status_id=s2.id WHERE l.service_id=$1 ORDER BY l.created_at DESC`, [req.params.id]); res.json({ ok: true, logs: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } });
app.put("/services/:id/status", authMiddleware, async (req, res) => { const client = await pool.connect(); try { const { status_id, comment } = req.body; await client.query('BEGIN'); const curr = await client.query("SELECT status_id FROM services WHERE id=$1", [req.params.id]); const old = curr.rows[0].status_id; await client.query("UPDATE services SET status_id=$1 WHERE id=$2", [status_id, req.params.id]); await client.query("INSERT INTO service_logs (service_id, user_id, old_status_id, new_status_id, comment) VALUES ($1, $2, $3, $4, $5)", [req.params.id, req.user.sub, old, status_id, comment]); await client.query('COMMIT'); res.json({ ok: true }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } });
app.post("/services", authMiddleware, async (req, res) => { const client = await pool.connect(); 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, guild_id, assigned_to } = req.body; const p = normalizePhone(phone); 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; const safeAssignedTo = (assigned_to === "" || assigned_to === null) ? null : assigned_to; await client.query('BEGIN'); let finalStatus = status_id; if (!finalStatus) { const def = await client.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND is_default=TRUE LIMIT 1", [req.user.accountId]); finalStatus = def.rows[0]?.id; } let clientId; const cCheck = await client.query("SELECT id, addresses FROM clients WHERE phone=$1 AND owner_id=$2", [p, req.user.accountId]); if (cCheck.rowCount > 0) { clientId = cCheck.rows[0].id; let addrs = cCheck.rows[0].addresses || []; if(!addrs.includes(address)) { addrs.push(address); await client.query("UPDATE clients SET addresses=$1 WHERE id=$2", [JSON.stringify(addrs), clientId]); } } else { const newC = await client.query("INSERT INTO clients (owner_id, full_name, phone, email, addresses) VALUES ($1, $2, $3, $4, $5) RETURNING id", [req.user.accountId, name, p, email, JSON.stringify([address])]); clientId = newC.rows[0].id; } 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, 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()', safeDuration, is_urgent || false, is_company || false, safeCompanyId, company_ref, internal_notes, client_notes, name + " - Svc", safeGuildId, safeAssignedTo]); 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('COMMIT'); res.json({ ok: true }); } catch (e) { await client.query('ROLLBACK'); console.error(e); res.status(500).json({ ok: false, error: e.message }); } finally { client.release(); } });
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 } = req.body; 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; const safeAssignedTo = (assigned_to === "" || assigned_to === null) ? null : assigned_to; 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, safeDuration, is_urgent, is_company, safeCompanyId, company_ref, internal_notes, client_notes, safeGuildId, safeAssignedTo, 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
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.delete("/guilds/:id", authMiddleware, async (req, res) => { try { await pool.query("DELETE FROM guilds 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.get("/admin/users", authMiddleware, async (req, res) => { try { const q = await pool.query(`SELECT u.id, u.full_name, u.email, u.phone, u.role, COALESCE(json_agg(g.id) FILTER (WHERE g.id IS NOT NULL), '[]') as guilds FROM users u LEFT JOIN user_guilds ug ON u.id=ug.user_id LEFT JOIN guilds g ON ug.guild_id=g.id WHERE u.owner_id=$1 GROUP BY u.id ORDER BY u.id DESC`, [req.user.accountId]); res.json({ ok: true, users: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } });
app.post("/admin/users", authMiddleware, async (req, res) => {
const client = await pool.connect();
try {
const { fullName, email, password, role, guilds, phone } = req.body;
if (!email || !password || !fullName || !phone) return res.status(400).json({ ok: false });
const p = normalizePhone(phone); const hash = await bcrypt.hash(password, 10);
const check = await client.query("SELECT id FROM users WHERE (phone=$1 OR email=$2) AND owner_id=$3", [p, email, req.user.accountId]);
if (check.rowCount > 0) return res.status(400).json({ ok: false, error: "Duplicado" });
await client.query('BEGIN');
const insert = await client.query("INSERT INTO users (full_name, email, password_hash, role, phone, is_verified, owner_id) VALUES ($1, $2, $3, $4, $5, TRUE, $6) RETURNING id", [fullName, email, hash, role || 'operario', p, req.user.accountId]);
const uid = insert.rows[0].id;
if (guilds) for (const gid of guilds) await client.query("INSERT INTO user_guilds (user_id, guild_id) VALUES ($1, $2)", [uid, gid]);
await client.query('COMMIT'); res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
});
app.put("/admin/users/:id", authMiddleware, async (req, res) => {
const client = await pool.connect();
try {
const userId = req.params.id; const { fullName, email, phone, role, guilds, password } = req.body; const p = normalizePhone(phone);
await client.query('BEGIN');
if(password) { const hash = await bcrypt.hash(password, 10); await client.query("UPDATE users SET full_name=$1, email=$2, phone=$3, role=$4, password_hash=$5 WHERE id=$6", [fullName, email, p, role, hash, userId]); }
else { await client.query("UPDATE users SET full_name=$1, email=$2, phone=$3, role=$4 WHERE id=$5", [fullName, email, p, role, userId]); }
if (guilds && Array.isArray(guilds)) { await client.query("DELETE FROM user_guilds WHERE user_id=$1", [userId]); for (const gid of guilds) await client.query("INSERT INTO user_guilds (user_id, guild_id) VALUES ($1, $2)", [userId, gid]); }
await client.query('COMMIT'); res.json({ ok: true });
} catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); }
});
app.post("/admin/users", authMiddleware, async (req, res) => { const client = await pool.connect(); try { const { fullName, email, password, role, guilds, phone } = req.body; if (!email || !password || !fullName || !phone) return res.status(400).json({ ok: false }); const p = normalizePhone(phone); const hash = await bcrypt.hash(password, 10); const check = await client.query("SELECT id FROM users WHERE (phone=$1 OR email=$2) AND owner_id=$3", [p, email, req.user.accountId]); if (check.rowCount > 0) return res.status(400).json({ ok: false, error: "Duplicado" }); await client.query('BEGIN'); const insert = await client.query("INSERT INTO users (full_name, email, password_hash, role, phone, is_verified, owner_id) VALUES ($1, $2, $3, $4, $5, TRUE, $6) RETURNING id", [fullName, email, hash, role || 'operario', p, req.user.accountId]); const uid = insert.rows[0].id; if (guilds) for (const gid of guilds) await client.query("INSERT INTO user_guilds (user_id, guild_id) VALUES ($1, $2)", [uid, gid]); await client.query('COMMIT'); res.json({ ok: true }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } });
app.put("/admin/users/:id", authMiddleware, async (req, res) => { const client = await pool.connect(); try { const userId = req.params.id; const { fullName, email, phone, role, guilds, password } = req.body; const p = normalizePhone(phone); await client.query('BEGIN'); if(password) { const hash = await bcrypt.hash(password, 10); await client.query("UPDATE users SET full_name=$1, email=$2, phone=$3, role=$4, password_hash=$5 WHERE id=$6", [fullName, email, p, role, hash, userId]); } else { await client.query("UPDATE users SET full_name=$1, email=$2, phone=$3, role=$4 WHERE id=$5", [fullName, email, p, role, userId]); } if (guilds && Array.isArray(guilds)) { await client.query("DELETE FROM user_guilds WHERE user_id=$1", [userId]); for (const gid of guilds) await client.query("INSERT INTO user_guilds (user_id, guild_id) VALUES ($1, $2)", [userId, gid]); } await client.query('COMMIT'); res.json({ ok: true }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } });
app.delete("/admin/users/:id", authMiddleware, async (req, res) => { try { await pool.query("DELETE FROM users 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 }); } });
const port = process.env.PORT || 3000;