# -*- coding: utf-8 -*- """ patch_b2b_modal_v6.py - PRIMERO: Elimina declaraciones duplicadas de abrirWizardEdicion y cleanText - LUEGO: Actualiza colores, repara el link de propuesta y aplica modal moderno """ import os, subprocess, re INDEX = "/home/julianurr/woll-crm/woll-crm-docker/frontend/index.html" def find_line(lines, marker): for i, l in enumerate(lines): if marker in l: return i return -1 def remove_duplicate_function(lines, fn_keyword, open_bracket="{", close_bracket="}"): """ Elimina la SEGUNDA ocurrencia de un bloque de función identificado por fn_keyword. Devuelve las lÃneas limpias. """ occurrences = [] for i, l in enumerate(lines): if fn_keyword in l: occurrences.append(i) if len(occurrences) <= 1: print(f" '{fn_keyword}': solo 1 ocurrencia, nada que limpiar.") return lines print(f" '{fn_keyword}': {len(occurrences)} ocurrencias encontradas en lÃneas {[o+1 for o in occurrences]}") # Eliminamos todas excepto la PRIMERA # Vamos de la última hacia la primera para no alterar Ãndices for occ in reversed(occurrences[1:]): # Retroceder hasta encontrar la lÃnea que da inicio al bloque (puede ser un React.useEffect o const) start = occ while start > 0 and lines[start-1].strip() == '': start -= 1 # Avanzar hasta cerrar el bloque contando llaves depth = 0 end = occ found_open = False for j in range(occ, min(len(lines), occ + 300)): for ch in lines[j]: if ch == open_bracket: depth += 1 found_open = True elif ch == close_bracket and found_open: depth -= 1 if found_open and depth <= 0: end = j break # Eliminar el bloque completo (incluyendo lÃneas vacÃas previas) print(f" Eliminando bloque duplicado desde lÃnea {start+1} a {end+2}") del lines[start:end+2] print(f" ✅ Bloque duplicado eliminado") return lines def run(): print("=== PATCH B2B MODAL v6 (LIMPIEZA + MEJORA) ===") print("Leyendo index.html...") with open(INDEX, "r", encoding="utf-8", errors="ignore") as f: raw = f.read() lines = raw.split("\n") print(f"Total lÃneas al inicio: {len(lines)}") # ── PASO 1: Limpiar duplicados ────────────────────────────────────────── print("\n[1] Buscando y eliminando declaraciones duplicadas...") # Limpiar duplicados de abrirWizardEdicion lines = remove_duplicate_function(lines, "const abrirWizardEdicion = function") # Limpiar duplicados de cleanText lines = remove_duplicate_function(lines, "var cleanText = function") # Limpiar duplicados de React.useEffect(function() { que carguen propuesta # (solo los que tienen "proposalData" en las siguientes 10 lÃneas) ue_occurrences = [] for i, l in enumerate(lines): if "React.useEffect(function() {" in l: context = "\n".join(lines[i:min(len(lines), i+12)]) if "setProposalData" in context or "proposalData" in context: ue_occurrences.append(i) if len(ue_occurrences) > 1: print(f" useEffect(proposalData): {len(ue_occurrences)} ocurrencias en lÃneas {[o+1 for o in ue_occurrences]}") for occ in reversed(ue_occurrences[1:]): start = occ while start > 0 and lines[start-1].strip() == '': start -= 1 depth = 0 end = occ found_open = False for j in range(occ, min(len(lines), occ + 30)): for ch in lines[j]: if ch == '{': depth += 1; found_open = True elif ch == '}' and found_open: depth -= 1 if found_open and depth <= 0: end = j break # Avanzar también el ], [solSel]); del useEffect if end + 1 < len(lines) and '], [solSel])' in lines[end+1]: end += 1 print(f" Eliminando useEffect duplicado desde lÃnea {start+1} a {end+2}") del lines[start:end+2] print(f" ✅ useEffect duplicado eliminado") else: print(f" useEffect(proposalData): solo {len(ue_occurrences)} ocurrencia(s), OK") print(f"\nTotal lÃneas tras limpieza: {len(lines)}") # Guardar lÃneas limpias en raw_clean raw_clean = "\n".join(lines) # ── PASO 2: Actualizar PASO_COLORS y PASO_BG ─────────────────────────── print("\n[2] Actualizando paleta de colores premium...") new_colors = "const PASO_COLORS = ['#94a3b8', '#38bdf8', '#14b8a6', '#3b82f6', '#34d399'];" new_bg = "const PASO_BG = ['rgba(148,163,184,0.1)', 'rgba(56,189,248,0.1)', 'rgba(20,184,166,0.1)', 'rgba(59,130,246,0.1)', 'rgba(52,211,153,0.1)'];" color_patterns = [ "const PASO_COLORS = ['#94a3b8', '#38bdf8', '#fbbf24', '#a78bfa', '#34d399'];", "const PASO_COLORS = ['#64748B', '#38bdf8', '#f59e0b', '#8b5cf6', '#10b981'];", "const PASO_COLORS = ['#94a3b8', '#38bdf8', '#dfc07b', '#8f83f8', '#34d399'];", "const PASO_COLORS = ['#94a3b8', '#38bdf8', '#14b8a6', '#3b82f6', '#34d399'];", ] bg_patterns = [ "const PASO_BG = ['rgba(148,163,184,0.1)', 'rgba(56,189,248,0.1)', 'rgba(251,191,36,0.1)', 'rgba(167,139,250,0.1)', 'rgba(52,211,153,0.1)'];", "const PASO_BG = ['rgba(148, 163, 184, 0.1)', 'rgba(56, 189, 248, 0.1)', 'rgba(245, 158, 11, 0.1)', 'rgba(139, 92, 246, 0.1)', 'rgba(16, 185, 129, 0.1)'];", "const PASO_BG = ['rgba(148,163,184,0.1)', 'rgba(56,189,248,0.1)', 'rgba(223,192,123,0.1)', 'rgba(143,131,248,0.1)', 'rgba(52,211,153,0.1)'];", "const PASO_BG = ['rgba(148,163,184,0.1)', 'rgba(56,189,248,0.1)', 'rgba(20,184,166,0.1)', 'rgba(59,130,246,0.1)', 'rgba(52,211,153,0.1)'];", ] for pat in color_patterns: if pat in raw_clean: raw_clean = raw_clean.replace(pat, new_colors) print(f" ✅ PASO_COLORS reemplazado") break else: # Verificar si ya está la versión nueva if new_colors in raw_clean: print(f" ✅ PASO_COLORS ya está actualizado") else: print(f" âš ï¸ PASO_COLORS no encontrado con ningún patrón") for pat in bg_patterns: if pat in raw_clean: raw_clean = raw_clean.replace(pat, new_bg) print(f" ✅ PASO_BG reemplazado") break else: if new_bg in raw_clean: print(f" ✅ PASO_BG ya está actualizado") else: print(f" âš ï¸ PASO_BG no encontrado con ningún patrón") lines = raw_clean.split("\n") # ── PASO 3: Re-localizar marcadores ──────────────────────────────────── print("\n[3] Localizando marcadores clave...") l_emitir = find_line(lines, "const [emitirSol, setEmitirSol] = React.useState(null);") l_calc = find_line(lines, "const calcPaso = (item) => {") l_modal_ver = find_line(lines, "Modal VER con pesta") if l_modal_ver == -1: l_modal_ver = find_line(lines, "{/* Modal VER */}") l_modal_edi = find_line(lines, "{/* Modal EDITAR */}") print(f" emitirSol → lÃnea {l_emitir+1}") print(f" calcPaso → lÃnea {l_calc+1}") print(f" Modal VER → lÃnea {l_modal_ver+1}") print(f" Modal EDITAR → lÃnea {l_modal_edi+1}") if -1 in [l_emitir, l_calc, l_modal_ver, l_modal_edi]: print("⌠No se encontraron todos los marcadores. Abortando.") return # ── PASO 4: Insertar estados nuevos si no existen ───────────────────── print("\n[4] Verificando estados React...") states_exist = any("proposalData" in lines[i] for i in range(l_emitir, min(len(lines), l_emitir+15))) if not states_exist: new_states = [ " const [activeTab, setActiveTab] = React.useState(1);", " const [proposalData, setProposalData] = React.useState(null);", " const [loadingProp, setLoadingProp] = React.useState(false);", ] for j, s in enumerate(new_states): lines.insert(l_emitir + 1 + j, s) print(f" ✅ Estados insertados después de lÃnea {l_emitir+1}") l_calc += 3 l_modal_ver += 3 l_modal_edi += 3 else: print(f" ✅ Estados ya existen, saltando.") # ── PASO 5: Insertar useEffect + cleanText + abrirWizardEdicion ──────── print("\n[5] Verificando funciones de apoyo...") joined = "\n".join(lines) if "abrirWizardEdicion" not in joined: new_fns = """ React.useEffect(function() { if (solSel) { setActiveTab(calcPaso(solSel)); setProposalData(null); if (solSel.propuesta_id) { setLoadingProp(true); api('/propuestas/' + solSel.propuesta_id) .then(function(res) { setProposalData(res.data || res); }) .catch(function(err) { console.error('propuesta err:', err); }) .finally(function() { setLoadingProp(false); }); } } }, [solSel]); var cleanText = function(str) { if (!str) return ''; return String(str) .replace(/\u00e2\u0080\u0093/g, '-') .replace(/\u00e2\u0086\u0092/g, '\u2192') .replace(/\u00e2\u009c\u0093/g, '\u2713') .replace(/\u00c3\u0093/g, '\u00d3') .replace(/\u00c2\u00b7/g, '\u00b7') .replace(/\u00e2\u009c\u008e/g, '\u270e') .replace(/\u00c3\u009a/g, '\u00da') .replace(/\u00c3\u00ba/g, '\u00fa') .replace(/\u00c3\u00b3/g, '\u00f3') .replace(/\u00c3\u00a1/g, '\u00e1') .replace(/\u00c3\u00a9/g, '\u00e9') .replace(/\u00c3\u00ad/g, '\u00ed') .replace(/\u00c3\u00b1/g, '\u00f1'); }; const abrirWizardEdicion = function(item) { setEmitirLoading(true); var propId = item.propuesta_id; function doOpen(prefill) { localStorage.setItem('woll_prefill_cotizacion', JSON.stringify(prefill)); localStorage.setItem('woll_open_wizard_prefill', 'true'); toast('Abriendo wizard de cotizaciones...'); setSolSel(null); setEmitirLoading(false); if (typeof setPage === 'function') setPage('cotizaciones'); } function buildFallback() { var pNames = (item.nombres_pasajeros || item.pasajero_nombre || '').split(/,|\n/).map(function(n){ return n.trim(); }).filter(Boolean); var dateOnly = function(dt) { return String(dt||'').split('T')[0]; }; doOpen({ cliente_id: item.cliente_id || '', b2b_solicitud_id: item.id, tipo_vuelo: item.tipo_vuelo || 'solo_ida', fecha_viaje_inicio: dateOnly(item.fecha_ida), fecha_viaje_fin: dateOnly(item.fecha_regreso), num_adultos: Math.max(1, pNames.length), num_ninos: 0, num_infantes: 0, notas_cliente: item.detalles || '', segs: [], viajeros: pNames.map(function(fn){ var pts = fn.split(/\s+/); var half = Math.ceil(pts.length/2); return { nombres: pts.slice(0,half).join(' '), apellidos: pts.slice(half).join(' '), categoria:'adulto', tipo_documento:'pasaporte', nacionalidad:'CO' }; }) }); } if (!propId) { buildFallback(); return; } api('/propuestas/' + propId).then(function(res) { var proposal = res.data || res; var aprobDetails = null; try { aprobDetails = typeof proposal.aprobacion_detalles === 'string' ? JSON.parse(proposal.aprobacion_detalles) : proposal.aprobacion_detalles; } catch(e){} var idxAprob = aprobDetails ? (aprobDetails.opcion_index - 1) : 0; var opcionElegida = proposal.opciones && (proposal.opciones[idxAprob] || proposal.opciones[0]); if (!opcionElegida || !opcionElegida.detalles_json) { buildFallback(); return; } var details = {}; try { details = typeof opcionElegida.detalles_json === 'string' ? JSON.parse(opcionElegida.detalles_json) : opcionElegida.detalles_json; } catch(e){} var tramos = details.tramos || []; var pNames = (item.nombres_pasajeros || item.pasajero_nombre || '').split(/,|\n/).map(function(n){ return n.trim(); }).filter(Boolean); var dateOnly = function(dt) { return String(dt||'').split('T')[0]; }; var fmtT = function(t) { if(!t) return '00:00'; var p=String(t).split(':'); return p[0].trim().padStart(2,'0')+':'+(p[1]||'00').trim().substring(0,2).padStart(2,'0'); }; var precioTotal = aprobDetails ? (aprobDetails.precio || 0) : 0; var esBodega = aprobDetails && aprobDetails.equipaje === 'Bodega'; var esEjecutiva = item.detalles && item.detalles.toLowerCase().includes('ejecutiva'); var mapped = tramos.map(function(t, idx) { var fechaBase = idx === 0 ? dateOnly(item.fecha_ida) : dateOnly(item.fecha_regreso || item.fecha_ida); return { orden: idx+1, origen_iata: t.from || '', destino_iata: t.to || '', origen_nombre: t.fromCity || '', destino_nombre: t.toCity || '', aerolinea_codigo: t.airline || '', numero_vuelo: t.flightNum || '', fecha_salida: fechaBase + 'T' + fmtT(t.dep), fecha_llegada: fechaBase + 'T' + fmtT(t.arr), clase_cabina: esEjecutiva ? 'Business' : 'Economy', precio_adulto: Math.round(precioTotal / Math.max(tramos.length,1)), precio_nino: 0, precio_infante: 0, equipaje_mano_kg: 10, equipaje_bodega_kg: esBodega ? 23 : 0, equipaje_bodega_piezas: esBodega ? 1 : 0, tiene_escala: false, escalas_count: 0 }; }); var tipoVuelo = item.tipo_vuelo || (tramos.length >= 2 ? 'ida_vuelta' : 'solo_ida'); if (tipoVuelo === 'ida_vuelta' && mapped.length === 1 && item.fecha_regreso) { var out = mapped[0]; mapped.push({ orden:2, origen_iata:out.destino_iata, destino_iata:out.origen_iata, origen_nombre:out.destino_nombre, destino_nombre:out.origen_nombre, aerolinea_codigo:out.aerolinea_codigo, numero_vuelo:'', fecha_salida: dateOnly(item.fecha_regreso)+'T00:00', fecha_llegada: dateOnly(item.fecha_regreso)+'T00:00', clase_cabina: out.clase_cabina, precio_adulto: out.precio_adulto, precio_nino:0, precio_infante:0, equipaje_mano_kg:10, equipaje_bodega_kg:out.equipaje_bodega_kg, equipaje_bodega_piezas:out.equipaje_bodega_piezas, tiene_escala:false, escalas_count:0 }); } doOpen({ cliente_id: item.cliente_id || proposal.cliente_id || '', b2b_solicitud_id: item.id, tipo_vuelo: tipoVuelo, fecha_viaje_inicio: dateOnly(item.fecha_ida), fecha_viaje_fin: dateOnly(item.fecha_regreso), num_adultos: Math.max(1, pNames.length), num_ninos: 0, num_infantes: 0, notas_cliente: item.detalles || '', segs: mapped, viajeros: pNames.map(function(fn){ var pts = fn.split(/\s+/); var half = Math.ceil(pts.length/2); return { nombres: pts.slice(0,half).join(' '), apellidos: pts.slice(half).join(' '), categoria:'adulto', tipo_documento:'pasaporte', nacionalidad:'CO' }; }) }); }).catch(function(e) { console.error(e); buildFallback(); }); }; """ fn_lines = new_fns.split("\n") for j, fl in enumerate(fn_lines): lines.insert(l_calc + j, fl) offset = len(fn_lines) l_modal_ver += offset l_modal_edi += offset print(f" ✅ Funciones insertadas ({offset} lÃneas)") else: print(f" ✅ abrirWizardEdicion ya existe, saltando inserción.") # Pero si cleanText no existe, lo insertamos solo if "cleanText =" not in "\n".join(lines): clean_block = """ var cleanText = function(str) { if (!str) return ''; return String(str) .replace(/\u00e2\u0080\u0093/g, '-') .replace(/\u00e2\u0086\u0092/g, '\u2192') .replace(/\u00e2\u009c\u0093/g, '\u2713') .replace(/\u00c3\u0093/g, '\u00d3') .replace(/\u00c2\u00b7/g, '\u00b7') .replace(/\u00c3\u009a/g, '\u00da') .replace(/\u00c3\u00ba/g, '\u00fa') .replace(/\u00c3\u00b3/g, '\u00f3') .replace(/\u00c3\u00a1/g, '\u00e1') .replace(/\u00c3\u00a9/g, '\u00e9') .replace(/\u00c3\u00ad/g, '\u00ed') .replace(/\u00c3\u00b1/g, '\u00f1'); }; """ cl_lines = clean_block.split("\n") # Insertar justo antes de abrirWizardEdicion l_abrir = find_line(lines, "const abrirWizardEdicion = function") if l_abrir != -1: for j, cl in enumerate(cl_lines): lines.insert(l_abrir + j, cl) l_modal_ver += len(cl_lines) l_modal_edi += len(cl_lines) print(f" ✅ cleanText insertado antes de abrirWizardEdicion (lÃnea {l_abrir+1})") # ── PASO 6: Reemplazar Modal VER ─────────────────────────────────────── print(f"\n[6] Reemplazando Modal VER (lÃneas {l_modal_ver+1} a {l_modal_edi})...") new_modal = """ {/* Modal VER con pestañas */} {solSel && !editSol && (
No hay propuesta vinculada a\u00fan.
Lista para formalizar la venta del boleto.