Arregla chat y presupuestos
This commit is contained in:
137
server.js
137
server.js
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user