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;