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) => {
|
app.post("/budgets", authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { client_phone, client_name, client_address, items, subtotal, tax, total } = req.body;
|
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 });
|
res.json({ ok: true });
|
||||||
} catch(e) { res.status(500).json({ok: false}); }
|
} catch(e) { res.status(500).json({ok: false}); }
|
||||||
});
|
});
|
||||||
|
// (DELETE /budgets/:id ya está registrado más arriba, junto con GET/POST/PUT /articles y GET /budgets — no se repite aquí)
|
||||||
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"});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 🤖 API COLA DEL ROBOT (REEMPLAZO FIREBASE)
|
// 🤖 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
|
// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS
|
||||||
// 🧠 Ejecuta realmente el turno de la IA para un expediente (llamado tras el debounce)
|
// 🧠 Ejecuta realmente el turno de la IA para un expediente (llamado tras el debounce)
|
||||||
async function procesarTurnoIA({ ownerId, service, mensajeTexto, telefonoCliente, instanceName }) {
|
async function procesarTurnoIA({ ownerId, service, mensajeTexto, telefonoCliente, instanceName }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user