Añade Ficha del Cliente: historial de servicios y notas del equipo
Nueva tabla client_notes + endpoints GET/POST /clients/:id/history y /clients/:id/notes. También corrige POST /clients/ensure para devolver la fila completa del cliente (antes solo devolvía portal_token, faltaba el id).
This commit is contained in:
84
server.js
84
server.js
@@ -4152,10 +4152,12 @@ app.post('/clients/ensure', authMiddleware, async (req, res) => {
|
||||
} else {
|
||||
const newToken = crypto.randomBytes(6).toString('hex');
|
||||
const insert = await pool.query(
|
||||
"INSERT INTO clients (owner_id, full_name, phone, addresses, portal_token) VALUES ($1, $2, $3, $4, $5) RETURNING portal_token",
|
||||
"INSERT INTO clients (owner_id, full_name, phone, addresses, portal_token) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
[ownerId, name || "Cliente", phone, JSON.stringify([address || ""]), newToken]
|
||||
);
|
||||
res.json({ ok: true, client: { portal_token: insert.rows[0].portal_token } });
|
||||
// 🆕 Devolvemos la fila completa (antes solo el portal_token), porque ahora la
|
||||
// Ficha del Cliente de la App necesita el id del cliente recién creado.
|
||||
res.json({ ok: true, client: insert.rows[0] });
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: "Error interno del servidor" });
|
||||
@@ -5032,6 +5034,84 @@ app.get("/services/:id/logs", authMiddleware, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 🧑🔧 FICHA DEL CLIENTE (App): historial de servicios + notas propias del equipo
|
||||
// ==========================================
|
||||
pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS client_notes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_id INT REFERENCES clients(id) ON DELETE CASCADE,
|
||||
owner_id INT REFERENCES users(id) ON DELETE CASCADE,
|
||||
user_name TEXT,
|
||||
note TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`).catch(console.error);
|
||||
|
||||
// Ficha completa: datos del cliente + todos sus servicios anteriores (de cualquier compañía),
|
||||
// buscados por teléfono igual que hace /clients/search.
|
||||
app.get("/clients/:id/history", authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const ownerId = req.user.accountId;
|
||||
const clientQ = await pool.query("SELECT * FROM clients WHERE id=$1 AND owner_id=$2", [req.params.id, ownerId]);
|
||||
if (clientQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Cliente no encontrado" });
|
||||
const client = clientQ.rows[0];
|
||||
|
||||
const bare = extractValidPhone(client.phone);
|
||||
const servicesQ = await pool.query(`
|
||||
SELECT s.id, s.provider, s.service_ref, s.status, s.raw_data, s.created_at,
|
||||
st.name as status_name, st.color as status_color, st.is_final as status_is_final
|
||||
FROM scraped_services s
|
||||
LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text
|
||||
WHERE s.owner_id=$1
|
||||
AND regexp_replace(COALESCE(s.raw_data->>'Teléfono', s.raw_data->>'TELEFONO', s.raw_data->>'TELEFONOS', ''), '\\D', '', 'g') LIKE '%' || $2 || '%'
|
||||
ORDER BY s.created_at DESC
|
||||
LIMIT 100
|
||||
`, [ownerId, bare]);
|
||||
|
||||
res.json({ ok: true, client, services: servicesQ.rows });
|
||||
} catch (e) {
|
||||
console.error("Error cargando ficha de cliente:", e);
|
||||
res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Notas del equipo sobre este cliente (visibles para toda la cuenta, no solo quien las escribe)
|
||||
app.get("/clients/:id/notes", authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const q = await pool.query(`
|
||||
SELECT n.* FROM client_notes n
|
||||
JOIN clients c ON n.client_id = c.id
|
||||
WHERE n.client_id = $1 AND c.owner_id = $2
|
||||
ORDER BY n.created_at DESC
|
||||
`, [req.params.id, req.user.accountId]);
|
||||
res.json({ ok: true, notes: q.rows });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/clients/:id/notes", authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { note } = req.body;
|
||||
if (!note || !note.trim()) return res.status(400).json({ ok: false, error: "La nota está vacía" });
|
||||
|
||||
const check = await pool.query("SELECT id FROM clients WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]);
|
||||
if (check.rowCount === 0) return res.status(403).json({ ok: false, error: "No autorizado" });
|
||||
|
||||
const userQ = await pool.query("SELECT full_name FROM users WHERE id=$1", [req.user.sub]);
|
||||
const userName = userQ.rows[0]?.full_name || "Usuario";
|
||||
|
||||
const insert = await pool.query(
|
||||
"INSERT INTO client_notes (client_id, owner_id, user_name, note) VALUES ($1, $2, $3, $4) RETURNING *",
|
||||
[req.params.id, req.user.accountId, userName, note.trim()]
|
||||
);
|
||||
res.json({ ok: true, note: insert.rows[0] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false });
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 📄 MOTOR DE PRESUPUESTOS (continuación) — POST/PUT/PATCH de /budgets
|
||||
// 🩹 FIX: este bloque duplicaba por completo la creación de tablas y las rutas
|
||||
|
||||
Reference in New Issue
Block a user