class EagleTouchSolarCalculator {
constructor() {
// Initialize calculator state
this.state = {
region: null,
municipality: null,
calculationType: null, // ‘appliance’ or ‘bill’
appliances: [],
customAppliances: [],
monthlyConsumption: null,
offsetPercentage: 100,
backupHours: 8,
backupPercentage: 50,
includeWindTurbine: false,
results: null,
selectedPackage: null,
customizedPackage: null,
};
// South African solar irradiance data by region (kWh/m²/day)
this.solarIrradianceData = {
‘Western Cape’: 5.8,
‘Eastern Cape’: 5.6,
‘Northern Cape’: 6.2,
‘North West’: 6.0,
‘Free State’: 5.9,
‘Gauteng’: 5.7,
‘Mpumalanga’: 5.5,
‘Limpopo’: 5.8,
‘KwaZulu-Natal’: 5.3
};
// Average Eskom and municipal tariffs (R/kWh)
this.electricityTariffs = {
‘Eskom Direct’: 3.25,
‘City of Cape Town’: 3.45,
‘City of Johannesburg’: 3.30,
‘eThekwini’: 3.20,
‘Tshwane’: 3.35,
‘Nelson Mandela Bay’: 3.28,
‘Ekurhuleni’: 3.32,
‘Buffalo City’: 3.22,
‘Mangaung’: 3.15,
‘Polokwane’: 3.18,
‘Default’: 3.25
};
// Municipalities by province
this.municipalitiesByProvince = {
‘Western Cape’: [‘City of Cape Town’, ‘Stellenbosch’, ‘Drakenstein’, ‘George’, ‘Other’],
‘Eastern Cape’: [‘Nelson Mandela Bay’, ‘Buffalo City’, ‘Makana’, ‘Kouga’, ‘Other’],
‘Northern Cape’: [‘Sol Plaatje’, ‘Nama Khoi’, ‘Emthanjeni’, ‘Gamagara’, ‘Other’],
‘North West’: [‘Rustenburg’, ‘Madibeng’, ‘Mahikeng’, ‘JB Marks’, ‘Other’],
‘Free State’: [‘Mangaung’, ‘Matjhabeng’, ‘Metsimaholo’, ‘Dihlabeng’, ‘Other’],
‘Gauteng’: [‘City of Johannesburg’, ‘Tshwane’, ‘Ekurhuleni’, ‘Emfuleni’, ‘Other’],
‘Mpumalanga’: [‘Mbombela’, ‘Emalahleni’, ‘Steve Tshwete’, ‘Govan Mbeki’, ‘Other’],
‘Limpopo’: [‘Polokwane’, ‘Makhado’, ‘Mogalakwena’, ‘Tzaneen’, ‘Other’],
‘KwaZulu-Natal’: [‘eThekwini’, ‘Msunduzi’, ‘Newcastle’, ‘uMhlathuze’, ‘Other’]
};
// Component pricing data
this.componentPricing = {
solarPanels: {
standard: {
pricePerWatt: 9.5, // Rands per Watt
brands: [‘Canadian Solar’, ‘JA Solar’, ‘Jinko Solar’]
},
premium: {
pricePerWatt: 12.5,
brands: [‘SunPower’, ‘LG’, ‘REC’]
},
bifacial: {
pricePerWatt: 14.0,
brands: [‘LONGi Solar’, ‘Trina Solar’, ‘JA Solar Bifacial’]
}
},
batteries: {
leadAcid: {
pricePerKwh: 2000, // Rands per kWh
brands: [‘Trojan’, ‘Rolls’, ‘First National Battery’],
cycleLife: 1500,
depthOfDischarge: 0.5
},
lithiumIon: {
pricePerKwh: 6000,
brands: [‘Tesla Powerwall’, ‘LG Chem RESU’, ‘BYD’, ‘Pylontech’],
cycleLife: 6000,
depthOfDischarge: 0.9
}
},
inverters: {
standard: {
pricePerKva: 3000, // Rands per kVA
brands: [‘Growatt’, ‘Goodwe’, ‘Solis’],
efficiency: 0.93
},
premium: {
pricePerKva: 5000,
brands: [‘SMA’, ‘Fronius’, ‘SolarEdge’],
efficiency: 0.96
},
hybrid: {
pricePerKva: 6000,
brands: [‘Victron’, ‘Sunsynk’, ‘Deye’],
efficiency: 0.95
}
},
windTurbines: {
small: {
price: 25000, // 1-2kW
brands: [‘Bergey’, ‘XZERES’, ‘Kestrel’],
output: 1.5 // kW
},
medium: {
price: 45000, // 3-5kW
brands: [‘Bergey’, ‘XZERES’, ‘Kestrel’],
output: 4 // kW
},
large: {
price: 85000, // 6-10kW
brands: [‘Bergey’, ‘XZERES’, ‘Kestrel’],
output: 8 // kW
}
},
installation: {
basicRate: 12000, // Base installation cost
perKwSolar: 1500, // Additional per kW of solar
perKwhBattery: 500 // Additional per kWh of battery
}
};
// Standard appliance library
this.applianceLibrary = {
lighting: [
{ id: ‘led_bulb’, name: ‘LED Light Bulb’, power: 10, avgHoursPerDay: 5, category: ‘lighting’ },
{ id: ‘tube_light’, name: ‘LED Tube Light’, power: 20, avgHoursPerDay: 8, category: ‘lighting’ },
{ id: ‘ceiling_fan’, name: ‘Ceiling Fan with Light’, power: 75, avgHoursPerDay: 8, category: ‘lighting’ }
],
kitchen: [
{ id: ‘refrigerator’, name: ‘Refrigerator’, power: 150, avgHoursPerDay: 24, category: ‘kitchen’ },
{ id: ‘freezer’, name: ‘Freezer’, power: 200, avgHoursPerDay: 24, category: ‘kitchen’ },
{ id: ‘microwave’, name: ‘Microwave Oven’, power: 1200, avgHoursPerDay: 0.5, category: ‘kitchen’ },
{ id: ‘electric_kettle’, name: ‘Electric Kettle’, power: 2000, avgHoursPerDay: 0.5, category: ‘kitchen’ },
{ id: ‘toaster’, name: ‘Toaster’, power: 1000, avgHoursPerDay: 0.2, category: ‘kitchen’ },
{ id: ‘coffee_maker’, name: ‘Coffee Maker’, power: 1000, avgHoursPerDay: 0.5, category: ‘kitchen’ },
{ id: ‘dishwasher’, name: ‘Dishwasher’, power: 1800, avgHoursPerDay: 1, category: ‘kitchen’ },
{ id: ‘electric_stove’, name: ‘Electric Stove (per plate)’, power: 2000, avgHoursPerDay: 1, category: ‘kitchen’ },
{ id: ‘electric_oven’, name: ‘Electric Oven’, power: 2500, avgHoursPerDay: 0.5, category: ‘kitchen’ }
],
entertainment: [
{ id: ‘tv_led_small’, name: ‘TV (LED 32″)’, power: 50, avgHoursPerDay: 4, category: ‘entertainment’ },
{ id: ‘tv_led_large’, name: ‘TV (LED 50″+)’, power: 100, avgHoursPerDay: 4, category: ‘entertainment’ },
{ id: ‘home_theater’, name: ‘Home Theater System’, power: 150, avgHoursPerDay: 3, category: ‘entertainment’ },
{ id: ‘gaming_console’, name: ‘Gaming Console’, power: 150, avgHoursPerDay: 2, category: ‘entertainment’ },
{ id: ‘dstv_decoder’, name: ‘DSTV Decoder’, power: 30, avgHoursPerDay: 6, category: ‘entertainment’ }
],
computing: [
{ id: ‘wifi_router’, name: ‘Wi-Fi Router’, power: 10, avgHoursPerDay: 24, category: ‘computing’ },
{ id: ‘desktop_computer’, name: ‘Desktop Computer’, power: 200, avgHoursPerDay: 4, category: ‘computing’ },
{ id: ‘laptop’, name: ‘Laptop’, power: 60, avgHoursPerDay: 4, category: ‘computing’ },
{ id: ‘printer’, name: ‘Printer’, power: 500, avgHoursPerDay: 0.2, category: ‘computing’ }
],
hvac: [
{ id: ‘fan’, name: ‘Standing Fan’, power: 70, avgHoursPerDay: 6, category: ‘hvac’ },
{ id: ‘aircon_small’, name: ‘Air Conditioner (1.5HP)’, power: 1200, avgHoursPerDay: 5, category: ‘hvac’ },
{ id: ‘aircon_medium’, name: ‘Air Conditioner (2.0HP)’, power: 1800, avgHoursPerDay: 5, category: ‘hvac’ },
{ id: ‘aircon_large’, name: ‘Air Conditioner (2.5HP)’, power: 2400, avgHoursPerDay: 5, category: ‘hvac’ },
{ id: ‘heater_small’, name: ‘Electric Heater (Small)’, power: 1000, avgHoursPerDay: 3, category: ‘hvac’ },
{ id: ‘heater_large’, name: ‘Electric Heater (Large)’, power: 2000, avgHoursPerDay: 3, category: ‘hvac’ }
],
water: [
{ id: ‘geyser’, name: ‘Electric Geyser’, power: 3000, avgHoursPerDay: 3, category: ‘water’ },
{ id: ‘pool_pump’, name: ‘Swimming Pool Pump’, power: 750, avgHoursPerDay: 6, category: ‘water’ },
{ id: ‘borehole_pump_small’, name: ‘Borehole Pump (Small)’, power: 750, avgHoursPerDay: 2, category: ‘water’ },
{ id: ‘borehole_pump_large’, name: ‘Borehole Pump (Large)’, power: 1500, avgHoursPerDay: 2, category: ‘water’ },
{ id: ‘water_feature’, name: ‘Water Feature Pump’, power: 100, avgHoursPerDay: 6, category: ‘water’ }
],
laundry: [
{ id: ‘washing_machine’, name: ‘Washing Machine’, power: 500, avgHoursPerDay: 1, category: ‘laundry’ },
{ id: ‘tumble_dryer’, name: ‘Tumble Dryer’, power: 2500, avgHoursPerDay: 1, category: ‘laundry’ },
{ id: ‘iron’, name: ‘Iron’, power: 1800, avgHoursPerDay: 0.5, category: ‘laundry’ }
],
other: [
{ id: ‘security_system’, name: ‘Security System’, power: 50, avgHoursPerDay: 24, category: ‘other’ },
{ id: ‘garage_door’, name: ‘Garage Door Opener’, power: 400, avgHoursPerDay: 0.1, category: ‘other’ },
{ id: ‘electric_fence’, name: ‘Electric Fence’, power: 100, avgHoursPerDay: 24, category: ‘other’ },
{ id: ‘sump_pump’, name: ‘Sump Pump’, power: 800, avgHoursPerDay: 0.5, category: ‘other’ }
]
};
// Package templates
this.packageTemplates = {
basic: {
name: “Basic Package: Essential Lighting”,
description: “Perfect for emergency lighting during load shedding”,
typicalAppliances: [“lighting”],
typicalDailyConsumption: 0.5, // kWh
solarSystemSize: 0.5, // kW
batteryCapacity: 1.5, // kWh
inverterSize: 0.8, // kVA
estimatedPriceRange: {
min: 15999,
max: 22999
},
suitableFor: “Homes needing basic lighting during outages”
},
essential: {
name: “Essential Package: Lights & Refrigeration”,
description: “Keep your essentials running during outages”,
typicalAppliances: [“lighting”, “refrigeration”],
typicalDailyConsumption: 2, // kWh
solarSystemSize: 1.5, // kW
batteryCapacity: 3.5, // kWh
inverterSize: 1.5, // kVA
estimatedPriceRange: {
min: 29999,
max: 42999
},
suitableFor: “Homes needing to power lights and preserve food”
},
standard: {
name: “Standard Package: Everyday Essentials”,
description: “Maintain normal household function during outages”,
typicalAppliances: [“lighting”, “refrigeration”, “entertainment”, “computing”],
typicalDailyConsumption: 5, // kWh
solarSystemSize: 3, // kW
batteryCapacity: 7, // kWh
inverterSize: 3, // kVA
estimatedPriceRange: {
min: 45999,
max: 65999
},
suitableFor: “Families wanting to maintain normal activities during outages”
},
comfort: {
name: “Comfort Package: Enhanced Living”,
description: “Comfortable living during extended outages”,
typicalAppliances: [“lighting”, “refrigeration”, “entertainment”, “computing”, “water heating”],
typicalDailyConsumption: 10, // kWh
solarSystemSize: 5, // kW
batteryCapacity: 14, // kWh
inverterSize: 5, // kVA
estimatedPriceRange: {
min: 69999,
max: 95999
},
suitableFor: “Households wanting hot water and full functionality”
},
premium: {
name: “Premium Package: Complete Comfort”,
description: “Near-complete home functionality during outages”,
typicalAppliances: [“lighting”, “refrigeration”, “entertainment”, “computing”, “water heating”, “air conditioning”],
typicalDailyConsumption: 15, // kWh
solarSystemSize: 8, // kW
batteryCapacity: 20, // kWh
inverterSize: 8, // kVA
estimatedPriceRange: {
min: 99999,
max: 145999
},
suitableFor: “Homes wanting climate control and full functionality”
},
ultimate: {
name: “Ultimate Package: Total Independence”,
description: “Complete energy autonomy for your property”,
typicalAppliances: [“full home”, “high power appliances”, “pumps”],
typicalDailyConsumption: 25, // kWh
solarSystemSize: 12, // kW
batteryCapacity: 30, // kWh
inverterSize: 12, // kVA
estimatedPriceRange: {
min: 149999,
max: 199999
},
suitableFor: “Properties seeking complete energy independence”
}
};
// Initialize event listeners
this.initEventListeners();
}
// Initialize event listeners for all calculator inputs
initEventListeners() {
// Region selection change
document.querySelectorAll(‘.region-selector’).forEach(selector => {
selector.addEventListener(‘change’, (e) => {
this.handleRegionChange(e.target.value);
});
});
// Municipality selection change
document.querySelectorAll(‘.municipality-selector’).forEach(selector => {
selector.addEventListener(‘change’, (e) => {
this.handleMunicipalityChange(e.target.value);
});
});
// Calculation type selection
document.querySelectorAll(‘.calculation-type-selector’).forEach(button => {
button.addEventListener(‘click’, (e) => {
this.handleCalculationTypeChange(e.target.dataset.calculationType);
});
});
// Appliance selection
document.querySelectorAll(‘.appliance-checkbox’).forEach(checkbox => {
checkbox.addEventListener(‘change’, (e) => {
const applianceId = e.target.value;
const isChecked = e.target.checked;
const quantityInput = document.querySelector(`#quantity-${applianceId}`);
const hoursInput = document.querySelector(`#hours-${applianceId}`);
const quantity = quantityInput ? parseInt(quantityInput.value) : 1;
const hours = hoursInput ? parseFloat(hoursInput.value) : this.getDefaultHoursForAppliance(applianceId);
if (isChecked) {
this.addAppliance(applianceId, quantity, hours);
} else {
this.removeAppliance(applianceId);
}
});
});
// Appliance quantity change
document.querySelectorAll(‘.appliance-quantity’).forEach(input => {
input.addEventListener(‘change’, (e) => {
const applianceId = e.target.dataset.applianceId;
const quantity = parseInt(e.target.value);
this.updateApplianceQuantity(applianceId, quantity);
});
});
// Appliance hours change
document.querySelectorAll(‘.appliance-hours’).forEach(input => {
input.addEventListener(‘change’, (e) => {
const applianceId = e.target.dataset.applianceId;
const hours = parseFloat(e.target.value);
this.updateApplianceHours(applianceId, hours);
});
});
// Add custom appliance button
document.querySelectorAll(‘.add-custom-appliance-btn’).forEach(button => {
button.addEventListener(‘click’, () => {
const nameInput = document.querySelector(‘#custom-appliance-name’);
const powerInput = document.querySelector(‘#custom-appliance-power’);
const hoursInput = document.querySelector(‘#custom-appliance-hours’);
const name = nameInput.value.trim();
const power = parseInt(powerInput.value);
const hours = parseFloat(hoursInput.value);
if (name && power && hours) {
this.addCustomAppliance(name, power, hours);
// Clear inputs
nameInput.value = ”;
powerInput.value = ”;
hoursInput.value = ”;
}
});
});
// Monthly consumption input
document.querySelectorAll(‘.monthly-consumption-input’).forEach(input => {
input.addEventListener(‘change’, (e) => {
this.updateMonthlyConsumption(parseFloat(e.target.value));
});
});
// Offset percentage change
document.querySelectorAll(‘.offset-percentage-slider’).forEach(slider => {
slider.addEventListener(‘input’, (e) => {
this.updateOffsetPercentage(parseInt(e.target.value));
document.querySelectorAll(‘.offset-percentage-display’).forEach(display => {
display.textContent = `${this.state.offsetPercentage}%`;
});
});
});
// Backup hours slider
document.querySelectorAll(‘.backup-hours-slider’).forEach(slider => {
slider.addEventListener(‘input’, (e) => {
this.updateBackupHours(parseInt(e.target.value));
document.querySelectorAll(‘.backup-hours-display’).forEach(display => {
display.textContent = `${this.state.backupHours} hours`;
});
});
});
// Backup percentage selector
document.querySelectorAll(‘.backup-percentage-selector’).forEach(selector => {
selector.addEventListener(‘change’, (e) => {
this.updateBackupPercentage(parseInt(e.target.value));
});
});
// Wind turbine toggle
document.querySelectorAll(‘.wind-turbine-toggle’).forEach(toggle => {
toggle.addEventListener(‘change’, (e) => {
this.updateWindTurbineOption(e.target.checked);
});
});
// Calculate button
document.querySelectorAll(‘.calculate-btn’).forEach(button => {
button.addEventListener(‘click’, () => {
this.calculateResults();
});
});
// Package selection
document.querySelectorAll(‘.package-select-btn’).forEach(button => {
button.addEventListener(‘click’, (e) => {
this.selectPackage(e.target.dataset.packageId);
});
});
// Bill upload handler
const billUploadInput = document.querySelector(‘#bill-upload-input’);
if (billUploadInput) {
billUploadInput.addEventListener(‘change’, (e) => {
const file = e.target.files[0];
if (file) {
this.handleBillUpload(file);
}
});
}
// Expert analysis request button
document.querySelectorAll(‘.expert-analysis-btn’).forEach(button => {
button.addEventListener(‘click’, () => {
this.requestExpertAnalysis();
});
});
// Proceed to checkout button
document.querySelectorAll(‘.checkout-btn’).forEach(button => {
button.addEventListener(‘click’, () => {
this.proceedToCheckout();
});
});
// Google Maps API initialization
window.initMap = this.initGoogleMaps.bind(this);
}
// Handle region change
handleRegionChange(region) {
this.state.region = region;
// Update municipalities dropdown
const municipalities = this.municipalitiesByProvince[region] || [];
const municipalitySelectors = document.querySelectorAll(‘.municipality-selector’);
municipalitySelectors.forEach(selector => {
// Clear existing options
selector.innerHTML = ‘‘;
// Add new options
municipalities.forEach(municipality => {
const option = document.createElement(‘option’);
option.value = municipality;
option.textContent = municipality;
selector.appendChild(option);
});
});
// Update solar irradiance info display
const irradiance = this.solarIrradianceData[region] || 5.5;
document.querySelectorAll(‘.solar-irradiance-display’).forEach(display => {
display.textContent = `${irradiance} kWh/m²/day`;
});
}
// Handle municipality change
handleMunicipalityChange(municipality) {
this.state.municipality = municipality;
// Update electricity tariff info display
const tariff = this.electricityTariffs[municipality] || this.electricityTariffs[‘Default’];
document.querySelectorAll(‘.electricity-tariff-display’).forEach(display => {
display.textContent = `R${tariff.toFixed(2)}/kWh`;
});
}
// Handle change in calculation type
handleCalculationTypeChange(calculationType) {
this.state.calculationType = calculationType;
// Show/hide relevant sections
if (calculationType === ‘appliance’) {
document.querySelectorAll(‘.appliance-based-section’).forEach(section => {
section.style.display = ‘block’;
});
document.querySelectorAll(‘.bill-based-section’).forEach(section => {
section.style.display = ‘none’;
});
} else if (calculationType === ‘bill’) {
document.querySelectorAll(‘.appliance-based-section’).forEach(section => {
section.style.display = ‘none’;
});
document.querySelectorAll(‘.bill-based-section’).forEach(section => {
section.style.display = ‘block’;
});
}
// Reset any previous calculation results
this.state.results = null;
this.state.selectedPackage = null;
this.hideResults();
}
// Add appliance to calculation
addAppliance(applianceId, quantity = 1, hours = null) {
// Find appliance in library
let appliance = null;
Object.values(this.applianceLibrary).forEach(category => {
const found = category.find(a => a.id === applianceId);
if (found) appliance = found;
});
if (!appliance) return;
// If hours not specified, use default from library
const usageHours = hours || appliance.avgHoursPerDay;
// Add to state
this.state.appliances.push({
id: applianceId,
name: appliance.name,
power: appliance.power,
quantity: quantity,
hours: usageHours,
daily: appliance.power * quantity * usageHours / 1000 // kWh per day
});
// Update daily consumption display
this.updateDailyConsumptionDisplay();
}
// Remove appliance from calculation
removeAppliance(applianceId) {
this.state.appliances = this.state.appliances.filter(a => a.id !== applianceId);
this.updateDailyConsumptionDisplay();
}
// Update appliance quantity
updateApplianceQuantity(applianceId, quantity) {
const appliance = this.state.appliances.find(a => a.id === applianceId);
if (appliance) {
appliance.quantity = quantity;
appliance.daily = appliance.power * quantity * appliance.hours / 1000;
this.updateDailyConsumptionDisplay();
}
}
// Update appliance usage hours
updateApplianceHours(applianceId, hours) {
const appliance = this.state.appliances.find(a => a.id === applianceId);
if (appliance) {
appliance.hours = hours;
appliance.daily = appliance.power * appliance.quantity * hours / 1000;
this.updateDailyConsumptionDisplay();
}
}
// Add custom appliance
addCustomAppliance(name, power, hours) {
const id = `custom_${Date.now()}`;
this.state.customAppliances.push({
id,
name,
power,
hours,
quantity: 1,
daily: power * hours / 1000
});
// Also add to main appliances list
this.state.appliances.push({
id,
name,
power,
hours,
quantity: 1,
daily: power * hours / 1000
});
// Update the UI
this.updateCustomAppliancesList();
this.updateDailyConsumptionDisplay();
}
// Update custom appliances list in UI
updateCustomAppliancesList() {
const container = document.querySelector(‘.custom-appliances-list’);
if (!container) return;
// Clear existing list
container.innerHTML = ”;
// Add each custom appliance
this.state.customAppliances.forEach(appliance => {
const applianceItem = document.createElement(‘div’);
applianceItem.className = ‘custom-appliance-item’;
applianceItem.innerHTML = `
${appliance.power}W × ${appliance.hours}h = ${appliance.daily.toFixed(2)} kWh/day
`;
container.appendChild(applianceItem);
// Add event listener to remove button
applianceItem.querySelector(‘.remove-custom-appliance’).addEventListener(‘click’, (e) => {
const id = e.target.dataset.applianceId;
this.removeCustomAppliance(id);
});
});
}
// Remove custom appliance
removeCustomAppliance(id) {
this.state.customAppliances = this.state.customAppliances.filter(a => a.id !== id);
this.state.appliances = this.state.appliances.filter(a => a.id !== id);
this.updateCustomAppliancesList();
this.updateDailyConsumptionDisplay();
}
// Update monthly consumption input
updateMonthlyConsumption(consumption) {
this.state.monthlyConsumption = consumption;
// Update daily equivalent display
const daily = consumption / 30;
document.querySelectorAll(‘.daily-consumption-display’).forEach(display => {
display.textContent = `${daily.toFixed(2)} kWh/day`;
});
}
// Update offset percentage
updateOffsetPercentage(percentage) {
this.state.offsetPercentage = percentage;
}
// Update backup hours
updateBackupHours(hours) {
this.state.backup
// Update backup hours
updateBackupHours(hours) {
this.state.backupHours = hours;
}
// Update backup percentage
updateBackupPercentage(percentage) {
this.state.backupPercentage = percentage;
}
// Update wind turbine option
updateWindTurbineOption(includeWindTurbine) {
this.state.includeWindTurbine = includeWindTurbine;
}
// Get default hours for an appliance from the library
getDefaultHoursForAppliance(applianceId) {
let defaultHours = 1; // Default fallback
Object.values(this.applianceLibrary).forEach(category => {
const appliance = category.find(a => a.id === applianceId);
if (appliance) {
defaultHours = appliance.avgHoursPerDay;
}
});
return defaultHours;
}
// Update daily consumption display based on selected appliances
updateDailyConsumptionDisplay() {
const dailyTotal = this.state.appliances.reduce((sum, appliance) => {
return sum + appliance.daily;
}, 0);
document.querySelectorAll(‘.appliance-daily-consumption’).forEach(display => {
display.textContent = `${dailyTotal.toFixed(2)} kWh/day`;
});
document.querySelectorAll(‘.appliance-monthly-consumption’).forEach(display => {
display.textContent = `${(dailyTotal * 30).toFixed(2)} kWh/month`;
});
}
// Calculate system requirements based on user inputs
calculateResults() {
// Determine daily energy consumption
let dailyConsumption = 0;
if (this.state.calculationType === ‘appliance’) {
dailyConsumption = this.state.appliances.reduce((sum, appliance) => {
return sum + appliance.daily;
}, 0);
} else if (this.state.calculationType === ‘bill’) {
dailyConsumption = this.state.monthlyConsumption / 30;
}
// Apply offset percentage
const offsetConsumption = dailyConsumption * (this.state.offsetPercentage / 100);
// Calculate solar system size
const regionIrradiance = this.solarIrradianceData[this.state.region] || 5.5; // Default to 5.5 if region not found
// Account for system losses (inverter efficiency, wiring, etc.)
const systemLosses = 0.25; // 25% losses
// Calculate required solar capacity in kW
const requiredSolarCapacity = offsetConsumption / ((regionIrradiance * (1 – systemLosses)));
// Calculate backup battery capacity
const backupPortion = this.state.backupPercentage / 100;
const backupConsumption = dailyConsumption * backupPortion;
const backupHours = this.state.backupHours;
// Calculate battery capacity in kWh, accounting for depth of discharge (DoD)
const batteryCycleEfficiency = 0.9; // 90% efficiency
const batteryDoD = 0.8; // 80% depth of discharge recommended
const requiredBatteryCapacity = (backupConsumption * (backupHours / 24)) / (batteryCycleEfficiency * batteryDoD);
// Calculate inverter size (needs to handle peak load)
// Estimate peak load as 1.5x the daily consumption converted to hourly rate
// With a minimum of 1.5x the largest appliance
let peakPower = (dailyConsumption / 8) * 1000; // Convert to W, assuming 8 hours of active usage
if (this.state.appliances.length > 0) {
// Find the largest appliance by power
const largestAppliance = this.state.appliances.reduce((max, appliance) => {
return (appliance.power > max.power) ? appliance : max;
}, { power: 0 });
// Ensure inverter can handle at least 1.5x largest appliance power
peakPower = Math.max(peakPower, largestAppliance.power * 1.5);
}
// Convert to kVA with a power factor of 0.8
const requiredInverterSize = peakPower / 1000 / 0.8;
// Calculate wind turbine contribution if selected
let windTurbineDetails = null;
if (this.state.includeWindTurbine) {
// Choose appropriate turbine size based on energy needs
let turbineSize = ‘small’;
if (dailyConsumption > 15) {
turbineSize = ‘large’;
} else if (dailyConsumption > 8) {
turbineSize = ‘medium’;
}
// Get turbine details
const turbine = this.componentPricing.windTurbines[turbineSize];
// Estimate daily production (kWh) assuming 4 hours of equivalent full output
const dailyWindProduction = turbine.output * 4;
windTurbineDetails = {
size: turbineSize,
capacity: turbine.output,
dailyProduction: dailyWindProduction,
price: turbine.price,
brands: turbine.brands
};
}
// Estimate costs
const solarPanelCost = requiredSolarCapacity * 1000 * this.componentPricing.solarPanels.standard.pricePerWatt;
const batteryCost = requiredBatteryCapacity * this.componentPricing.batteries.lithiumIon.pricePerKwh;
const inverterCost = requiredInverterSize * this.componentPricing.inverters.hybrid.pricePerKva;
const installationCost = this.componentPricing.installation.basicRate +
(requiredSolarCapacity * this.componentPricing.installation.perKwSolar) +
(requiredBatteryCapacity * this.componentPricing.installation.perKwhBattery);
let windTurbineCost = 0;
if (windTurbineDetails) {
windTurbineCost = windTurbineDetails.price;
}
const totalCost = solarPanelCost + batteryCost + inverterCost + installationCost + windTurbineCost;
// Calculate ROI
const annualElectricityProduction = offsetConsumption * 365; // Annual kWh production
const electricityPrice = this.electricityTariffs[this.state.municipality] || this.electricityTariffs[‘Default’];
const annualSavings = annualElectricityProduction * electricityPrice;
const simplePaybackPeriod = totalCost / annualSavings;
// Prepare recommended packages
const recommendedPackage = this.recommendPackage(dailyConsumption);
// Store results in state
this.state.results = {
dailyConsumption,
offsetConsumption,
requiredSolarCapacity,
requiredBatteryCapacity,
requiredInverterSize,
solarPanelCost,
batteryCost,
inverterCost,
installationCost,
windTurbineCost,
windTurbineDetails,
totalCost,
annualElectricityProduction,
annualSavings,
simplePaybackPeriod,
recommendedPackage
};
// Display results
this.displayResults();
}
// Find the appropriate pre-configured package based on daily consumption
recommendPackage(dailyConsumption) {
// Sort packages by typical daily consumption ascending
const sortedPackages = Object.values(this.packageTemplates).sort((a, b) => {
return a.typicalDailyConsumption – b.typicalDailyConsumption;
});
// Find the first package that meets or exceeds the daily consumption
for (const pkg of sortedPackages) {
if (pkg.typicalDailyConsumption >= dailyConsumption) {
return pkg;
}
}
// If no package is sufficient, return the highest one
return sortedPackages[sortedPackages.length – 1];
}
// Display calculation results
displayResults() {
if (!this.state.results) return;
const results = this.state.results;
// Show results section
document.querySelectorAll(‘.results-section’).forEach(section => {
section.style.display = ‘block’;
});
// Update result values
document.querySelectorAll(‘.result-daily-consumption’).forEach(el => {
el.textContent = `${results.dailyConsumption.toFixed(2)} kWh/day`;
});
document.querySelectorAll(‘.result-monthly-consumption’).forEach(el => {
el.textContent = `${(results.dailyConsumption * 30).toFixed(2)} kWh/month`;
});
document.querySelectorAll(‘.result-solar-capacity’).forEach(el => {
el.textContent = `${results.requiredSolarCapacity.toFixed(2)} kW`;
});
document.querySelectorAll(‘.result-battery-capacity’).forEach(el => {
el.textContent = `${results.requiredBatteryCapacity.toFixed(2)} kWh`;
});
document.querySelectorAll(‘.result-inverter-size’).forEach(el => {
el.textContent = `${results.requiredInverterSize.toFixed(2)} kVA`;
});
// Update cost breakdown
document.querySelectorAll(‘.result-solar-cost’).forEach(el => {
el.textContent = `R${Math.round(results.solarPanelCost).toLocaleString()}`;
});
document.querySelectorAll(‘.result-battery-cost’).forEach(el => {
el.textContent = `R${Math.round(results.batteryCost).toLocaleString()}`;
});
document.querySelectorAll(‘.result-inverter-cost’).forEach(el => {
el.textContent = `R${Math.round(results.inverterCost).toLocaleString()}`;
});
document.querySelectorAll(‘.result-installation-cost’).forEach(el => {
el.textContent = `R${Math.round(results.installationCost).toLocaleString()}`;
});
if (results.windTurbineDetails) {
document.querySelectorAll(‘.result-wind-turbine-row’).forEach(row => {
row.style.display = ‘table-row’;
});
document.querySelectorAll(‘.result-wind-turbine-cost’).forEach(el => {
el.textContent = `R${Math.round(results.windTurbineCost).toLocaleString()}`;
});
} else {
document.querySelectorAll(‘.result-wind-turbine-row’).forEach(row => {
row.style.display = ‘none’;
});
}
document.querySelectorAll(‘.result-total-cost’).forEach(el => {
el.textContent = `R${Math.round(results.totalCost).toLocaleString()}`;
});
// Update ROI information
document.querySelectorAll(‘.result-annual-production’).forEach(el => {
el.textContent = `${Math.round(results.annualElectricityProduction).toLocaleString()} kWh`;
});
document.querySelectorAll(‘.result-annual-savings’).forEach(el => {
el.textContent = `R${Math.round(results.annualSavings).toLocaleString()}`;
});
document.querySelectorAll(‘.result-payback-period’).forEach(el => {
el.textContent = `${results.simplePaybackPeriod.toFixed(1)} years`;
});
// Display recommended package
const recommendedPackage = results.recommendedPackage;
if (recommendedPackage) {
document.querySelectorAll(‘.recommended-package-name’).forEach(el => {
el.textContent = recommendedPackage.name;
});
document.querySelectorAll(‘.recommended-package-description’).forEach(el => {
el.textContent = recommendedPackage.description;
});
document.querySelectorAll(‘.recommended-package-price-range’).forEach(el => {
el.textContent = `R${recommendedPackage.estimatedPriceRange.min.toLocaleString()} – R${recommendedPackage.estimatedPriceRange.max.toLocaleString()}`;
});
}
// Populate package cards
this.displayPackageOptions();
}
// Hide results section
hideResults() {
document.querySelectorAll(‘.results-section’).forEach(section => {
section.style.display = ‘none’;
});
}
// Display package options based on calculation results
displayPackageOptions() {
const packagesContainer = document.querySelector(‘.packages-container’);
if (!packagesContainer) return;
// Clear existing packages
packagesContainer.innerHTML = ”;
const dailyConsumption = this.state.results.dailyConsumption;
// Display all packages, highlighting the recommended one
Object.entries(this.packageTemplates).forEach(([id, pkg]) => {
const isRecommended = pkg === this.state.results.recommendedPackage;
const packageCard = document.createElement(‘div’);
packageCard.className = `package-card ${isRecommended ? ‘recommended-package’ : ”}`;
packageCard.innerHTML = `
${pkg.name}
${isRecommended ? ‘Recommended‘ : ”}
${pkg.description}
${pkg.solarSystemSize} kW
${pkg.batteryCapacity} kWh
${pkg.inverterSize} kVA
${pkg.suitableFor}
`;
packagesContainer.appendChild(packageCard);
// Add event listeners to the new buttons
packageCard.querySelector(‘.package-select-btn’).addEventListener(‘click’, () => {
this.selectPackage(id);
});
packageCard.querySelector(‘.package-customize-btn’).addEventListener(‘click’, () => {
this.customizePackage(id);
});
});
}
// Handle package selection
selectPackage(packageId) {
this.state.selectedPackage = this.packageTemplates[packageId];
this.showSelectedPackageDetails();
}
// Handle package customization
customizePackage(packageId) {
const basePackage = this.packageTemplates[packageId];
// Clone the package to customize
this.state.customizedPackage = JSON.parse(JSON.stringify(basePackage));
// Show customization form
document.querySelectorAll(‘.package-customization-section’).forEach(section => {
section.style.display = ‘block’;
});
// Pre-fill form with package values
document.querySelectorAll(‘.custom-solar-size’).forEach(input => {
input.value = basePackage.solarSystemSize;
});
document.querySelectorAll(‘.custom-battery-capacity’).forEach(input => {
input.value = basePackage.batteryCapacity;
});
document.querySelectorAll(‘.custom-inverter-size’).forEach(input => {
input.value = basePackage.inverterSize;
});
// Add event listeners for customization
document.querySelectorAll(‘.custom-solar-size’).forEach(input => {
input.addEventListener(‘change’, (e) => {
this.state.customizedPackage.solarSystemSize = parseFloat(e.target.value);
this.updateCustomizedPackagePrice();
});
});
document.querySelectorAll(‘.custom-battery-capacity’).forEach(input => {
input.addEventListener(‘change’, (e) => {
this.state.customizedPackage.batteryCapacity = parseFloat(e.target.value);
this.updateCustomizedPackagePrice();
});
});
document.querySelectorAll(‘.custom-inverter-size’).forEach(input => {
input.addEventListener(‘change’, (e) => {
this.state.customizedPackage.inverterSize = parseFloat(e.target.value);
this.updateCustomizedPackagePrice();
});
});
// Initial price update
this.updateCustomizedPackagePrice();
}
// Update the price of the customized package
updateCustomizedPackagePrice() {
if (!this.state.customizedPackage) return;
const pkg = this.state.customizedPackage;
// Calculate new prices based on component costs
const solarCost = pkg.solarSystemSize * 1000 * this.componentPricing.solarPanels.standard.pricePerWatt;
const batteryCost = pkg.batteryCapacity * this.componentPricing.batteries.lithiumIon.pricePerKwh;
const inverterCost = pkg.inverterSize * this.componentPricing.inverters.hybrid.pricePerKva;
const installationCost = this.componentPricing.installation.basicRate +
(pkg.solarSystemSize * this.componentPricing.installation.perKwSolar) +
(pkg.batteryCapacity * this.componentPricing.installation.perKwhBattery);
const totalCost = solarCost + batteryCost + inverterCost + installationCost;
// Add 5% margin for price range
pkg.estimatedPriceRange = {
min: Math.round(totalCost * 0.95),
max: Math.round(totalCost * 1.05)
};
// Update displayed price
document.querySelectorAll(‘.customized-package-price’).forEach(el => {
el.textContent = `R${pkg.estimatedPriceRange.min.toLocaleString()} – R${pkg.estimatedPriceRange.max.toLocaleString()}`;
});
}
// Show details of the selected package
showSelectedPackageDetails() {
if (!this.state.selectedPackage) return;
// Show selected package section
document.querySelectorAll(‘.selected-package-section’).forEach(section => {
section.style.display = ‘block’;
});
const pkg = this.state.selectedPackage;
// Update package details
document.querySelectorAll(‘.selected-package-name’).forEach(el => {
el.textContent = pkg.name;
});
document.querySelectorAll(‘.selected-package-description’).forEach(el => {
el.textContent = pkg.description;
});
document.querySelectorAll(‘.selected-package-solar’).forEach(el => {
el.textContent = `${pkg.solarSystemSize} kW`;
});
document.querySelectorAll(‘.selected-package-battery’).forEach(el => {
el.textContent = `${pkg.batteryCapacity} kWh`;
});
document.querySelectorAll(‘.selected-package-inverter’).forEach(el => {
el.textContent = `${pkg.inverterSize} kVA`;
});
document.querySelectorAll(‘.selected-package-price’).forEach(el => {
el.textContent = `R${pkg.estimatedPriceRange.min.toLocaleString()} – R${pkg.estimatedPriceRange.max.toLocaleString()}`;
});
}
// Handle bill upload
handleBillUpload(file) {
const reader = new FileReader();
reader.onload = (e) => {
// In a real implementation, this would use machine learning or OCR to extract data
// For now, we’ll simulate extraction with a prompt
const consumption = prompt(‘Enter the monthly consumption (kWh) from your bill:’, ‘500’);
if (consumption) {
this.updateMonthlyConsumption(parseFloat(consumption));
}
};
reader.readAsText(file);
}
// Request expert analysis
requestExpertAnalysis() {
// In a real implementation, this would send data to a backend for analysis
// For now, show a confirmation message
alert(‘Thank you for requesting an expert analysis. Our team will contact you within 24 hours.’);
// Collect user contact information
const name = prompt(‘Please enter your name:’);
const email = prompt(‘Please enter your email address:’);
const phone = prompt(‘Please enter your phone number:’);
if (name && email) {
// In a real implementation, this data would be sent to a backend
console.log(‘Expert analysis requested by:’, name, email, phone);
}
}
// Proceed to checkout
proceedToCheckout() {
// Determine which package to use
const package = this.state.customizedPackage || this.state.selectedPackage;
if (!package) {
alert(‘Please select a package before proceeding to checkout.’);
return;
}
// In a real implementation, this would navigate to a checkout page
// For now, we’ll simulate with an alert
alert(`Proceeding to checkout with ${package.name}. Total estimated price: R${package.estimatedPriceRange.min.toLocaleString()} – R${package.estimatedPri
// Calculate ROI metrics
const netPresentValue = this.calculateNPV(yearlyAnalysis, package.estimatedPriceRange.min);
const internalRateOfReturn = this.calculateIRR(yearlyAnalysis, package.estimatedPriceRange.min);
return {
systemCost: package.estimatedPriceRange,
loanOptions: loanOptions,
yearlyAnalysis: yearlyAnalysis,
netPresentValue: netPresentValue,
internalRateOfReturn: internalRateOfReturn,
paybackPeriod: results.simplePaybackPeriod,
lifetimeSavings: cumulativeSavings,
lifetimeROI: (cumulativeSavings / package.estimatedPriceRange.min) * 100
};
}
// Calculate Net Present Value
calculateNPV(yearlyAnalysis, initialInvestment) {
const discountRate = 0.06; // 6% discount rate
let npv = -initialInvestment;
yearlyAnalysis.forEach((yearData, index) => {
const year = index + 1;
const netCashFlow = yearData.savings – yearData.maintenanceCost;
npv += netCashFlow / Math.pow(1 + discountRate, year);
});
return npv;
}
// Calculate Internal Rate of Return
calculateIRR(yearlyAnalysis, initialInvestment) {
// Simplified IRR calculation using approximation
// In a real implementation, you would use a more sophisticated algorithm
const maxIterations = 1000;
const tolerance = 0.0001;
let irr = 0.1; // Initial guess
let step = 0.05;
for (let i = 0; i < maxIterations; i++) {
// Calculate NPV with current IRR guess
let npv = -initialInvestment;
yearlyAnalysis.forEach((yearData, index) => {
const year = index + 1;
const netCashFlow = yearData.savings – yearData.maintenanceCost;
npv += netCashFlow / Math.pow(1 + irr, year);
});
// Check if NPV is close enough to zero
if (Math.abs(npv) < tolerance) {
return irr * 100; // Convert to percentage
}
// Adjust IRR based on NPV sign
if (npv > 0) {
irr += step;
} else {
irr -= step;
}
// Reduce step size to converge
step *= 0.9;
}
// Return best approximation
return irr * 100; // Convert to percentage
}
// Display financial report
displayFinancialReport() {
const report = this.generateFinancialReport();
if (!report) return;
// Show financial report section
document.querySelectorAll(‘.financial-report-section’).forEach(section => {
section.style.display = ‘block’;
});
// Update report values
document.querySelectorAll(‘.report-system-cost’).forEach(el => {
el.textContent = `R${report.systemCost.min.toLocaleString()} – R${report.systemCost.max.toLocaleString()}`;
});
document.querySelectorAll(‘.report-payback-period’).forEach(el => {
el.textContent = `${report.paybackPeriod.toFixed(1)} years`;
});
document.querySelectorAll(‘.report-npv’).forEach(el => {
el.textContent = `R${Math.round(report.netPresentValue).toLocaleString()}`;
});
document.querySelectorAll(‘.report-irr’).forEach(el => {
el.textContent = `${report.internalRateOfReturn.toFixed(1)}%`;
});
document.querySelectorAll(‘.report-lifetime-savings’).forEach(el => {
el.textContent = `R${Math.round(report.lifetimeSavings).toLocaleString()}`;
});
document.querySelectorAll(‘.report-lifetime-roi’).forEach(el => {
el.textContent = `${report.lifetimeROI.toFixed(1)}%`;
});
// Populate loan options table
const loanTableBody = document.querySelector(‘.loan-options-table tbody’);
if (loanTableBody) {
loanTableBody.innerHTML = ”;
report.loanOptions.forEach(option => {
const row = document.createElement(‘tr’);
row.innerHTML = `
`;
loanTableBody.appendChild(row);
});
}
// Create cash flow chart if Chart.js is available
if (window.Chart && document.getElementById(‘cash-flow-chart’)) {
this.createCashFlowChart(report.yearlyAnalysis);
}
}
// Create cash flow chart using Chart.js
createCashFlowChart(yearlyAnalysis) {
const ctx = document.getElementById(‘cash-flow-chart’).getContext(‘2d’);
// Extract data for chart
const labels = yearlyAnalysis.map(year => `Year ${year.year}`);
const cumulativeSavings = yearlyAnalysis.map(year => year.cumulativeSavings);
const cumulativeCosts = yearlyAnalysis.map(year => year.cumulativeCosts);
const netSavings = yearlyAnalysis.map(year => year.netSavings);
// Destroy existing chart if it exists
if (this.cashFlowChart) {
this.cashFlowChart.destroy();
}
// Create new chart
this.cashFlowChart = new Chart(ctx, {
type: ‘line’,
data: {
labels: labels,
datasets: [
{
label: ‘Cumulative Savings’,
data: cumulativeSavings,
borderColor: ‘rgba(75, 192, 192, 1)’,
backgroundColor: ‘rgba(75, 192, 192, 0.2)’,
tension: 0.1
},
{
label: ‘Cumulative Costs’,
data: cumulativeCosts,
borderColor: ‘rgba(255, 99, 132, 1)’,
backgroundColor: ‘rgba(255, 99, 132, 0.2)’,
tension: 0.1
},
{
label: ‘Net Savings’,
data: netSavings,
borderColor: ‘rgba(54, 162, 235, 1)’,
backgroundColor: ‘rgba(54, 162, 235, 0.2)’,
tension: 0.1
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: ‘South African Rand (R)’
}
}
}
}
});
}
// Generate and display PDF report
generatePdfReport() {
// In a real implementation, this would use a PDF generation library
// For now, we’ll just simulate it
alert(‘Generating PDF report…’);
// Collect report data
const reportData = {
customerInfo: {
name: prompt(‘Please enter your name for the report:’),
email: prompt(‘Please enter your email address:’),
phone: prompt(‘Please enter your phone number:’),
address: prompt(‘Please enter your address:’)
},
systemSpecs: this.state.results,
selectedPackage: this.state.selectedPackage || this.state.customizedPackage,
financialAnalysis: this.generateFinancialReport()
};
console.log(‘Report data:’, reportData);
// In a real implementation, this would generate a PDF and offer it for download
alert(‘Your PDF report has been generated and sent to your email.’);
}
// Initialize the app
init() {
// Populate appliance library
this.populateApplianceLibrary();
// Initialize location map
this.initGoogleMaps();
// Set up event listeners
this.setupEventListeners();
// Hide results initially
this.hideResults();
}
}
// Initialize the calculator when the DOM is loaded
document.addEventListener(‘DOMContentLoaded’, () => {
const solarCalculator = new SolarCalculator();
solarCalculator.init();
// Make calculator available globally for debugging
window.solarCalculator = solarCalculator;
});