Arregla chat y presupuestos

This commit is contained in:
2026-08-19 19:35:48 +00:00
parent 81669eb2eb
commit 9f3e1d96c4

137
server.js
View File

@@ -4694,73 +4694,12 @@ app.get("/services/:id/logs", authMiddleware, async (req, res) => {
});
// ==========================================
// 📄 MOTOR DE PRESUPUESTOS Y CATÁLOGO DE ARTÍCULOS
// 📄 MOTOR DE PRESUPUESTOS (continuación) — POST/PUT/PATCH de /budgets
// 🩹 FIX: este bloque duplicaba por completo la creación de tablas y las rutas
// GET/POST/PUT /articles, GET /budgets y DELETE /budgets/:id que ya se registran
// arriba (Express siempre usaba la primera copia; la segunda era código muerto).
// Se ha fusionado en un único bloque, dejando solo las rutas que eran nuevas aquí.
// ==========================================
pool.query(`
CREATE TABLE IF NOT EXISTS articles (
id SERIAL PRIMARY KEY,
owner_id INT REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
price DECIMAL(10,2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS budgets (
id SERIAL PRIMARY KEY,
owner_id INT REFERENCES users(id) ON DELETE CASCADE,
client_phone TEXT,
client_name TEXT,
client_address TEXT,
items JSONB DEFAULT '[]',
subtotal DECIMAL(10,2) DEFAULT 0.00,
tax DECIMAL(10,2) DEFAULT 0.00,
total DECIMAL(10,2) DEFAULT 0.00,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
);
`).catch(console.error);
// --- CATÁLOGO DE ARTÍCULOS ---
app.get("/articles", authMiddleware, async (req, res) => {
try {
const q = await pool.query("SELECT * FROM articles WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]);
res.json({ ok: true, articles: q.rows });
} catch(e) { res.status(500).json({ok: false}); }
});
app.post("/articles", authMiddleware, async (req, res) => {
try {
const { name, price } = req.body;
await pool.query("INSERT INTO articles (owner_id, name, price) VALUES ($1, $2, $3)", [req.user.accountId, name, price]);
res.json({ ok: true });
} catch(e) { res.status(500).json({ok: false}); }
});
app.put("/articles/:id", authMiddleware, async (req, res) => {
try {
const { name, price } = req.body;
await pool.query("UPDATE articles SET name=$1, price=$2 WHERE id=$3 AND owner_id=$4", [name, price, req.params.id, req.user.accountId]);
res.json({ ok: true });
} catch(e) { res.status(500).json({ok: false}); }
});
// --- PRESUPUESTOS ---
app.get("/budgets", authMiddleware, async (req, res) => {
try {
const q = await pool.query(`
SELECT b.*,
s.status as linked_service_status,
st.name as linked_service_status_name
FROM budgets b
LEFT JOIN scraped_services s ON s.service_ref = 'PRE-' || b.id AND s.owner_id = b.owner_id
LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text
WHERE b.owner_id=$1
ORDER BY b.created_at DESC
`, [req.user.accountId]);
res.json({ ok: true, budgets: q.rows });
} catch(e) { res.status(500).json({ok: false}); }
});
app.post("/budgets", authMiddleware, async (req, res) => {
try {
const { client_phone, client_name, client_address, items, subtotal, tax, total } = req.body;
@@ -4851,36 +4790,7 @@ app.patch("/budgets/:id/status", authMiddleware, async (req, res) => {
res.json({ ok: true });
} catch(e) { res.status(500).json({ok: false}); }
});
app.delete("/budgets/:id", authMiddleware, async (req, res) => {
try {
const q = await pool.query("SELECT status FROM budgets WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]);
if (q.rowCount === 0) return res.status(404).json({ok: false, error: "No encontrado"});
const status = q.rows[0].status;
const sq = await pool.query(`
SELECT st.name as status_name
FROM scraped_services s
LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text
WHERE s.service_ref = $1 AND s.owner_id = $2
`, [`PRE-${req.params.id}`, req.user.accountId]);
let isAnulado = false;
if (sq.rowCount > 0 && sq.rows[0].status_name && sq.rows[0].status_name.toLowerCase().includes('anulado')) {
isAnulado = true;
}
if ((status === 'accepted' || status === 'converted') && !isAnulado) {
return res.status(400).json({ok: false, error: "Para poder borrar un presupuesto, el servicio primero debe estar anulado."});
}
await pool.query("DELETE FROM budgets 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, error: "Error interno"});
}
});
// (DELETE /budgets/:id ya está registrado más arriba, junto con GET/POST/PUT /articles y GET /budgets — no se repite aquí)
// ==========================================
// 🤖 API COLA DEL ROBOT (REEMPLAZO FIREBASE)
@@ -4980,6 +4890,41 @@ app.get("/services/:id/chat", authMiddleware, async (req, res) => {
}
});
// 🩹 FIX: faltaba el endpoint para ENVIAR mensajes desde el panel (solo existía el de leer historial)
app.post("/services/:id/chat", authMiddleware, async (req, res) => {
try {
const { id } = req.params;
const { message, is_internal } = req.body;
if (!message || !message.toString().trim()) {
return res.status(400).json({ ok: false, error: "El mensaje está vacío" });
}
// Verificamos que el servicio pertenece a esta cuenta antes de escribir
const svcQ = await pool.query("SELECT id FROM scraped_services WHERE id=$1 AND owner_id=$2", [id, req.user.accountId]);
if (svcQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Servicio no encontrado" });
const isOperario = req.user.role === 'operario' || req.user.role === 'operario_cerrado';
// Las notas internas son solo cosa de admin: un operario nunca puede marcar su mensaje como interno
const finalIsInternal = isOperario ? false : (is_internal === true || is_internal === 'true');
const userQ = await pool.query("SELECT full_name FROM users WHERE id=$1", [req.user.sub]);
const senderName = userQ.rows[0]?.full_name || (isOperario ? 'Operario' : 'Admin');
const insertQ = await pool.query(
`INSERT INTO service_communications (scraped_id, owner_id, sender_id, sender_name, sender_role, message, is_internal)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, sender_id, sender_name, sender_role, message, is_internal, created_at`,
[id, req.user.accountId, req.user.sub, senderName, req.user.role, message.toString().trim(), finalIsInternal]
);
res.json({ ok: true, message: insertQ.rows[0] });
} catch (e) {
console.error("Error enviando mensaje de chat:", e);
res.status(500).json({ ok: false });
}
});
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
// 🧠 Ejecuta realmente el turno de la IA para un expediente (llamado tras el debounce)
async function procesarTurnoIA({ ownerId, service, mensajeTexto, telefonoCliente, instanceName }) {