const inputs = { avgPrice: document.getElementById('avg-price'), cvrLeadToApo: document.getElementById('cvr-lead-to-apo'), cvrApoToDeal: document.getElementById('cvr-apo-to-deal'), cvrDealToContract: document.getElementById('cvr-deal-to-contract'), annualGoal: document.getElementById('annual-goal'), salesMembers: document.getElementById('sales-members'), dealsPerMember: document.getElementById('deals-per-member'), }; const outputs = { requiredDeals: document.getElementById('required-deals'), teamCapacity: document.getElementById('team-capacity'), capacityResult: document.getElementById('capacity-result'), funnelContainer: document.getElementById('funnel-container'), monthlyLeadTotal: document.getElementById('monthly-lead-total'), alertContainer: document.getElementById('alert-container'), funnelWrapper: document.getElementById('funnel-wrapper'), }; const leadPlanContainer = document.getElementById('lead-plan-container'); const addLeadSourceBtn = document.getElementById('add-lead-source'); const calcModeTopDownBtn = document.getElementById('calc-mode-top-down'); const calcModeBottomUpBtn = document.getElementById('calc-mode-bottom-up'); let calculationMode = 'bottom-up'; let leadSources = [ { type: 'web', name: 'SEO (自然検索)', impressions: 50000, ctr: 1.0, cvr: 2.0, leads: 0 }, { type: 'web', name: 'リスティング広告', impressions: 20000, ctr: 2.0, cvr: 5.0, leads: 0 }, { type: 'direct', name: '展示会', impressions: 0, ctr: 0, cvr: 0, leads: 30 }, ]; const formatNumber = (num) => { if (isNaN(num) || !isFinite(num)) return '0'; const rounded = Math.round(num * 10) / 10; return rounded.toLocaleString(); }; const createFunnelStep = (title, value, unit, iconHtml, isMonthly = false) => { return `
${iconHtml}

${isMonthly ? '月次' : '年間'} ${title}

${formatNumber(value)} ${unit}

`; } function renderLeadPlan() { leadPlanContainer.innerHTML = ''; leadSources.forEach((source, index) => { const isWeb = source.type === 'web'; const calculatedLeads = isWeb ? (source.impressions || 0) * ((source.ctr || 0) / 100) * ((source.cvr || 0) / 100) : (source.leads || 0); const row = document.createElement('div'); row.className = 'plan-row'; row.dataset.index = index; row.innerHTML = `
${isWeb ? `
%
%

${formatNumber(calculatedLeads)}

` : `
`}
`; leadPlanContainer.appendChild(row); }); addLeadPlanEventListeners(); calculateAndSync(); } function addLeadPlanEventListeners() { leadPlanContainer.addEventListener('input', (e) => { if (e.target.matches('input')) { const index = e.target.dataset.index; if (index === undefined) return; const classList = e.target.className.split(' '); const leadSourceClass = classList.find(cls => cls.startsWith('lead-source-')); if (leadSourceClass) { const property = leadSourceClass.replace('lead-source-', ''); const source = leadSources[index]; source[property] = e.target.type === 'number' ? parseFloat(e.target.value) || 0 : e.target.value; if (source.type === 'web') { const rowElement = e.target.closest('.plan-row'); const predictionEl = rowElement.querySelector('.lead-prediction-value'); if (predictionEl) { const calculatedLeads = (source.impressions || 0) * ((source.ctr || 0) / 100) * ((source.cvr || 0) / 100); predictionEl.textContent = formatNumber(calculatedLeads); } } clearTimeout(window.mainUpdateTimeout); window.mainUpdateTimeout = setTimeout(calculateAndSync, 300); } } }); leadPlanContainer.addEventListener('click', (e) => { const target = e.target.closest('button'); if (!target) return; const index = target.dataset.index; if (index === undefined) return; if (target.matches('.remove-btn')) { leadSources.splice(index, 1); renderLeadPlan(); } if (target.matches('.plan-type-toggle')) { const newType = target.dataset.type; const currentSource = leadSources[index]; if (currentSource.type !== newType) { currentSource.type = newType; renderLeadPlan(); } } }); } addLeadSourceBtn.addEventListener('click', () => { leadSources.push({ type: 'direct', name: '新規施策', impressions: 0, ctr: 0, cvr: 0, leads: 10 }); renderLeadPlan(); }); function calculateAndSync() { let monthlyTotalLeads = 0; leadSources.forEach(source => { if (source.type === 'web') { monthlyTotalLeads += (source.impressions || 0) * ((source.ctr || 0) / 100) * ((source.cvr || 0) / 100); } else { monthlyTotalLeads += (source.leads || 0); } }); outputs.monthlyLeadTotal.textContent = formatNumber(monthlyTotalLeads); calculateFunnel(monthlyTotalLeads); } const calculateFunnel = (monthlyLeadsFromPlan) => { const avgPrice = parseFloat(inputs.avgPrice.value) || 0; const cvrLeadToApo = parseFloat(inputs.cvrLeadToApo.value) / 100 || 0; const cvrApoToDeal = parseFloat(inputs.cvrApoToDeal.value) / 100 || 0; const cvrDealToContract = parseFloat(inputs.cvrDealToContract.value) / 100 || 0; const salesMembers = parseInt(inputs.salesMembers.value) || 0; const dealsPerMember = parseInt(inputs.dealsPerMember.value) || 0; let annualGoal, annualLeads, annualApos, annualDeals, annualContracts; if (calculationMode === 'bottom-up') { annualLeads = monthlyLeadsFromPlan * 12; annualApos = annualLeads * cvrLeadToApo; annualDeals = annualApos * cvrApoToDeal; annualContracts = annualDeals * cvrDealToContract; annualGoal = annualContracts * avgPrice; if (document.activeElement !== inputs.annualGoal) { inputs.annualGoal.value = Math.round(annualGoal); } } else { annualGoal = parseFloat(inputs.annualGoal.value) || 0; annualContracts = avgPrice > 0 ? annualGoal / avgPrice : 0; annualDeals = cvrDealToContract > 0 ? annualContracts / cvrDealToContract : 0; annualApos = cvrApoToDeal > 0 ? annualDeals / cvrApoToDeal : 0; annualLeads = cvrLeadToApo > 0 ? annualApos / cvrApoToDeal : 0; const requiredMonthlyLeads = annualLeads / 12; outputs.monthlyLeadTotal.innerHTML = formatNumber(monthlyLeadsFromPlan) + ' (目標: ' + formatNumber(requiredMonthlyLeads) + ')'; } const monthlyContracts = (annualContracts || 0) / 12; if (monthlyContracts < 1 && calculationMode === 'bottom-up') { outputs.funnelWrapper.classList.add('hidden'); outputs.alertContainer.classList.remove('hidden'); outputs.alertContainer.innerHTML = `
計画の見直しが必要です。
現在のプランでは、月間1件以上の契約を見込めません。(予測: ${formatNumber(monthlyContracts)}件/月)
リード獲得数や各転換率を改善してください。
`; } else { outputs.funnelWrapper.classList.remove('hidden'); outputs.alertContainer.classList.add('hidden'); } const monthlyDeals = (annualDeals || 0) / 12; const teamCapacity = salesMembers * dealsPerMember; outputs.requiredDeals.textContent = `${formatNumber(monthlyDeals)} 件`; outputs.teamCapacity.textContent = `${formatNumber(teamCapacity)} 件`; if (teamCapacity >= monthlyDeals) { outputs.capacityResult.className = 'bg-green-100 text-green-800'; outputs.capacityResult.textContent = '計画は現実的です。'; } else { const shortage = monthlyDeals - teamCapacity; outputs.capacityResult.className = 'bg-yellow-100 text-yellow-800'; outputs.capacityResult.textContent = `キャパシティ不足 (月あたり約${formatNumber(shortage)}件の商談)`; } const funnelIcons = { leads: ``, apos: ``, deals: ``, contracts: `` }; const funnelData = [ { title: 'リード獲得数', value: annualLeads, unit: '件', icon: `
${funnelIcons.leads}
` }, { title: 'アポイント数', value: annualApos, unit: '件', icon: `
${funnelIcons.apos}
` }, { title: '商談数', value: annualDeals, unit: '件', icon: `
${funnelIcons.deals}
` }, { title: '契約件数', value: annualContracts, unit: '件', icon: `
${funnelIcons.contracts}
` }, ]; let funnelHTML = '

年間目標

'; funnelData.forEach((step, index) => { funnelHTML += createFunnelStep(step.title, step.value, step.unit, step.icon); if (index < funnelData.length - 1) funnelHTML += '
'; }); funnelHTML += '

月次目標 (平均)

'; funnelData.forEach((step, index) => { funnelHTML += createFunnelStep(step.title, step.value / 12, step.unit, step.icon, true); if (index < funnelData.length - 1) funnelHTML += '
'; }); outputs.funnelContainer.innerHTML = funnelHTML; }; function setCalcMode(mode) { calculationMode = mode; if (mode === 'bottom-up') { calcModeBottomUpBtn.classList.add('active'); calcModeTopDownBtn.classList.remove('active'); inputs.annualGoal.readOnly = true; } else { calcModeTopDownBtn.classList.add('active'); calcModeBottomUpBtn.classList.remove('active'); inputs.annualGoal.readOnly = false; } calculateAndSync(); } Object.values(inputs).forEach(input => { input.addEventListener('input', () => { clearTimeout(window.mainUpdateTimeout); window.mainUpdateTimeout = setTimeout(calculateAndSync, 300); }); }); calcModeTopDownBtn.addEventListener('click', () => setCalcMode('top-down')); calcModeBottomUpBtn.addEventListener('click', () => setCalcMode('bottom-up')); setCalcMode('bottom-up'); renderLeadPlan();