// PATCH v2: añadir campañas email manuales // (sobreescribe renderPlanRow y añade funciones de campañas) // ============================================================ // Estado adicional para el formulario "Nueva campaña" if(state.plan && !state.plan.addingCampaign){ state.plan.addingCampaign = new Set(); } function renderPlanRow(a){ const proj = PLAN_PROJECTS[a.project_code] || { label: a.project_code || '—', color: '#9ca3af' }; const ch = PLAN_CHANNELS[a.channel] || { label: a.channel, icon:'•', color:'#f1f5f9', fg:'#475569' }; const linked = Array.isArray(a.linked_automations) ? a.linked_automations : []; const linkedCampaigns = Array.isArray(a.linked_manual_campaigns) ? a.linked_manual_campaigns : []; const isOpen = state.plan.expanded.has(a.id); const isAddingCamp = state.plan.addingCampaign && state.plan.addingCampaign.has(a.id); const reach = +a.total_reach || 0; const totalLinks = linked.length + linkedCampaigns.length; const dateStr = `${String(a.day).padStart(2,'0')} ${PLAN_MONTHS[a.month]}`; let expandedHtml = ''; if(isOpen){ expandedHtml = `
${a.note ? `
Nota: ${esc(a.note)}
` : ''}
Automations vinculadas (${linked.length})
${linked.length ? `
${linked.map(l => `
${esc(l.name)} ${(+l.completed||0).toLocaleString('es-ES')} ok ${l.pending ? `${l.pending} pending` : ''}
`).join('')}
` : '
Sin automations vinculadas.
'}
Campañas email vinculadas (${linkedCampaigns.length})
${linkedCampaigns.length ? `
${linkedCampaigns.map(c => `
${esc(c.name)}${c.sent_date ? ` · ${c.sent_date}` : ''} ${c.clientify_id ? ` ` : ''} ${(+c.recipients||0).toLocaleString('es-ES')} env. ${c.unique_opens ? `${(+c.unique_opens).toLocaleString('es-ES')} ab.` : ''} ${c.unique_clicks ? `${(+c.unique_clicks).toLocaleString('es-ES')} clk` : ''}
`).join('')}
` : '
Sin campañas email vinculadas.
'} ${isAddingCamp ? `
Nueva campaña email
` : ` `}
`; } return `
${dateStr} ${ch.icon} ${ch.label} ${proj.label} ${esc(a.title)} ${totalLinks ? `🔗 ${totalLinks} · ${reach.toLocaleString('es-ES')}` : ''} ${a.kpi ? `${esc(a.kpi)}` : ''} ${isOpen?'▾':'▸'}
${expandedHtml}
`; } function planShowAddCampaign(aid){ if(!state.plan.addingCampaign) state.plan.addingCampaign = new Set(); state.plan.addingCampaign.add(aid); if(!state.plan.expanded.has(aid)) state.plan.expanded.add(aid); drawPlan(); } function planHideAddCampaign(aid){ if(state.plan.addingCampaign) state.plan.addingCampaign.delete(aid); drawPlan(); } async function planSaveCampaign(aid){ const get = (k) => document.getElementById(`camp_${aid}_${k}`); const v = (k) => get(k) ? get(k).value : ''; const numv = (k) => { const x = +v(k); return isNaN(x) ? null : x; }; const name = v('name').trim(); if(!name){ toast('Falta el nombre','error'); return; } const action = state.plan.actions.find(x => x.id === aid); const campaign = { name, sent_date: v('date') || null, clientify_id: numv('clientify'), recipients: numv('recipients') || 0, opens: numv('opens') || 0, unique_opens: numv('unique_opens') || 0, clicks: numv('clicks') || 0, unique_clicks: numv('unique_clicks') || 0, unsubscribes: numv('unsubs') || 0, bounces: numv('bounces') || 0, note: v('note') || null, project_code: action ? action.project_code : null }; const { data: ins, error: e1 } = await sb .from('email_campaigns_manual') .insert(campaign) .select() .single(); if(e1){ toast('Error guardando campaña: '+e1.message,'error'); return; } const { error: e2 } = await sb.from('action_clientify_links').insert({ action_id: aid, manual_campaign_id: ins.id, link_type: 'manual' }); if(e2){ toast('Campaña creada pero error al vincular: '+e2.message,'error'); } if(state.plan.addingCampaign) state.plan.addingCampaign.delete(aid); await loadPlan(); drawPlan(); toast('Campaña creada y vinculada','success'); } async function planUnlinkCampaign(aid, manualId){ if(!confirm('¿Desvincular esta campaña? (la campaña no se borra, solo se desvincula de esta acción)')) return; const { error } = await sb.from('action_clientify_links') .delete() .eq('action_id', aid) .eq('manual_campaign_id', manualId); if(error){ toast('Error: '+error.message,'error'); return; } await loadPlan(); drawPlan(); toast('Desvinculada','success'); } // ===================================================================== // PATCH v3 — Vínculo de campañas Clientify a acciones de marketing // ===================================================================== async function planLoadClientifyCampaigns(){ if(state.plan.clientifyCampaigns) return; try { const { data, error } = await sb .from('clientify_campaigns') .select('id,name,subject,sent_date,sent,opens,clicks,opens_percent,clicks_percent,project_code_guess,from_name') .order('sent_date', { ascending: false, nullsFirst: false }); if(error) throw error; state.plan.clientifyCampaigns = data || []; console.log(`[plan] cargadas ${state.plan.clientifyCampaigns.length} campañas Clientify`); } catch(e){ console.error('[plan] error cargando campañas Clientify:', e); state.plan.clientifyCampaigns = []; } } const _origLoadPlan_v3 = window.loadPlan; if(typeof _origLoadPlan_v3 === 'function'){ window.loadPlan = async function(){ await _origLoadPlan_v3.apply(this, arguments); await planLoadClientifyCampaigns(); }; } function planClientifyOptions(action){ const list = state.plan.clientifyCampaigns || []; const linked = new Set((action.linked_clientify_campaigns||[]).map(l=>l.id)); const all = list.filter(c => !linked.has(c.id)); const sameProj = all.filter(c => c.project_code_guess === action.project_code); const other = all.filter(c => c.project_code_guess !== action.project_code); const opt = (c) => { const date = c.sent_date ? c.sent_date.slice(0,10) : 'sin fecha'; return ``; }; let html = ''; if(sameProj.length){ html += `${sameProj.map(opt).join('')}`; } html += `${other.map(opt).join('')}`; return html; } async function planLinkCampaign(aid){ const sel = document.getElementById(`plan_sel_camp_${aid}`); const cid = sel && sel.value; if(!cid){ toast('Elige una campaña','error'); return; } const { error } = await sb.from('action_clientify_links').insert({ action_id: aid, clientify_campaign_id: +cid, link_type: 'clientify' }); if(error){ toast('Error al vincular: '+error.message,'error'); return; } await loadPlan(); drawPlan(); toast('Campaña vinculada','success'); } async function planUnlinkCampaign(aid, cid){ if(!confirm('¿Desvincular esta campaña?')) return; const { error } = await sb.from('action_clientify_links') .delete() .eq('action_id', aid) .eq('clientify_campaign_id', cid); if(error){ toast('Error: '+error.message,'error'); return; } await loadPlan(); drawPlan(); toast('Desvinculada','success'); } function renderPlanRow(a){ const proj = PLAN_PROJECTS[a.project_code] || { label: a.project_code || '—', color: '#9ca3af' }; const ch = PLAN_CHANNELS[a.channel] || { label: a.channel, icon:'•', color:'#f1f5f9', fg:'#475569' }; const linked = Array.isArray(a.linked_automations) ? a.linked_automations : []; const linkedCamps = Array.isArray(a.linked_clientify_campaigns) ? a.linked_clientify_campaigns : []; const isOpen = state.plan.expanded.has(a.id); const reach = +a.total_reach || 0; const dateStr = `${String(a.day).padStart(2,'0')} ${PLAN_MONTHS[a.month]}`; let expandedHtml = ''; if(isOpen){ const campsHtml = linkedCamps.length ? `
${linkedCamps.map(c => { const opens = +c.opens || 0, clicks = +c.clicks || 0, sent = +c.sent || 0; const op = sent ? Math.round(opens*100/sent) : 0; const cp = sent ? Math.round(clicks*100/sent) : 0; const date = c.sent_date ? c.sent_date.slice(0,10) : ''; return `
📧 ${esc(c.name)}${date?` · ${date}`:''} ${sent.toLocaleString('es-ES')} ${opens.toLocaleString('es-ES')} · ${op}% ${clicks.toLocaleString('es-ES')} · ${cp}%
`; }).join('')}
` : '
Sin campañas Clientify vinculadas.
'; expandedHtml = `
${a.note ? `
Nota: ${esc(a.note)}
` : ''}
Automations vinculadas (${linked.length})
${linked.length ? `
${linked.map(l => `
${esc(l.name)} ${(+l.completed||0).toLocaleString('es-ES')} ok ${l.pending ? `${l.pending} pending` : ''}
`).join('')}
` : '
Sin automations vinculadas.
'}
Campañas Clientify vinculadas (${linkedCamps.length})
${campsHtml}
`; } const totalLinks = linked.length + linkedCamps.length; return `
${dateStr} ${ch.icon} ${ch.label} ${proj.label} ${esc(a.title)} ${totalLinks ? `🔗 ${totalLinks} · ${reach.toLocaleString('es-ES')}` : ''} ${a.kpi ? `${esc(a.kpi)}` : ''} ${isOpen?'▾':'▸'}
${expandedHtml}
`; }