Añade endpoint /services/search para el nuevo buscador global de la App

Busca en TODO el histórico de la cuenta (cualquier técnico, cualquier estado: citado/anulado/finalizado), filtrando por fecha de expediente o de cita, texto libre y compañía, con paginación (limit/offset). Exige fecha o texto para no devolver la tabla entera.
This commit is contained in:
2026-09-07 07:41:06 +00:00
parent 219d049045
commit f4d6d985c6

View File

@@ -3682,6 +3682,75 @@ app.get("/services/active", authMiddleware, async (req, res) => {
}
});
// ==========================================
// 🔎 BUSCADOR GLOBAL (App): cualquier servicio de la empresa, cualquier estado, filtrando por fechas
// ==========================================
// A diferencia de /services/active (que para un operario solo enseña lo suyo, y solo lo archivado
// de los últimos 15 días), este endpoint busca en TODO el histórico de la cuenta —cualquier técnico,
// cualquier estado (citado, anulado, finalizado...)— pero exige un filtro (fecha o texto) para no
// volcar la base de datos entera de golpe.
app.get("/services/search", authMiddleware, async (req, res) => {
try {
const ownerId = req.user.accountId;
const { q, date_field, date_from, date_to, company } = req.query;
const limit = Math.min(parseInt(req.query.limit) || 30, 100);
const offset = Math.max(parseInt(req.query.offset) || 0, 0);
const useCitaDate = date_field === 'scheduled_date';
const conditions = ["s.owner_id = $1", "s.provider != 'SYSTEM_BLOCK'"];
const params = [ownerId];
let idx = 2;
if (useCitaDate) {
if (date_from) { conditions.push(`(s.raw_data->>'scheduled_date') >= $${idx++}`); params.push(date_from); }
if (date_to) { conditions.push(`(s.raw_data->>'scheduled_date') <= $${idx++}`); params.push(date_to); }
} else {
if (date_from) { conditions.push(`s.created_at >= $${idx++}::date`); params.push(date_from); }
if (date_to) { conditions.push(`s.created_at < ($${idx++}::date + INTERVAL '1 day')`); params.push(date_to); }
}
if (company) {
conditions.push(`COALESCE(s.raw_data->>'Compañía', s.raw_data->>'COMPAÑIA', s.raw_data->>'Procedencia', '') ILIKE $${idx++}`);
params.push(company);
}
const hasDateRange = !!(date_from || date_to);
const term = (q || "").trim();
if (term !== "") {
conditions.push(`(
s.service_ref ILIKE $${idx} OR
COALESCE(s.raw_data->>'Nombre Cliente', s.raw_data->>'CLIENTE', '') ILIKE $${idx} OR
COALESCE(s.raw_data->>'Dirección', s.raw_data->>'DOMICILIO', '') ILIKE $${idx} OR
COALESCE(s.raw_data->>'Población', s.raw_data->>'POBLACION-PROVINCIA', '') ILIKE $${idx} OR
COALESCE(s.raw_data->>'Teléfono', s.raw_data->>'TELEFONO', s.raw_data->>'TELEFONOS', '') ILIKE $${idx} OR
COALESCE(s.raw_data->>'Compañía', s.raw_data->>'COMPAÑIA', '') ILIKE $${idx}
)`);
params.push(`%${term}%`);
idx++;
} else if (!hasDateRange) {
// Ni fecha ni texto: no devolvemos nada para no volcar el histórico entero.
return res.json({ ok: true, services: [], hasMore: false, needsFilter: true });
}
params.push(limit, offset);
const query = `
SELECT s.*, u.full_name as assigned_name
FROM scraped_services s
LEFT JOIN users u ON s.assigned_to = u.id
WHERE ${conditions.join(' AND ')}
ORDER BY s.created_at DESC
LIMIT $${idx++} OFFSET $${idx}
`;
const result = await pool.query(query, params);
const servicesWithPhones = result.rows.map(s => ({ ...s, phones: extractAllPhones(s.raw_data) }));
res.json({ ok: true, services: servicesWithPhones, hasMore: result.rows.length === limit });
} catch (e) {
console.error("Error en /services/search:", e);
res.status(500).json({ ok: false });
}
});
app.put("/services/set-appointment/:id", authMiddleware, async (req, res) => {
try {
const { id } = req.params;