From f4d6d985c6e64f354ab5bb35b574db15bf2e70d7 Mon Sep 17 00:00:00 2001 From: marsalva Date: Mon, 7 Sep 2026 07:41:06 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1ade=20endpoint=20/services/search=20para?= =?UTF-8?q?=20el=20nuevo=20buscador=20global=20de=20la=20App?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- server.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/server.js b/server.js index 86c22d6..0639c1e 100644 --- a/server.js +++ b/server.js @@ -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;