Actualizar server.js
This commit is contained in:
202
server.js
202
server.js
@@ -2482,6 +2482,208 @@ app.get("/services/:id/logs", authMiddleware, async (req, res) => {
|
|||||||
} catch(e) { res.status(500).json({ ok: false }); }
|
} catch(e) { res.status(500).json({ ok: false }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 📄 MOTOR DE PRESUPUESTOS Y CATÁLOGO DE ARTÍCULOS
|
||||||
|
// ==========================================
|
||||||
|
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;
|
||||||
|
await pool.query(
|
||||||
|
"INSERT INTO budgets (owner_id, client_phone, client_name, client_address, items, subtotal, tax, total) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||||
|
[req.user.accountId, client_phone, client_name, client_address, JSON.stringify(items), subtotal, tax, total]
|
||||||
|
);
|
||||||
|
res.json({ ok: true });
|
||||||
|
} catch(e) { res.status(500).json({ok: false}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch("/budgets/:id/status", authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await pool.query("UPDATE budgets SET status=$1 WHERE id=$2 AND owner_id=$3", [req.body.status, req.params.id, req.user.accountId]);
|
||||||
|
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"});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convertir Presupuesto en Servicio Activo (CON SOPORTE RED INTERNA Y TRAZABILIDAD)
|
||||||
|
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];
|
||||||
|
|
||||||
|
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 || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 })
|
||||||
|
}).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) {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- INICIO TRAZABILIDAD ---
|
||||||
|
if (typeof registrarMovimiento === "function") {
|
||||||
|
await registrarMovimiento(newServiceId, req.user.sub, "Aviso Creado", `Servicio generado a raíz del presupuesto aceptado #PRE-${budget.id}.`);
|
||||||
|
}
|
||||||
|
// --- FIN TRAZABILIDAD ---
|
||||||
|
|
||||||
|
res.json({ ok: true });
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Error convirtiendo presupuesto:", e);
|
||||||
|
res.status(500).json({ok: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 🕒 EL RELOJ DEL SISTEMA (Ejecutar cada minuto)
|
// 🕒 EL RELOJ DEL SISTEMA (Ejecutar cada minuto)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user