Actualizar server.js
This commit is contained in:
168
server.js
168
server.js
@@ -1750,6 +1750,84 @@ app.post("/config/company", authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// RUTA: Alta de expediente manual con validación de cliente
|
||||||
|
app.post("/services/manual-high", authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { phone, name, address, description, guild_id, assigned_to, duration_minutes, mode, is_company, company_name, company_ref } = req.body;
|
||||||
|
const ownerId = req.user.accountId;
|
||||||
|
|
||||||
|
// 1. Manejo del Cliente (Buscamos si existe por teléfono)
|
||||||
|
const cleanPhone = phone.replace(/\D/g, "");
|
||||||
|
let clientQ = await pool.query("SELECT id, addresses FROM clients WHERE phone LIKE $1 AND owner_id = $2", [`%${cleanPhone}%`, ownerId]);
|
||||||
|
|
||||||
|
let clientId;
|
||||||
|
if (clientQ.rowCount > 0) {
|
||||||
|
clientId = clientQ.rows[0].id;
|
||||||
|
let currentAddrs = clientQ.rows[0].addresses || [];
|
||||||
|
// Si la dirección es nueva, la añadimos a su ficha
|
||||||
|
if (!currentAddrs.includes(address)) {
|
||||||
|
currentAddrs.push(address);
|
||||||
|
await pool.query("UPDATE clients SET addresses = $1 WHERE id = $2", [JSON.stringify(currentAddrs), clientId]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si no existe, creamos cliente nuevo
|
||||||
|
const token = crypto.randomBytes(6).toString('hex');
|
||||||
|
const newClient = await pool.query(
|
||||||
|
"INSERT INTO clients (owner_id, full_name, phone, addresses, portal_token) VALUES ($1, $2, $3, $4, $5) RETURNING id",
|
||||||
|
[ownerId, name, phone, JSON.stringify([address]), token]
|
||||||
|
);
|
||||||
|
clientId = newClient.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Crear el Expediente
|
||||||
|
const rawData = {
|
||||||
|
"Nombre Cliente": name,
|
||||||
|
"Teléfono": phone,
|
||||||
|
"Dirección": address,
|
||||||
|
"Descripción": description,
|
||||||
|
"guild_id": guild_id,
|
||||||
|
"scheduled_date": "",
|
||||||
|
"scheduled_time": "",
|
||||||
|
"duration_minutes": duration_minutes || 60,
|
||||||
|
"Compañía": is_company ? company_name : "Particular"
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceReference = is_company ? company_ref : `M-${Date.now().toString().slice(-6)}`;
|
||||||
|
|
||||||
|
const insertSvc = await pool.query(
|
||||||
|
`INSERT INTO scraped_services (owner_id, provider, service_ref, status, automation_status, assigned_to, raw_data)
|
||||||
|
VALUES ($1, 'MANUAL', $2, 'pending', $3, $4, $5) RETURNING id`,
|
||||||
|
[
|
||||||
|
ownerId,
|
||||||
|
serviceReference,
|
||||||
|
mode === 'auto' ? 'in_progress' : 'manual',
|
||||||
|
assigned_to || null,
|
||||||
|
JSON.stringify(rawData)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newId = insertSvc.rows[0].id;
|
||||||
|
|
||||||
|
// 3. Si se eligió "Mandar a la bolsa", llamamos internamente al robot
|
||||||
|
if (mode === 'auto' && guild_id) {
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
fetch(`http://127.0.0.1:${port}/providers/automate/${newId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': req.headers.authorization },
|
||||||
|
body: JSON.stringify({ guild_id, cp: "00000" })
|
||||||
|
}).catch(e => console.error("Error lanzando bolsa:", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TRAZABILIDAD ---
|
||||||
|
await registrarMovimiento(newId, req.user.sub, "Alta Manual", `Servicio creado manualmente (${rawData["Compañía"]}).`);
|
||||||
|
|
||||||
|
res.json({ ok: true, id: newId });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error Alta Manual:", e);
|
||||||
|
res.status(500).json({ ok: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 🛠️ RUTAS DE GREMIOS E INTELIGENCIA ARTIFICIAL
|
// 🛠️ RUTAS DE GREMIOS E INTELIGENCIA ARTIFICIAL
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -2279,94 +2357,6 @@ app.post("/budgets/:id/convert", authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convertir Presupuesto en Servicio Activo (CON SOPORTE RED INTERNA)
|
|
||||||
app.post("/budgets/:id/convert", authMiddleware, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { date, time, guild_id, assigned_to, use_automation } = req.body;
|
|
||||||
const bq = await pool.query("SELECT * FROM budgets WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]);
|
|
||||||
if (bq.rowCount === 0) return res.status(404).json({ok: false});
|
|
||||||
const budget = bq.rows[0];
|
|
||||||
|
|
||||||
// 1. Montamos el Raw Data para el servicio
|
|
||||||
const rawData = {
|
|
||||||
"Nombre Cliente": budget.client_name,
|
|
||||||
"Teléfono": budget.client_phone,
|
|
||||||
"Dirección": budget.client_address,
|
|
||||||
"Compañía": "Particular",
|
|
||||||
"Descripción": "PRESUPUESTO ACEPTADO.\n" + budget.items.map(i => `${i.qty}x ${i.concept}`).join("\n"),
|
|
||||||
"guild_id": guild_id || null,
|
|
||||||
"assigned_to": assigned_to || null,
|
|
||||||
"scheduled_date": date || "",
|
|
||||||
"scheduled_time": time || ""
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Insertamos en el Panel Operativo (Buzón) empezando en manual
|
|
||||||
const insertSvc = await pool.query(
|
|
||||||
"INSERT INTO scraped_services (owner_id, provider, service_ref, status, automation_status, assigned_to, raw_data) VALUES ($1, 'particular', $2, 'pending', 'manual', $3, $4) RETURNING id",
|
|
||||||
[
|
|
||||||
req.user.accountId,
|
|
||||||
`PRE-${budget.id}`,
|
|
||||||
assigned_to || null,
|
|
||||||
JSON.stringify(rawData)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const newServiceId = insertSvc.rows[0].id;
|
|
||||||
|
|
||||||
// 3. Marcamos presupuesto como convertido y le enlazamos la ficha financiera por el total
|
|
||||||
await pool.query("UPDATE budgets SET status='converted' WHERE id=$1", [budget.id]);
|
|
||||||
await pool.query(
|
|
||||||
"INSERT INTO service_financials (scraped_id, amount, payment_method) VALUES ($1, $2, 'Pendiente')",
|
|
||||||
[newServiceId, budget.total]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Si pide automatización, la disparamos internamente llamando a nuestra propia IP (127.0.0.1)
|
|
||||||
if (use_automation && guild_id) {
|
|
||||||
const cpMatch = budget.client_address ? budget.client_address.match(/\b\d{5}\b/) : null;
|
|
||||||
const cp = cpMatch ? cpMatch[0] : "00000";
|
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
// IMPORTANTE: 127.0.0.1 en lugar de localhost para evitar errores en Node
|
|
||||||
const autoUrl = `http://127.0.0.1:${port}/providers/automate/${newServiceId}`;
|
|
||||||
|
|
||||||
fetch(autoUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': req.headers.authorization
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ guild_id, cp, useDelay: false })
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(d => console.log("Llamada interna a automatización finalizada.", d))
|
|
||||||
.catch(e => console.error("Error lanzando automatización interna:", e));
|
|
||||||
|
|
||||||
if (budget.client_phone) {
|
|
||||||
const msg = `✅ *PRESUPUESTO ACEPTADO*\n\nHola ${budget.client_name}, confirmamos la aceptación del presupuesto por un total de *${budget.total}€*.\n\nEn breve un técnico se pondrá en contacto contigo para agendar la cita. ¡Gracias por confiar en nosotros!`;
|
|
||||||
sendWhatsAppAuto(budget.client_phone, msg, `cliente_${req.user.accountId}`, false).catch(console.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (budget.client_phone && date && time) {
|
|
||||||
// Asignación directa a un técnico con fecha y hora
|
|
||||||
const [y, m, d] = date.split('-');
|
|
||||||
const msg = `✅ *PRESUPUESTO ACEPTADO*\n\nHola ${budget.client_name}, confirmamos la aceptación del presupuesto por un total de *${budget.total}€*.\n\nEl servicio ha sido agendado para el *${d}/${m}/${y} a las ${time}*. ¡Gracias por confiar en nosotros!`;
|
|
||||||
sendWhatsAppAuto(budget.client_phone, msg, `cliente_${req.user.accountId}`, false).catch(console.error);
|
|
||||||
|
|
||||||
if (assigned_to) {
|
|
||||||
const statusQ = await pool.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND name ILIKE '%asignado%' LIMIT 1", [req.user.accountId]);
|
|
||||||
if (statusQ.rowCount > 0) {
|
|
||||||
rawData.status_operativo = statusQ.rows[0].id;
|
|
||||||
await pool.query("UPDATE scraped_services SET raw_data = $1 WHERE id = $2", [JSON.stringify(rawData), newServiceId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ ok: true });
|
|
||||||
} catch(e) {
|
|
||||||
console.error("Error convirtiendo presupuesto:", e);
|
|
||||||
res.status(500).json({ok: false});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -2490,7 +2480,7 @@ app.post("/services/:id/log", authMiddleware, async (req, res) => {
|
|||||||
// Ruta para LEER el historial de un servicio
|
// Ruta para LEER el historial de un servicio
|
||||||
app.get("/services/:id/logs", authMiddleware, async (req, res) => {
|
app.get("/services/:id/logs", authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// JOIN para asegurar que el log pertenece a un servicio del dueño actual
|
// Cruce con la tabla principal para verificar el dueño (owner_id)
|
||||||
const q = await pool.query(`
|
const q = await pool.query(`
|
||||||
SELECT l.* FROM scraped_service_logs l
|
SELECT l.* FROM scraped_service_logs l
|
||||||
JOIN scraped_services s ON l.scraped_id = s.id
|
JOIN scraped_services s ON l.scraped_id = s.id
|
||||||
|
|||||||
Reference in New Issue
Block a user