Actualizar server.js
This commit is contained in:
166
server.js
166
server.js
@@ -2019,82 +2019,126 @@ app.post("/public/assignment/:token/reject", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 💰 MOTOR FINANCIERO Y CONTABILIDAD (PREPARADO PARA ROBOT PDF)
|
// 📄 MOTOR DE PRESUPUESTOS Y CATÁLOGO DE ARTÍCULOS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Creamos la tabla financiera preparada para facturas y robots
|
|
||||||
pool.query(`
|
pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS service_financials (
|
CREATE TABLE IF NOT EXISTS articles (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
scraped_id INT REFERENCES scraped_services(id) ON DELETE CASCADE UNIQUE,
|
owner_id INT REFERENCES users(id) ON DELETE CASCADE,
|
||||||
amount DECIMAL(10,2) DEFAULT 0.00,
|
name TEXT NOT NULL,
|
||||||
payment_method TEXT,
|
price DECIMAL(10,2) DEFAULT 0.00,
|
||||||
is_paid BOOLEAN DEFAULT false,
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
invoice_ref TEXT,
|
);
|
||||||
pdf_raw_data JSONB,
|
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
CREATE TABLE IF NOT EXISTS budgets (
|
||||||
updated_at TIMESTAMP DEFAULT NOW()
|
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', -- pending, accepted, rejected, converted
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
`).catch(console.error);
|
`).catch(console.error);
|
||||||
|
|
||||||
// Obtener toda la contabilidad
|
// --- CATÁLOGO DE ARTÍCULOS ---
|
||||||
app.get("/financials", authMiddleware, async (req, res) => {
|
app.get("/articles", authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// 1. Truco de magia: Si hay servicios que no tienen ficha financiera, se la creamos automáticamente.
|
const q = await pool.query("SELECT * FROM articles WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]);
|
||||||
// Si tiene compañía asignada, le ponemos "Cobro Banco" por defecto. Si no, "Pendiente".
|
res.json({ ok: true, articles: q.rows });
|
||||||
await pool.query(`
|
} catch(e) { res.status(500).json({ok: false}); }
|
||||||
INSERT INTO service_financials (scraped_id, payment_method)
|
|
||||||
SELECT id,
|
|
||||||
CASE WHEN raw_data->>'Compañía' IS NOT NULL AND raw_data->>'Compañía' != '' AND raw_data->>'Compañía' != 'Particular'
|
|
||||||
THEN 'Cobro Banco'
|
|
||||||
ELSE 'Pendiente' END
|
|
||||||
FROM scraped_services
|
|
||||||
WHERE owner_id = $1 AND id NOT IN (SELECT scraped_id FROM service_financials)
|
|
||||||
`, [req.user.accountId]);
|
|
||||||
|
|
||||||
// 2. Devolvemos la lista cruzando las finanzas con los datos del servicio
|
|
||||||
const q = await pool.query(`
|
|
||||||
SELECT f.*, s.service_ref, s.raw_data, s.status
|
|
||||||
FROM service_financials f
|
|
||||||
JOIN scraped_services s ON f.scraped_id = s.id
|
|
||||||
WHERE s.owner_id = $1
|
|
||||||
ORDER BY f.updated_at DESC
|
|
||||||
`, [req.user.accountId]);
|
|
||||||
|
|
||||||
res.json({ ok: true, financials: q.rows });
|
|
||||||
} catch(e) {
|
|
||||||
console.error("Error financiero:", e);
|
|
||||||
res.status(500).json({ ok: false });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Guardar un cobro/pago
|
app.post("/articles", authMiddleware, async (req, res) => {
|
||||||
app.put("/financials/:id", authMiddleware, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { amount, payment_method } = req.body;
|
const { name, price } = req.body;
|
||||||
const parsedAmount = parseFloat(amount) || 0;
|
await pool.query("INSERT INTO articles (owner_id, name, price) VALUES ($1, $2, $3)", [req.user.accountId, name, price]);
|
||||||
|
res.json({ ok: true });
|
||||||
// NUEVA REGLA: Si el método de pago es "Pendiente", NO está pagado,
|
} catch(e) { res.status(500).json({ok: false}); }
|
||||||
// independientemente del importe que tenga apuntado (Ej: Presupuestos).
|
});
|
||||||
const isPaid = payment_method !== 'Pendiente';
|
|
||||||
|
|
||||||
await pool.query(`
|
app.put("/articles/:id", authMiddleware, async (req, res) => {
|
||||||
UPDATE service_financials
|
try {
|
||||||
SET amount = $1, payment_method = $2, is_paid = $3, updated_at = NOW()
|
const { name, price } = req.body;
|
||||||
WHERE scraped_id = $4
|
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]);
|
||||||
`, [parsedAmount, payment_method, isPaid, req.params.id]);
|
res.json({ ok: true });
|
||||||
|
} catch(e) { res.status(500).json({ok: false}); }
|
||||||
|
});
|
||||||
|
|
||||||
// LOG AUTOMÁTICO DE TRAZABILIDAD
|
// --- PRESUPUESTOS ---
|
||||||
const userQ = await pool.query("SELECT full_name FROM users WHERE id=$1", [req.user.sub]);
|
app.get("/budgets", authMiddleware, async (req, res) => {
|
||||||
const userName = userQ.rows[0]?.full_name || "Sistema";
|
try {
|
||||||
|
const q = await pool.query("SELECT * FROM budgets WHERE owner_id=$1 ORDER BY 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(
|
await pool.query(
|
||||||
"INSERT INTO scraped_service_logs (scraped_id, user_name, action, details) VALUES ($1, $2, $3, $4)",
|
"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.params.id, userName, "Cobro Actualizado", `Importe: ${parsedAmount}€ | Método: ${payment_method}`]
|
[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 {
|
||||||
|
const { status } = req.body;
|
||||||
|
await pool.query("UPDATE budgets SET status=$1 WHERE id=$2 AND owner_id=$3", [status, req.params.id, req.user.accountId]);
|
||||||
|
res.json({ ok: true });
|
||||||
|
} catch(e) { res.status(500).json({ok: false}); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convertir Presupuesto en Servicio Activo
|
||||||
|
app.post("/budgets/:id/convert", authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { date, time } = 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"),
|
||||||
|
"scheduled_date": date,
|
||||||
|
"scheduled_time": time
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Insertamos en el Panel Operativo (Buzón)
|
||||||
|
const insertSvc = await pool.query(
|
||||||
|
"INSERT INTO scraped_services (owner_id, provider, service_ref, status, automation_status, raw_data) VALUES ($1, 'particular', $2, 'pending', 'manual', $3) RETURNING id",
|
||||||
|
[req.user.accountId, `PRE-${budget.id}`, JSON.stringify(rawData)]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ ok: true, is_paid: isPaid });
|
// 3. Marcamos presupuesto como convertido y le enlazamos la ficha financiera por el total
|
||||||
} catch(e) {
|
await pool.query("UPDATE budgets SET status='converted' WHERE id=$1", [budget.id]);
|
||||||
console.error(e);
|
await pool.query(
|
||||||
res.status(500).json({ ok: false });
|
"INSERT INTO service_financials (scraped_id, amount, payment_method) VALUES ($1, $2, 'Pendiente')",
|
||||||
|
[insertSvc.rows[0].id, budget.total]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Mandamos WhatsApp de confirmación
|
||||||
|
if (budget.client_phone) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ ok: true });
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Error convirtiendo presupuesto:", e);
|
||||||
|
res.status(500).json({ok: false});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user