// 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 ? `
` : `
`}
`;
}
return `
`;
}
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 += ``;
}
html += ``;
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 `
`;
}