Number of Users/Employees Directly Affected must be at least 1.
";
document.getElementById('btuc-download-pdf').disabled = true;
return;
}
const allCosts = [];
let subtotalDirectCosts = 0;
document.querySelectorAll('#costs .btuc-cost-category-section').forEach(section => {
if (section.style.display === 'block' || section.classList.contains('btuc-visible')) { // Only process visible sections
// One-time costs
section.querySelectorAll('.btuc-cost-item').forEach(input => {
const cost = getNum(input.value);
if (cost > 0) {
allCosts.push({
category: input.dataset.category,
name: input.dataset.itemName,
amount: cost,
type: 'One-Time'
});
subtotalDirectCosts += cost;
}
});
// Recurring costs (calculate first year)
section.querySelectorAll('.btuc-cost-item-recurring-val').forEach(input => {
const costPerCycle = getNum(input.value);
const cycleSelect = section.querySelector(`.btuc-cost-item-recurring-cycle[data-target-id="${input.id}"]`) ||
section.querySelector(`#${input.id.replace('-cost', '-cycle')}`); // Find corresponding cycle select
const cycle = cycleSelect ? cycleSelect.value : 'Annually';
if (costPerCycle > 0) {
let firstYearCost = costPerCycle;
if (cycle === 'Monthly') firstYearCost *= 12;
// If Annually, it's already the first year cost.
allCosts.push({
category: input.dataset.category,
name: input.dataset.itemName + ` (${currency}${costPerCycle.toFixed(2)}/${cycle})`,
amount: firstYearCost,
type: 'First Year Recurring'
});
subtotalDirectCosts += firstYearCost;
}
});
}
});
document.querySelectorAll('.btuc-custom-cost-item').forEach(item => {
const name = item.querySelector('.btuc-custom-name').value || 'Custom Cost';
const amount = getNum(item.querySelector('.btuc-custom-amount').value);
const type = item.querySelector('.btuc-custom-type').value; // OneTime or RecurringFirstYear
if (amount > 0) {
allCosts.push({ category: 'Custom Upgrade Costs', name, amount, type });
subtotalDirectCosts += amount;
}
});
const contingencyType = CONFIG_INPUTS.contingencyType.value;
const contingencyValue = getNum(CONFIG_INPUTS.contingencyValue.value);
let contingencyAmount = 0;
if (contingencyType === 'percentage') {
contingencyAmount = subtotalDirectCosts * (contingencyValue / 100);
} else { // fixed
contingencyAmount = contingencyValue;
}
const grandTotal = subtotalDirectCosts + contingencyAmount;
html += `
Upgrade Project Budget Summary:
`;
html += `
Project Name: ${CONFIG_INPUTS.projectName.value || 'N/A'}
`;
const selectedScopesText = Array.from(CONFIG_INPUTS.scopeCheckboxes).filter(cb=>cb.checked).map(cb=>cb.labels[0].textContent.trim()).join(', ');
html += `
Scope: ${selectedScopesText || 'N/A'}
`;
html += `
Users Affected: ${numUsers}
`;
html += `
`;
html += '
Estimated Costs Breakdown:
';
html += '
| Category | Item | Type | Estimated Cost |
';
if (allCosts.length === 0 && contingencyAmount === 0) {
html += '| No costs entered. |
';
} else {
const groupedByCategory = allCosts.reduce((acc, item) => {
(acc[item.category] = acc[item.category] || []).push(item);
return acc;
}, {});
for (const category in groupedByCategory) {
html += ``;
groupedByCategory[category].forEach(item => {
html += `
| ${item.name} |
${item.type} |
${currency}${item.amount.toFixed(2)} |
`;
});
}
html += `| Subtotal of Direct Costs: | ${currency}${subtotalDirectCosts.toFixed(2)} |
`;
html += `| Contingency (${contingencyType === 'percentage' ? contingencyValue + '%' : 'Fixed'}): | ${currency}${contingencyAmount.toFixed(2)} |
`;
}
html += `| GRAND TOTAL ESTIMATED UPGRADE COST: | ${currency}${grandTotal.toFixed(2)} |
`;
html += '
';
html += `
Estimated Cost Per Affected User: ${currency}${(numUsers > 0 ? grandTotal / numUsers : 0).toFixed(2)}
`;
summaryOutput.innerHTML = html;
document.getElementById('btuc-download-pdf').disabled = allCosts.length === 0 && contingencyAmount === 0;
}
// PDF Download
document.getElementById('btuc-download-pdf').addEventListener('click', function () {
if (typeof jspdf === 'undefined' || typeof jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library is not loaded.'); return;
}
const { jsPDF } = jspdf;
const doc = new jsPDF('p');
const currency = CONFIG_INPUTS.currencySymbol.value || '$';
const numUsers = getNum(CONFIG_INPUTS.numUsers.value);
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--btuc-primary-color').trim();
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--btuc-secondary-color').trim();
const textColor = getComputedStyle(document.documentElement).getPropertyValue('--btuc-text-color').trim();
const buttonTextColor = getComputedStyle(document.documentElement).getPropertyValue('--btuc-button-text-color').trim();
doc.setFontSize(18);
doc.setTextColor(primaryColor);
doc.text('Business Technology Upgrade Cost Estimate', doc.internal.pageSize.getWidth() / 2, 20, { align: 'center' });
doc.setFontSize(12);
doc.setTextColor(textColor);
let lastY = 30;
doc.text(`Business Name: ${CONFIG_INPUTS.businessName.value || 'N/A'}`, 14, lastY); lastY += 7;
doc.text(`Project Name: ${CONFIG_INPUTS.projectName.value || 'N/A'}`, 14, lastY); lastY += 7;
const selectedScopesText = Array.from(CONFIG_INPUTS.scopeCheckboxes).filter(cb=>cb.checked).map(cb=>cb.labels[0].textContent.trim()).join(', ');
doc.text(`Scope: ${selectedScopesText || 'N/A'}`, 14, lastY); lastY += 7;
doc.text(`Users Affected: ${numUsers}`, 14, lastY); lastY += 7;
doc.text(`Project Duration: ${CONFIG_INPUTS.projectDuration.value || 'N/A'}`, 14, lastY); lastY += 7;
doc.text(`Currency: ${currency}`, 14, lastY); lastY += 10;
const allCostsPdf = [];
let subtotalDirectCostsPdf = 0;
// Re-collect for PDF
document.querySelectorAll('#costs .btuc-cost-category-section').forEach(section => {
if (section.style.display === 'block' || section.classList.contains('btuc-visible')) {
section.querySelectorAll('.btuc-cost-item').forEach(input => {
const cost = getNum(input.value);
if (cost > 0) {
allCostsPdf.push({ category: input.dataset.category, name: input.dataset.itemName, type: 'One-Time', amount: cost });
subtotalDirectCostsPdf += cost;
}
});
section.querySelectorAll('.btuc-cost-item-recurring-val').forEach(input => {
const costPerCycle = getNum(input.value);
const cycleSelect = section.querySelector(`.btuc-cost-item-recurring-cycle[data-target-id="${input.id}"]`) || section.querySelector(`#${input.id.replace('-cost', '-cycle')}`);
const cycle = cycleSelect ? cycleSelect.value : 'Annually';
if (costPerCycle > 0) {
let firstYearCost = (cycle === 'Monthly') ? costPerCycle * 12 : costPerCycle;
allCostsPdf.push({ category: input.dataset.category, name: input.dataset.itemName + ` (${currency}${costPerCycle.toFixed(2)}/${cycle})`, type: '1st Year Recurring', amount: firstYearCost });
subtotalDirectCostsPdf += firstYearCost;
}
});
}
});
document.querySelectorAll('.btuc-custom-cost-item').forEach(item => {
const name = item.querySelector('.btuc-custom-name').value || 'Custom Cost';
const amount = getNum(item.querySelector('.btuc-custom-amount').value);
const type = item.querySelector('.btuc-custom-type').value;
if (amount > 0) {
allCostsPdf.push({ category: 'Custom Upgrade Costs', name, type, amount });
subtotalDirectCostsPdf += amount;
}
});
const contingencyTypePdf = CONFIG_INPUTS.contingencyType.value;
const contingencyValuePdf = getNum(CONFIG_INPUTS.contingencyValue.value);
let contingencyAmountPdf = (contingencyTypePdf === 'percentage') ? subtotalDirectCostsPdf * (contingencyValuePdf / 100) : contingencyValuePdf;
const grandTotalPdf = subtotalDirectCostsPdf + contingencyAmountPdf;
const tableBody = [];
const groupedByCategoryPdf = allCostsPdf.reduce((acc, item) => {
(acc[item.category] = acc[item.category] || []).push(item);
return acc;
}, {});
for (const category in groupedByCategoryPdf) {
tableBody.push([{ content: category, colSpan: 3, styles: { fontStyle: 'bold', fillColor: '#f0f0f0' } }]);
groupedByCategoryPdf[category].forEach(item => {
tableBody.push([item.name, item.type, `${currency}${item.amount.toFixed(2)}`]);
});
}
tableBody.push([
{ content: 'Subtotal of Direct Costs:', colSpan: 2, styles: { halign: 'right', fontStyle: 'bold'} },
{ content: `${currency}${subtotalDirectCostsPdf.toFixed(2)}`, styles: { halign: 'right', fontStyle: 'bold' } }
]);
tableBody.push([
{ content: `Contingency (${contingencyTypePdf === 'percentage' ? contingencyValuePdf + '%' : 'Fixed'}):`, colSpan: 2, styles: { halign: 'right', fontStyle: 'bold'} },
{ content: `${currency}${contingencyAmountPdf.toFixed(2)}`, styles: { halign: 'right', fontStyle: 'bold' } }
]);
tableBody.push([
{ content: 'GRAND TOTAL:', colSpan: 2, styles: { halign: 'right', fontStyle: 'bold', fillColor: primaryColor, textColor: buttonTextColor } },
{ content: `${currency}${grandTotalPdf.toFixed(2)}`, styles: { halign: 'right', fontStyle: 'bold', fillColor: primaryColor, textColor: buttonTextColor } }
]);
doc.autoTable({
startY: lastY,
head: [['Item/Category', 'Type', 'Estimated Cost']],
body: tableBody,
theme: 'grid',
headStyles: { fillColor: secondaryColor, textColor: buttonTextColor, fontSize: 9 },
styles: { fontSize: 8, cellPadding: 2 },
columnStyles: { 2: { halign: 'right' } },
didDrawCell: (data) => {
if (data.cell.raw && data.cell.raw.colSpan === 3) { /* Category Header Row */ }
}
});
lastY = doc.lastAutoTable.finalY + 10;
doc.setFontSize(10);
doc.setTextColor(textColor);
doc.text(`Estimated Cost Per Affected User: ${currency}${(numUsers > 0 ? grandTotalPdf / numUsers : 0).toFixed(2)}`, 14, lastY);
doc.save(`Tech_Upgrade_Budget_${CONFIG_INPUTS.projectName.value.replace(/\s+/g, '_') || 'Report'}.pdf`);
});
// Initial setup
checkEmptyState(COST_CONTAINERS.customCosts, 'btuc-no-items-text');
toggleCostSections(); // Initial call to set visibility based on default scope
toggleContingencyInputType(); // Set initial contingency input style
showTab(0);
updateAllCurrencyPrefixes();
});