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 = `
`;
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();