diff --git a/server.js b/server.js index e13549e..b5422a9 100644 --- a/server.js +++ b/server.js @@ -2280,6 +2280,52 @@ app.post("/public/portal/:token/interest", async (req, res) => { // ========================================== // 🔗 PORTAL PÚBLICO DEL CLIENTE // ========================================== +// 🧽 LIMPIEZA DE AVERÍA PARA EL CLIENTE (con IA + caché en raw_data) +// El campo "Descripción" va acumulando avisos automáticos, cambios de cita, códigos internos, etc. +// Aquí lo convertimos en una frase breve y sin ruido para que la vea el cliente en el Portal. +async function getClientFriendlyDescription(service) { + const rawDesc = (service.raw_data?.["Descripción"] || service.raw_data?.["DESCRIPCION"] || "").toString().trim(); + if (!rawDesc) return "Aviso de reparación"; + + const rawHash = crypto.createHash('md5').update(rawDesc).digest('hex'); + + // Si ya la limpiamos antes y el texto original no ha cambiado, reutilizamos la versión guardada + if (service.raw_data?.portal_desc_hash === rawHash && service.raw_data?.portal_desc_clean) { + return service.raw_data.portal_desc_clean; + } + + try { + const completion = await openai.chat.completions.create({ + model: OPENAI_MODEL || "gpt-4o-mini", + messages: [ + { + role: "system", + content: "Redactas, para el CLIENTE de una empresa de reparaciones del hogar, una frase muy breve (máximo 18 palabras) que describa SOLO el problema técnico o avería en su domicilio. No menciones fechas, horas, citas, cambios de cita, nombres de operarios o agentes, teléfonos, códigos de expediente, aseguradoras, ni el estado del trámite administrativo. Si el texto no describe ningún problema técnico claro, responde exactamente: \"Revisión técnica en el domicilio.\". Responde en español, en una sola frase, sin comillas ni markdown." + }, + { role: "user", content: rawDesc.slice(0, 2000) } + ], + temperature: 0.2, + max_tokens: 60 + }); + + const clean = completion.choices?.[0]?.message?.content?.trim(); + if (!clean) throw new Error("La IA no devolvió texto"); + + // Guardamos en caché de forma asíncrona (no bloqueamos la respuesta al cliente ni si falla) + pool.query(` + UPDATE scraped_services + SET raw_data = jsonb_set(jsonb_set(COALESCE(raw_data, '{}'::jsonb), '{portal_desc_clean}', $1::jsonb), '{portal_desc_hash}', $2::jsonb) + WHERE id = $3 + `, [JSON.stringify(clean), JSON.stringify(rawHash), service.id]).catch(e => console.error("⚠️ No se pudo cachear la avería limpia:", e.message)); + + return clean; + } catch (e) { + console.error("❌ Error limpiando avería con IA:", e.message); + // Si la IA falla, devolvemos algo razonable en lugar del volcado crudo completo + return rawDesc.length > 150 ? rawDesc.slice(0, 150).trim() + "..." : rawDesc; + } +} + app.get("/public/portal/:token", async (req, res) => { try { const { token } = req.params; @@ -2331,20 +2377,21 @@ app.get("/public/portal/:token", async (req, res) => { `, [ownerId, `%${phoneMatch}%`]); } - const formattedServices = qServices.rows.map(s => { + const formattedServices = await Promise.all(qServices.rows.map(async s => { return { id: s.id, title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`, description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación", - status_name: s.real_status_name || "En gestión", - is_final: s.is_status_final || false, + description_clean: await getClientFriendlyDescription(s), + status_name: s.real_status_name || "En gestión", + is_final: s.is_status_final || false, scheduled_date: s.raw_data?.scheduled_date || "", scheduled_time: s.raw_data?.scheduled_time || "", assigned_worker: s.worker_name || null, - worker_phone: s.worker_phone || null, + worker_phone: s.worker_phone || null, raw_data: s.raw_data }; - }); + })); // 4. BUSCAR PRESUPUESTOS CON SUS ARTÍCULOS let cleanPhoneSearch = String(client.phone || "").replace(/[^0-9]/g, "");