- Reformat HTML for readability and semantic structure - Refactor JavaScript for modularity and robustness - Improve language switching and translation logic - Enhance input validation and error handling - Add missing DOM element checks and warnings - Update UI rendering for results and utility lists - Remove unused code and streamline event listeners
1354 lines
61 KiB
HTML
1354 lines
61 KiB
HTML
<!doctype html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Calcolatore di Risparmio Fotovoltaico (Completo)</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<style>
|
|
body {
|
|
font-family: "Inter", sans-serif;
|
|
}
|
|
.card-shadow {
|
|
box-shadow:
|
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
.action-btn:hover {
|
|
opacity: 0.8;
|
|
}
|
|
.action-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
.lang-btn {
|
|
opacity: 0.6;
|
|
}
|
|
.lang-btn.active {
|
|
opacity: 1;
|
|
font-weight: 800;
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 flex items-center justify-center min-h-screen p-4">
|
|
<div
|
|
class="w-full max-w-3xl bg-white rounded-2xl card-shadow overflow-hidden"
|
|
>
|
|
<div class="p-6 md:p-8">
|
|
<div class="flex justify-end gap-3 text-sm mb-4">
|
|
<button id="lang-it" class="lang-btn">IT</button>
|
|
<button id="lang-en" class="lang-btn">EN</button>
|
|
</div>
|
|
|
|
<h1
|
|
class="text-2xl md:text-3xl font-bold text-gray-800 text-center mb-2"
|
|
data-translate-key="mainTitle"
|
|
>
|
|
Calcolatore Economico Fotovoltaico
|
|
</h1>
|
|
<p
|
|
class="text-gray-600 text-center mb-6 md:mb-8"
|
|
data-translate-key="mainSubtitle"
|
|
>
|
|
Analizza costi, risparmi e guadagni del tuo impianto solare.
|
|
</p>
|
|
|
|
<!-- Sezione Input Principali -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div>
|
|
<label
|
|
for="consumoTotale"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="totalConsumptionLabel"
|
|
>Consumo Totale Casa (kWh)</label
|
|
>
|
|
<input
|
|
type="number"
|
|
id="consumoTotale"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
|
data-translate-key-placeholder="totalConsumptionPlaceholder"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
for="consumoRete"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="gridConsumptionLabel"
|
|
>Consumo dalla Rete (kWh)</label
|
|
>
|
|
<input
|
|
type="number"
|
|
id="consumoRete"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
|
data-translate-key-placeholder="gridConsumptionPlaceholder"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
for="energiaImmessa"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="feedInEnergyLabel"
|
|
>Energia Immessa in Rete (kWh)</label
|
|
>
|
|
<input
|
|
type="number"
|
|
id="energiaImmessa"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
|
data-translate-key-placeholder="feedInEnergyPlaceholder"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
for="costoKwh"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="purchasePriceLabel"
|
|
>Costo Acquisto kWh (€)</label
|
|
>
|
|
<input
|
|
type="number"
|
|
id="costoKwh"
|
|
step="0.01"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
|
data-translate-key-placeholder="purchasePricePlaceholder"
|
|
/>
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label
|
|
for="prezzoVendita"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="sellPriceLabel"
|
|
>Prezzo Vendita kWh (€)</label
|
|
>
|
|
<input
|
|
type="number"
|
|
id="prezzoVendita"
|
|
step="0.01"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
|
data-translate-key-placeholder="sellPricePlaceholder"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sezione Gestione Gruppi -->
|
|
<div class="border-t pt-6 mb-6">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-700 mb-4 text-center"
|
|
data-translate-key="groupManagementTitle"
|
|
>
|
|
Gestione Gruppi di Utenze
|
|
</h3>
|
|
<div class="bg-gray-50 p-4 rounded-lg">
|
|
<div
|
|
class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end"
|
|
>
|
|
<div>
|
|
<label
|
|
for="groupNameInput"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="newGroupNameLabel"
|
|
>Nome Nuovo Gruppo</label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="groupNameInput"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
data-translate-key-placeholder="newGroupNamePlaceholder"
|
|
/>
|
|
</div>
|
|
<button
|
|
id="saveGroupBtn"
|
|
class="w-full bg-teal-600 text-white font-semibold py-2 px-4 rounded-lg action-btn transition-colors"
|
|
data-translate-key="saveGroupBtn"
|
|
>
|
|
Salva Lista Corrente come Gruppo
|
|
</button>
|
|
</div>
|
|
<div class="border-t my-4"></div>
|
|
<div
|
|
class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end"
|
|
>
|
|
<div class="md:col-span-2">
|
|
<label
|
|
for="groupSelect"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="savedGroupsLabel"
|
|
>Gruppi Salvati</label
|
|
>
|
|
<select
|
|
id="groupSelect"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white"
|
|
></select>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button
|
|
id="loadGroupBtn"
|
|
class="w-full bg-blue-500 text-white font-semibold py-2 px-4 rounded-lg action-btn transition-colors"
|
|
data-translate-key="loadBtn"
|
|
>
|
|
Carica
|
|
</button>
|
|
<button
|
|
id="deleteGroupBtn"
|
|
class="w-full bg-red-500 text-white font-semibold py-2 px-4 rounded-lg action-btn transition-colors"
|
|
data-translate-key="deleteBtn"
|
|
>
|
|
Elimina
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sezione Utenze Specifiche -->
|
|
<div class="border-t pt-6">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-700 mb-4 text-center"
|
|
data-translate-key="specificUtilitiesTitle"
|
|
>
|
|
Aggiungi Utenze Specifiche (Opzionale)
|
|
</h3>
|
|
<div class="flex flex-col md:flex-row gap-4 items-end mb-4">
|
|
<div class="flex-grow w-full">
|
|
<label
|
|
for="nomeUtenza"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="utilityNameLabel"
|
|
>Nome Utenza</label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="nomeUtenza"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
data-translate-key-placeholder="utilityNamePlaceholder"
|
|
/>
|
|
</div>
|
|
<div class="flex-grow w-full md:w-auto">
|
|
<label
|
|
for="consumoUtenza"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
data-translate-key="consumptionLabel"
|
|
>Consumo (kWh)</label
|
|
>
|
|
<input
|
|
type="number"
|
|
id="consumoUtenza"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
data-translate-key-placeholder="consumptionPlaceholder"
|
|
/>
|
|
</div>
|
|
<button
|
|
id="addUtenzaBtn"
|
|
class="w-full md:w-auto bg-gray-700 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-4 focus:ring-gray-300 transition-all"
|
|
data-translate-key="addBtn"
|
|
>
|
|
Aggiungi
|
|
</button>
|
|
</div>
|
|
<div id="listaUtenze" class="space-y-2">
|
|
<!-- Le utenze aggiunte verranno mostrate qui -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pulsante di Calcolo -->
|
|
<div class="text-center mt-8">
|
|
<button
|
|
id="calcolaBtn"
|
|
class="bg-blue-600 text-white font-bold py-3 px-8 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 transition-all duration-300 transform hover:scale-105"
|
|
data-translate-key="calculateBtn"
|
|
>
|
|
Calcola Bilancio
|
|
</button>
|
|
</div>
|
|
<!-- Messaggio di Errore -->
|
|
<div
|
|
id="error-message"
|
|
class="hidden mt-4 text-center text-red-600 font-medium p-3 bg-red-100 rounded-lg"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- Sezione Risultati -->
|
|
<div
|
|
id="risultati"
|
|
class="hidden bg-gray-50 p-6 md:p-8 border-t border-gray-200"
|
|
>
|
|
<!-- Riepilogo Economico -->
|
|
<div class="mb-8">
|
|
<h2
|
|
class="text-xl md:text-2xl font-bold text-gray-800 text-center mb-6"
|
|
data-translate-key="economicSummaryTitle"
|
|
>
|
|
Riepilogo Economico
|
|
</h2>
|
|
<div
|
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-center"
|
|
>
|
|
<div
|
|
class="bg-white p-4 rounded-lg border border-red-200"
|
|
>
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="costFromGridLabel"
|
|
>
|
|
Costo dalla Rete
|
|
</p>
|
|
<p
|
|
id="costoRete"
|
|
class="text-2xl font-bold text-red-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
<div
|
|
class="bg-white p-4 rounded-lg border border-green-200"
|
|
>
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="savingsFromSelfConsumptionLabel"
|
|
>
|
|
Risparmio Autoconsumo
|
|
</p>
|
|
<p
|
|
id="risparmioFV"
|
|
class="text-2xl font-bold text-green-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
<div
|
|
class="bg-white p-4 rounded-lg border border-yellow-400"
|
|
>
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="feedInRevenueLabel"
|
|
>
|
|
Guadagno Immissione
|
|
</p>
|
|
<p
|
|
id="guadagnoImmissione"
|
|
class="text-2xl font-bold text-yellow-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
<div
|
|
id="bilancioCard"
|
|
class="bg-white p-4 rounded-lg border-2"
|
|
>
|
|
<p
|
|
class="text-sm font-semibold"
|
|
data-translate-key="finalBalanceLabel"
|
|
>
|
|
Bilancio Finale
|
|
</p>
|
|
<p id="bilancioFinale" class="text-2xl font-bold">
|
|
-
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Riepilogo Energetico -->
|
|
<div class="mb-8 pt-6 border-t">
|
|
<h2
|
|
class="text-xl md:text-2xl font-bold text-gray-800 text-center mb-6"
|
|
data-translate-key="energySummaryTitle"
|
|
>
|
|
Riepilogo Energetico (kWh)
|
|
</h2>
|
|
<div
|
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-center"
|
|
>
|
|
<div class="bg-white p-4 rounded-lg border">
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="selfConsumedEnergyLabel"
|
|
>
|
|
Energia Autoconsumata
|
|
</p>
|
|
<p
|
|
id="energiaFV"
|
|
class="text-2xl font-bold text-blue-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
<div class="bg-white p-4 rounded-lg border">
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="energyFromGridLabel"
|
|
>
|
|
Energia da Rete
|
|
</p>
|
|
<p
|
|
id="energiaRete"
|
|
class="text-2xl font-bold text-red-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
<div class="bg-white p-4 rounded-lg border">
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="fedInEnergySummaryLabel"
|
|
>
|
|
Energia Immessa
|
|
</p>
|
|
<p
|
|
id="energiaImmessaOut"
|
|
class="text-2xl font-bold text-yellow-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
<div class="bg-white p-4 rounded-lg border">
|
|
<p
|
|
class="text-sm text-gray-500"
|
|
data-translate-key="totalPVProductionLabel"
|
|
>
|
|
Totale Prodotto da FV
|
|
</p>
|
|
<p
|
|
id="energiaProdotta"
|
|
class="text-2xl font-bold text-green-600"
|
|
>
|
|
-
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analisi Costi Utenze Specifiche -->
|
|
<div
|
|
id="analisi-costi-specifica"
|
|
class="hidden mb-8 pt-6 border-t"
|
|
>
|
|
<h3
|
|
class="text-lg font-semibold text-gray-700 mb-4 text-center"
|
|
data-translate-key="specificUtilitiesCostAnalysisTitle"
|
|
>
|
|
Analisi Costi Utenze Specifiche
|
|
</h3>
|
|
<div id="lista-costi-utenze" class="space-y-3">
|
|
<!-- I costi delle utenze verranno mostrati qui -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grafico Ripartizione Fonti -->
|
|
<div class="pt-6 border-t">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-700 mb-3 text-center"
|
|
data-translate-key="energySourcesBreakdownTitle"
|
|
>
|
|
Ripartizione Fonti Energetiche (Consumo Casa)
|
|
</h3>
|
|
<div
|
|
class="w-full bg-gray-200 rounded-full h-8 overflow-hidden flex"
|
|
>
|
|
<div
|
|
id="barraRete"
|
|
class="bg-red-500 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500"
|
|
style="width: 0%"
|
|
></div>
|
|
<div
|
|
id="barraFV"
|
|
class="bg-green-500 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500"
|
|
style="width: 0%"
|
|
></div>
|
|
</div>
|
|
<div class="flex justify-between mt-2 text-sm">
|
|
<div class="flex items-center">
|
|
<span
|
|
class="w-3 h-3 bg-red-500 rounded-full mr-2"
|
|
></span>
|
|
<span id="percentualeRete">Dalla Rete: -%</span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<span
|
|
class="w-3 h-3 bg-green-500 rounded-full mr-2"
|
|
></span>
|
|
<span id="percentualeFV">Da Fotovoltaico: -%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grafico Incidenza Consumi -->
|
|
<div id="grafico-specifica" class="hidden mt-8 pt-6 border-t">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-700 mb-3 text-center"
|
|
data-translate-key="utilitiesImpactTitle"
|
|
>
|
|
Incidenza Utenze sui Consumi Totali
|
|
</h3>
|
|
<div
|
|
class="w-full bg-gray-200 rounded-full h-8 overflow-hidden flex"
|
|
>
|
|
<div
|
|
id="barraSpecifica"
|
|
class="bg-purple-600 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500"
|
|
style="width: 0%"
|
|
></div>
|
|
<div
|
|
id="barraAltri"
|
|
class="bg-gray-400 h-full flex items-center justify-center text-white text-sm font-medium transition-all duration-500"
|
|
style="width: 0%"
|
|
></div>
|
|
</div>
|
|
<div class="flex justify-between mt-2 text-sm">
|
|
<div class="flex items-center">
|
|
<span
|
|
class="w-3 h-3 bg-purple-600 rounded-full mr-2"
|
|
></span>
|
|
<span id="percentualeSpecifica"
|
|
>Totale Utenze Specifiche: -%</span
|
|
>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<span
|
|
class="w-3 h-3 bg-gray-400 rounded-full mr-2"
|
|
></span>
|
|
<span id="percentualeAltri">Altri Consumi: -%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// --- STATE MANAGEMENT ---
|
|
let specificUtilities = [];
|
|
let utilityGroups = [];
|
|
let currentLanguage = "it";
|
|
let lastCalculationData = null;
|
|
|
|
// --- TRANSLATIONS ---
|
|
const translations = {
|
|
mainTitle: {
|
|
it: "Calcolatore Economico Fotovoltaico",
|
|
en: "Economic Photovoltaic Calculator",
|
|
},
|
|
mainSubtitle: {
|
|
it: "Analizza costi, risparmi e guadagni del tuo impianto solare.",
|
|
en: "Analyze costs, savings, and earnings of your solar system.",
|
|
},
|
|
totalConsumptionLabel: {
|
|
it: "Consumo Totale Casa (kWh)",
|
|
en: "Total Home Consumption (kWh)",
|
|
},
|
|
totalConsumptionPlaceholder: { it: "Es. 450", en: "E.g. 450" },
|
|
gridConsumptionLabel: {
|
|
it: "Consumo dalla Rete (kWh)",
|
|
en: "Consumption from Grid (kWh)",
|
|
},
|
|
gridConsumptionPlaceholder: { it: "Es. 150", en: "E.g. 150" },
|
|
feedInEnergyLabel: {
|
|
it: "Energia Immessa in Rete (kWh)",
|
|
en: "Energy Fed into Grid (kWh)",
|
|
},
|
|
feedInEnergyPlaceholder: { it: "Es. 100", en: "E.g. 100" },
|
|
purchasePriceLabel: {
|
|
it: "Costo Acquisto kWh (€)",
|
|
en: "Purchase Price per kWh (€)",
|
|
},
|
|
purchasePricePlaceholder: { it: "Es. 0.25", en: "E.g. 0.25" },
|
|
sellPriceLabel: {
|
|
it: "Prezzo Vendita kWh (€)",
|
|
en: "Selling Price per kWh (€)",
|
|
},
|
|
sellPricePlaceholder: { it: "Es. 0.10", en: "E.g. 0.10" },
|
|
groupManagementTitle: {
|
|
it: "Gestione Gruppi di Utenze",
|
|
en: "Utility Groups Management",
|
|
},
|
|
newGroupNameLabel: {
|
|
it: "Nome Nuovo Gruppo",
|
|
en: "New Group Name",
|
|
},
|
|
newGroupNamePlaceholder: {
|
|
it: "Es. Utenze Notturne",
|
|
en: "E.g. Night Utilities",
|
|
},
|
|
saveGroupBtn: {
|
|
it: "Salva Lista Corrente come Gruppo",
|
|
en: "Save Current List as Group",
|
|
},
|
|
savedGroupsLabel: { it: "Gruppi Salvati", en: "Saved Groups" },
|
|
loadBtn: { it: "Carica", en: "Load" },
|
|
deleteBtn: { it: "Elimina", en: "Delete" },
|
|
specificUtilitiesTitle: {
|
|
it: "Aggiungi Utenze Specifiche (Opzionale)",
|
|
en: "Add Specific Utilities (Optional)",
|
|
},
|
|
utilityNameLabel: { it: "Nome Utenza", en: "Utility Name" },
|
|
utilityNamePlaceholder: {
|
|
it: "Es. Auto Elettrica",
|
|
en: "E.g. Electric Car",
|
|
},
|
|
consumptionLabel: {
|
|
it: "Consumo (kWh)",
|
|
en: "Consumption (kWh)",
|
|
},
|
|
consumptionPlaceholder: { it: "100", en: "100" },
|
|
addBtn: { it: "Aggiungi", en: "Add" },
|
|
calculateBtn: {
|
|
it: "Calcola Bilancio",
|
|
en: "Calculate Balance",
|
|
},
|
|
economicSummaryTitle: {
|
|
it: "Riepilogo Economico",
|
|
en: "Economic Summary",
|
|
},
|
|
costFromGridLabel: {
|
|
it: "Costo dalla Rete",
|
|
en: "Cost from Grid",
|
|
},
|
|
savingsFromSelfConsumptionLabel: {
|
|
it: "Risparmio Autoconsumo",
|
|
en: "Self-Consumption Savings",
|
|
},
|
|
feedInRevenueLabel: {
|
|
it: "Guadagno Immissione",
|
|
en: "Feed-in Revenue",
|
|
},
|
|
finalBalanceLabel: {
|
|
it: "Bilancio Finale",
|
|
en: "Final Balance",
|
|
},
|
|
energySummaryTitle: {
|
|
it: "Riepilogo Energetico (kWh)",
|
|
en: "Energy Summary (kWh)",
|
|
},
|
|
selfConsumedEnergyLabel: {
|
|
it: "Energia Autoconsumata",
|
|
en: "Self-Consumed Energy",
|
|
},
|
|
energyFromGridLabel: {
|
|
it: "Energia da Rete",
|
|
en: "Energy from Grid",
|
|
},
|
|
fedInEnergySummaryLabel: {
|
|
it: "Energia Immessa",
|
|
en: "Fed-in Energy",
|
|
},
|
|
totalPVProductionLabel: {
|
|
it: "Totale Prodotto da FV",
|
|
en: "Total PV Production",
|
|
},
|
|
specificUtilitiesCostAnalysisTitle: {
|
|
it: "Analisi Costi Utenze Specifiche",
|
|
en: "Specific Utilities Cost Analysis",
|
|
},
|
|
energySourcesBreakdownTitle: {
|
|
it: "Ripartizione Fonti Energetiche (Consumo Casa)",
|
|
en: "Energy Sources Breakdown (Home Consumption)",
|
|
},
|
|
utilitiesImpactTitle: {
|
|
it: "Incidenza Utenze sui Consumi Totali",
|
|
en: "Utilities Impact on Total Consumption",
|
|
},
|
|
noUtilitiesAdded: {
|
|
it: "Nessuna utenza specifica aggiunta.",
|
|
en: "No specific utilities added.",
|
|
},
|
|
utilityNamePlaceholderEdit: {
|
|
it: "Nome Utenza",
|
|
en: "Utility Name",
|
|
},
|
|
saveBtn: { it: "Salva", en: "Save" },
|
|
cancelBtn: { it: "Annulla", en: "Cancel" },
|
|
editBtn: { it: "Modifica", en: "Edit" },
|
|
removeBtn: { it: "Rimuovi", en: "Remove" },
|
|
fromGridLabel: { it: "Dalla Rete", en: "From Grid" },
|
|
fromPVLabel: { it: "Da Fotovoltaico", en: "From PV" },
|
|
totalSpecificUtilitiesLabel: {
|
|
it: "Totale Utenze Specifiche",
|
|
en: "Total Specific Utilities",
|
|
},
|
|
otherConsumptionsLabel: {
|
|
it: "Altri Consumi",
|
|
en: "Other Consumptions",
|
|
},
|
|
totalUtilitiesCostLabel: {
|
|
it: "Costo Totale Utenze",
|
|
en: "Total Utilities Cost",
|
|
},
|
|
selectGroupOption: {
|
|
it: "-- Seleziona un gruppo --",
|
|
en: "-- Select a group --",
|
|
},
|
|
};
|
|
|
|
// --- DOM REFERENCES ---
|
|
const allDomRefs = {
|
|
consumoTotaleInput: document.getElementById("consumoTotale"),
|
|
consumoReteInput: document.getElementById("consumoRete"),
|
|
energiaImmessaInput: document.getElementById("energiaImmessa"),
|
|
costoKwhInput: document.getElementById("costoKwh"),
|
|
prezzoVenditaInput: document.getElementById("prezzoVendita"),
|
|
nomeUtenzaInput: document.getElementById("nomeUtenza"),
|
|
consumoUtenzaInput: document.getElementById("consumoUtenza"),
|
|
addUtenzaBtn: document.getElementById("addUtenzaBtn"),
|
|
listaUtenzeDiv: document.getElementById("listaUtenze"),
|
|
groupNameInput: document.getElementById("groupNameInput"),
|
|
saveGroupBtn: document.getElementById("saveGroupBtn"),
|
|
groupSelect: document.getElementById("groupSelect"),
|
|
loadGroupBtn: document.getElementById("loadGroupBtn"),
|
|
deleteGroupBtn: document.getElementById("deleteGroupBtn"),
|
|
calcolaBtn: document.getElementById("calcolaBtn"),
|
|
risultatiDiv: document.getElementById("risultati"),
|
|
errorMessageDiv: document.getElementById("error-message"),
|
|
costoReteEl: document.getElementById("costoRete"),
|
|
risparmioFVEl: document.getElementById("risparmioFV"),
|
|
guadagnoImmissioneEl:
|
|
document.getElementById("guadagnoImmissione"),
|
|
bilancioFinaleEl: document.getElementById("bilancioFinale"),
|
|
bilancioCardEl: document.getElementById("bilancioCard"),
|
|
energiaFVEl: document.getElementById("energiaFV"),
|
|
energiaReteEl: document.getElementById("energiaRete"),
|
|
energiaImmessaOutEl:
|
|
document.getElementById("energiaImmessaOut"),
|
|
energiaProdottaEl: document.getElementById("energiaProdotta"),
|
|
analisiCostiSpecificaDiv: document.getElementById(
|
|
"analisi-costi-specifica",
|
|
),
|
|
listaCostiUtenzeDiv:
|
|
document.getElementById("lista-costi-utenze"),
|
|
barraReteEl: document.getElementById("barraRete"),
|
|
barraFVEl: document.getElementById("barraFV"),
|
|
percentualeReteEl: document.getElementById("percentualeRete"),
|
|
percentualeFVEl: document.getElementById("percentualeFV"),
|
|
graficoSpecificaDiv:
|
|
document.getElementById("grafico-specifica"),
|
|
barraSpecificaEl: document.getElementById("barraSpecifica"),
|
|
barraAltriEl: document.getElementById("barraAltri"),
|
|
percentualeSpecificaEl: document.getElementById(
|
|
"percentualeSpecifica",
|
|
),
|
|
percentualeAltriEl: document.getElementById("percentualeAltri"),
|
|
langItBtn: document.getElementById("lang-it"),
|
|
langEnBtn: document.getElementById("lang-en"),
|
|
};
|
|
|
|
// Check for missing DOM elements
|
|
const missingElements = [];
|
|
for (const [key, element] of Object.entries(allDomRefs)) {
|
|
if (!element) {
|
|
missingElements.push(key);
|
|
}
|
|
}
|
|
if (missingElements.length > 0) {
|
|
console.warn("Missing DOM elements:", missingElements);
|
|
}
|
|
|
|
// --- FUNCTIONS ---
|
|
const setLanguage = (lang) => {
|
|
if (lang !== "it" && lang !== "en") {
|
|
lang = "en"; // Default to English
|
|
}
|
|
currentLanguage = lang;
|
|
localStorage.setItem("photovoltaicCalculator_language", lang);
|
|
|
|
document.documentElement.lang = lang;
|
|
allDomRefs.langItBtn.classList.toggle("active", lang === "it");
|
|
allDomRefs.langEnBtn.classList.toggle("active", lang === "en");
|
|
|
|
document
|
|
.querySelectorAll("[data-translate-key]")
|
|
.forEach((el) => {
|
|
const key = el.dataset.translateKey;
|
|
if (translations[key] && translations[key][lang]) {
|
|
el.textContent = translations[key][lang];
|
|
}
|
|
});
|
|
|
|
document
|
|
.querySelectorAll("[data-translate-key-placeholder]")
|
|
.forEach((el) => {
|
|
const key = el.dataset.translateKeyPlaceholder;
|
|
if (translations[key] && translations[key][lang]) {
|
|
el.placeholder = translations[key][lang];
|
|
}
|
|
});
|
|
|
|
renderUtilitiesList();
|
|
populateGroupSelect();
|
|
if (
|
|
allDomRefs.risultatiDiv.classList.contains("hidden") ===
|
|
false &&
|
|
lastCalculationData
|
|
) {
|
|
renderResults(lastCalculationData);
|
|
}
|
|
};
|
|
|
|
const renderUtilitiesList = () => {
|
|
const { listaUtenzeDiv } = allDomRefs;
|
|
listaUtenzeDiv.innerHTML = "";
|
|
if (specificUtilities.length === 0) {
|
|
listaUtenzeDiv.innerHTML = `<p class="text-center text-gray-500 text-sm">${translations.noUtilitiesAdded[currentLanguage]}</p>`;
|
|
return;
|
|
}
|
|
|
|
specificUtilities.forEach((utenza, index) => {
|
|
const utenzaEl = document.createElement("div");
|
|
utenzaEl.className = "bg-gray-100 p-3 rounded-lg";
|
|
|
|
if (utenza.isEditing) {
|
|
utenzaEl.innerHTML = `
|
|
<div class="flex flex-col sm:flex-row gap-2">
|
|
<input type="text" value="${utenza.name}" data-type="name" data-index="${index}" class="edit-input-name w-full px-2 py-1 border border-gray-300 rounded-md" placeholder="${translations.utilityNamePlaceholderEdit[currentLanguage]}">
|
|
<input type="number" value="${utenza.consumption}" data-type="consumption" data-index="${index}" class="edit-input-consumption w-full sm:w-32 px-2 py-1 border border-gray-300 rounded-md" placeholder="kWh">
|
|
<div class="flex gap-2 justify-end">
|
|
<button data-index="${index}" class="save-btn action-btn bg-green-500 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">${translations.saveBtn[currentLanguage]}</button>
|
|
<button data-index="${index}" class="cancel-btn action-btn bg-gray-400 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">${translations.cancelBtn[currentLanguage]}</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
utenzaEl.innerHTML = `
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<span class="font-semibold text-gray-800">${utenza.name}</span>:
|
|
<span class="text-gray-600">${utenza.consumption} kWh</span>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button data-index="${index}" class="edit-btn action-btn bg-blue-500 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">${translations.editBtn[currentLanguage]}</button>
|
|
<button data-index="${index}" class="remove-btn action-btn bg-red-500 text-white text-xs font-bold py-1 px-3 rounded-md transition-colors">${translations.removeBtn[currentLanguage]}</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
listaUtenzeDiv.appendChild(utenzaEl);
|
|
});
|
|
};
|
|
|
|
const addUtenza = () => {
|
|
const { nomeUtenzaInput, consumoUtenzaInput } = allDomRefs;
|
|
const name = nomeUtenzaInput.value.trim();
|
|
const consumption = parseFloat(consumoUtenzaInput.value);
|
|
|
|
if (!name || isNaN(consumption) || consumption <= 0) {
|
|
alert(
|
|
"Per favore, inserisci un nome valido e un consumo positivo per l'utenza.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
specificUtilities.push({ name, consumption, isEditing: false });
|
|
nomeUtenzaInput.value = "";
|
|
consumoUtenzaInput.value = "";
|
|
nomeUtenzaInput.focus();
|
|
renderUtilitiesList();
|
|
};
|
|
|
|
const mostraErrore = (messaggio) => {
|
|
allDomRefs.errorMessageDiv.textContent = messaggio;
|
|
allDomRefs.errorMessageDiv.classList.remove("hidden");
|
|
allDomRefs.risultatiDiv.classList.add("hidden");
|
|
};
|
|
|
|
const nascondiErrore = () => {
|
|
allDomRefs.errorMessageDiv.classList.add("hidden");
|
|
};
|
|
|
|
// --- STORAGE FUNCTIONS ---
|
|
const savePricesToStorage = () => {
|
|
if (allDomRefs.costoKwhInput) {
|
|
localStorage.setItem(
|
|
"photovoltaicCalculator_costoKwh",
|
|
allDomRefs.costoKwhInput.value,
|
|
);
|
|
}
|
|
if (allDomRefs.prezzoVenditaInput) {
|
|
localStorage.setItem(
|
|
"photovoltaicCalculator_prezzoVendita",
|
|
allDomRefs.prezzoVenditaInput.value,
|
|
);
|
|
}
|
|
};
|
|
|
|
const loadPricesFromStorage = () => {
|
|
const storedCosto = localStorage.getItem(
|
|
"photovoltaicCalculator_costoKwh",
|
|
);
|
|
const storedPrezzo = localStorage.getItem(
|
|
"photovoltaicCalculator_prezzoVendita",
|
|
);
|
|
if (storedCosto && allDomRefs.costoKwhInput) {
|
|
allDomRefs.costoKwhInput.value = storedCosto;
|
|
}
|
|
if (storedPrezzo && allDomRefs.prezzoVenditaInput) {
|
|
allDomRefs.prezzoVenditaInput.value = storedPrezzo;
|
|
}
|
|
};
|
|
|
|
const saveGroupsToStorage = () => {
|
|
localStorage.setItem(
|
|
"photovoltaicCalculatorGroups",
|
|
JSON.stringify(utilityGroups),
|
|
);
|
|
};
|
|
|
|
const populateGroupSelect = () => {
|
|
const { groupSelect } = allDomRefs;
|
|
groupSelect.innerHTML = `<option value="">${translations.selectGroupOption[currentLanguage]}</option>`;
|
|
utilityGroups.forEach((group) => {
|
|
const option = document.createElement("option");
|
|
option.value = group.groupName;
|
|
option.textContent = group.groupName;
|
|
groupSelect.appendChild(option);
|
|
});
|
|
};
|
|
|
|
const loadGroupsFromStorage = () => {
|
|
try {
|
|
const storedGroups = localStorage.getItem(
|
|
"photovoltaicCalculatorGroups",
|
|
);
|
|
if (storedGroups) {
|
|
utilityGroups = JSON.parse(storedGroups);
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
"Errore nel caricamento dei gruppi dal localStorage:",
|
|
error,
|
|
);
|
|
utilityGroups = [];
|
|
}
|
|
};
|
|
|
|
const renderResults = (data) => {
|
|
const {
|
|
costoReteEl,
|
|
risparmioFVEl,
|
|
guadagnoImmissioneEl,
|
|
bilancioFinaleEl,
|
|
bilancioCardEl,
|
|
energiaFVEl,
|
|
energiaReteEl,
|
|
energiaImmessaOutEl,
|
|
energiaProdottaEl,
|
|
listaCostiUtenzeDiv,
|
|
analisiCostiSpecificaDiv,
|
|
barraReteEl,
|
|
barraFVEl,
|
|
percentualeReteEl,
|
|
percentualeFVEl,
|
|
graficoSpecificaDiv,
|
|
barraSpecificaEl,
|
|
barraAltriEl,
|
|
percentualeSpecificaEl,
|
|
percentualeAltriEl,
|
|
} = allDomRefs;
|
|
|
|
const {
|
|
costoDaRete,
|
|
risparmioDaFV,
|
|
guadagnoDaImmissione,
|
|
bilancioFinale,
|
|
energiaAutoconsumata,
|
|
consumoRete,
|
|
energiaImmessa,
|
|
energiaProdottaTotale,
|
|
costoMedioKwhConsumato,
|
|
totalSpecificConsumption,
|
|
altriConsumi,
|
|
} = data;
|
|
|
|
const locale = currentLanguage === "it" ? "it-IT" : "en-US";
|
|
|
|
costoReteEl.textContent = `${costoDaRete.toLocaleString(locale, { style: "currency", currency: "EUR" })}`;
|
|
risparmioFVEl.textContent = `${risparmioDaFV.toLocaleString(locale, { style: "currency", currency: "EUR" })}`;
|
|
guadagnoImmissioneEl.textContent = `${guadagnoDaImmissione.toLocaleString(locale, { style: "currency", currency: "EUR" })}`;
|
|
bilancioFinaleEl.textContent = `${bilancioFinale.toLocaleString(locale, { style: "currency", currency: "EUR" })}`;
|
|
|
|
bilancioCardEl.classList.toggle(
|
|
"border-green-400",
|
|
bilancioFinale >= 0,
|
|
);
|
|
bilancioCardEl.classList.toggle(
|
|
"border-red-400",
|
|
bilancioFinale < 0,
|
|
);
|
|
bilancioFinaleEl.classList.toggle(
|
|
"text-green-600",
|
|
bilancioFinale >= 0,
|
|
);
|
|
bilancioFinaleEl.classList.toggle(
|
|
"text-red-600",
|
|
bilancioFinale < 0,
|
|
);
|
|
|
|
energiaFVEl.textContent = `${energiaAutoconsumata.toFixed(2)} kWh`;
|
|
energiaReteEl.textContent = `${consumoRete.toFixed(2)} kWh`;
|
|
energiaImmessaOutEl.textContent = `${energiaImmessa.toFixed(2)} kWh`;
|
|
energiaProdottaEl.textContent = `${energiaProdottaTotale.toFixed(2)} kWh`;
|
|
|
|
if (specificUtilities.length > 0) {
|
|
listaCostiUtenzeDiv.innerHTML = "";
|
|
let costoTotaleUtenzeSpecifiche = 0;
|
|
|
|
specificUtilities.forEach((utenza) => {
|
|
const costoUtenza =
|
|
utenza.consumption * costoMedioKwhConsumato;
|
|
costoTotaleUtenzeSpecifiche += costoUtenza;
|
|
const costoEl = document.createElement("div");
|
|
costoEl.className =
|
|
"flex justify-between items-center bg-white p-3 rounded-lg border";
|
|
costoEl.innerHTML = `
|
|
<span class="font-medium text-gray-800">${utenza.name}</span>
|
|
<span class="font-bold text-purple-700">${costoUtenza.toLocaleString(locale, { style: "currency", currency: "EUR" })}</span>
|
|
`;
|
|
listaCostiUtenzeDiv.appendChild(costoEl);
|
|
});
|
|
|
|
const totaleCostoEl = document.createElement("div");
|
|
totaleCostoEl.className =
|
|
"flex justify-between items-center bg-gray-200 p-3 rounded-lg border-t-2 border-gray-300 mt-3";
|
|
totaleCostoEl.innerHTML = `
|
|
<span class="font-bold text-gray-800">${translations.totalUtilitiesCostLabel[currentLanguage]}</span>
|
|
<span class="font-extrabold text-purple-800">${costoTotaleUtenzeSpecifiche.toLocaleString(locale, { style: "currency", currency: "EUR" })}</span>
|
|
`;
|
|
listaCostiUtenzeDiv.appendChild(totaleCostoEl);
|
|
|
|
analisiCostiSpecificaDiv.classList.remove("hidden");
|
|
} else {
|
|
analisiCostiSpecificaDiv.classList.add("hidden");
|
|
}
|
|
|
|
const percRete =
|
|
data.consumoTotale > 0
|
|
? (consumoRete / data.consumoTotale) * 100
|
|
: 0;
|
|
const percFV =
|
|
data.consumoTotale > 0
|
|
? (energiaAutoconsumata / data.consumoTotale) * 100
|
|
: 0;
|
|
barraReteEl.style.width = `${percRete}%`;
|
|
barraFVEl.style.width = `${percFV}%`;
|
|
barraReteEl.textContent =
|
|
percRete > 10 ? `${percRete.toFixed(1)}%` : "";
|
|
barraFVEl.textContent =
|
|
percFV > 10 ? `${percFV.toFixed(1)}%` : "";
|
|
percentualeReteEl.textContent = `${translations.fromGridLabel[currentLanguage]}: ${percRete.toFixed(1)}%`;
|
|
percentualeFVEl.textContent = `${translations.fromPVLabel[currentLanguage]}: ${percFV.toFixed(1)}%`;
|
|
|
|
if (totalSpecificConsumption > 0) {
|
|
graficoSpecificaDiv.classList.remove("hidden");
|
|
const percSpecifica =
|
|
(totalSpecificConsumption / data.consumoTotale) * 100;
|
|
const percAltri = (altriConsumi / data.consumoTotale) * 100;
|
|
|
|
barraSpecificaEl.style.width = `${percSpecifica}%`;
|
|
barraAltriEl.style.width = `${percAltri}%`;
|
|
barraSpecificaEl.textContent =
|
|
percSpecifica > 10
|
|
? `${percSpecifica.toFixed(1)}%`
|
|
: "";
|
|
barraAltriEl.textContent =
|
|
percAltri > 10 ? `${percAltri.toFixed(1)}%` : "";
|
|
percentualeSpecificaEl.textContent = `${translations.totalSpecificUtilitiesLabel[currentLanguage]}: ${percSpecifica.toFixed(1)}% (${totalSpecificConsumption.toFixed(1)} kWh)`;
|
|
percentualeAltriEl.textContent = `${translations.otherConsumptionsLabel[currentLanguage]}: ${percAltri.toFixed(1)}% (${altriConsumi.toFixed(1)} kWh)`;
|
|
} else {
|
|
graficoSpecificaDiv.classList.add("hidden");
|
|
}
|
|
};
|
|
|
|
// --- EVENT LISTENERS ---
|
|
if (allDomRefs.addUtenzaBtn) {
|
|
allDomRefs.addUtenzaBtn.addEventListener("click", addUtenza);
|
|
}
|
|
if (allDomRefs.costoKwhInput) {
|
|
allDomRefs.costoKwhInput.addEventListener(
|
|
"input",
|
|
savePricesToStorage,
|
|
);
|
|
}
|
|
if (allDomRefs.prezzoVenditaInput) {
|
|
allDomRefs.prezzoVenditaInput.addEventListener(
|
|
"input",
|
|
savePricesToStorage,
|
|
);
|
|
}
|
|
if (allDomRefs.langItBtn) {
|
|
allDomRefs.langItBtn.addEventListener("click", () =>
|
|
setLanguage("it"),
|
|
);
|
|
}
|
|
if (allDomRefs.langEnBtn) {
|
|
allDomRefs.langEnBtn.addEventListener("click", () =>
|
|
setLanguage("en"),
|
|
);
|
|
}
|
|
|
|
if (allDomRefs.listaUtenzeDiv) {
|
|
allDomRefs.listaUtenzeDiv.addEventListener("click", (e) => {
|
|
const button = e.target.closest("button");
|
|
if (!button) return;
|
|
|
|
const index = parseInt(button.dataset.index, 10);
|
|
|
|
if (button.classList.contains("remove-btn")) {
|
|
specificUtilities.splice(index, 1);
|
|
} else if (button.classList.contains("edit-btn")) {
|
|
specificUtilities.forEach(
|
|
(u, i) => (u.isEditing = i === index),
|
|
);
|
|
} else if (button.classList.contains("cancel-btn")) {
|
|
specificUtilities[index].isEditing = false;
|
|
} else if (button.classList.contains("save-btn")) {
|
|
const parentDiv = button.closest(
|
|
".flex-col, .flex-row",
|
|
);
|
|
const newName = parentDiv
|
|
.querySelector(".edit-input-name")
|
|
.value.trim();
|
|
const newConsumption = parseFloat(
|
|
parentDiv.querySelector(".edit-input-consumption")
|
|
.value,
|
|
);
|
|
|
|
if (
|
|
newName &&
|
|
!isNaN(newConsumption) &&
|
|
newConsumption > 0
|
|
) {
|
|
specificUtilities[index].name = newName;
|
|
specificUtilities[index].consumption =
|
|
newConsumption;
|
|
specificUtilities[index].isEditing = false;
|
|
} else {
|
|
alert(
|
|
"Per favore, inserisci dati validi prima di salvare.",
|
|
);
|
|
}
|
|
}
|
|
renderUtilitiesList();
|
|
});
|
|
}
|
|
|
|
// Group Event Listeners
|
|
if (allDomRefs.saveGroupBtn) {
|
|
allDomRefs.saveGroupBtn.addEventListener("click", () => {
|
|
const groupName = allDomRefs.groupNameInput.value.trim();
|
|
if (!groupName) {
|
|
alert("Per favore, inserisci un nome per il gruppo.");
|
|
return;
|
|
}
|
|
if (specificUtilities.length === 0) {
|
|
alert(
|
|
"Aggiungi almeno un'utenza alla lista prima di salvare un gruppo.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const existingGroupIndex = utilityGroups.findIndex(
|
|
(g) => g.groupName === groupName,
|
|
);
|
|
if (existingGroupIndex !== -1) {
|
|
if (
|
|
!confirm(
|
|
`Un gruppo con nome "${groupName}" esiste già. Vuoi sovrascriverlo?`,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const cleanUtilities = specificUtilities.map(
|
|
({ name, consumption }) => ({ name, consumption }),
|
|
);
|
|
const newGroup = { groupName, utilities: cleanUtilities };
|
|
|
|
if (existingGroupIndex !== -1) {
|
|
utilityGroups[existingGroupIndex] = newGroup;
|
|
} else {
|
|
utilityGroups.push(newGroup);
|
|
}
|
|
|
|
saveGroupsToStorage();
|
|
populateGroupSelect();
|
|
allDomRefs.groupSelect.value = groupName;
|
|
allDomRefs.groupNameInput.value = "";
|
|
alert(`Gruppo "${groupName}" salvato con successo!`);
|
|
});
|
|
}
|
|
|
|
if (allDomRefs.loadGroupBtn) {
|
|
allDomRefs.loadGroupBtn.addEventListener("click", () => {
|
|
const selectedGroupName = allDomRefs.groupSelect.value;
|
|
if (!selectedGroupName) {
|
|
alert("Per favore, seleziona un gruppo da caricare.");
|
|
return;
|
|
}
|
|
|
|
const groupToLoad = utilityGroups.find(
|
|
(g) => g.groupName === selectedGroupName,
|
|
);
|
|
if (groupToLoad) {
|
|
specificUtilities = groupToLoad.utilities.map((u) => ({
|
|
...u,
|
|
isEditing: false,
|
|
}));
|
|
renderUtilitiesList();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (allDomRefs.deleteGroupBtn) {
|
|
allDomRefs.deleteGroupBtn.addEventListener("click", () => {
|
|
const selectedGroupName = allDomRefs.groupSelect.value;
|
|
if (!selectedGroupName) {
|
|
alert("Per favore, seleziona un gruppo da eliminare.");
|
|
return;
|
|
}
|
|
|
|
if (
|
|
confirm(
|
|
`Sei sicuro di voler eliminare il gruppo "${selectedGroupName}"?`,
|
|
)
|
|
) {
|
|
utilityGroups = utilityGroups.filter(
|
|
(g) => g.groupName !== selectedGroupName,
|
|
);
|
|
saveGroupsToStorage();
|
|
populateGroupSelect();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (allDomRefs.calcolaBtn) {
|
|
allDomRefs.calcolaBtn.addEventListener("click", () => {
|
|
// 1. Read and Validate Inputs
|
|
const consumoTotale = parseFloat(
|
|
allDomRefs.consumoTotaleInput.value,
|
|
);
|
|
const consumoRete = parseFloat(
|
|
allDomRefs.consumoReteInput.value,
|
|
);
|
|
const energiaImmessa = parseFloat(
|
|
allDomRefs.energiaImmessaInput.value,
|
|
);
|
|
const costoKwh = parseFloat(allDomRefs.costoKwhInput.value);
|
|
const costoFisso = 0; // Fixed costs not implemented in this version
|
|
const prezzoVendita = parseFloat(
|
|
allDomRefs.prezzoVenditaInput.value,
|
|
);
|
|
|
|
if (
|
|
[
|
|
consumoTotale,
|
|
consumoRete,
|
|
energiaImmessa,
|
|
costoKwh,
|
|
prezzoVendita,
|
|
].some(isNaN) ||
|
|
consumoTotale <= 0 ||
|
|
costoKwh < 0 ||
|
|
prezzoVendita < 0
|
|
) {
|
|
mostraErrore(
|
|
"Inserire valori validi. I consumi totali devono essere > 0. I prezzi non possono essere negativi.",
|
|
);
|
|
return;
|
|
}
|
|
if (consumoRete < 0 || energiaImmessa < 0) {
|
|
mostraErrore(
|
|
"I valori di consumo/immissione non possono essere negativi.",
|
|
);
|
|
return;
|
|
}
|
|
if (consumoRete > consumoTotale) {
|
|
mostraErrore(
|
|
"Il consumo dalla rete non può superare il consumo totale della casa.",
|
|
);
|
|
return;
|
|
}
|
|
const totalSpecificConsumption = specificUtilities.reduce(
|
|
(sum, u) => sum + u.consumption,
|
|
0,
|
|
);
|
|
if (totalSpecificConsumption > consumoTotale) {
|
|
mostraErrore(
|
|
"La somma dei consumi delle utenze specifiche non può superare il consumo totale della casa.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
nascondiErrore();
|
|
|
|
// 2. Core Calculations
|
|
const energiaAutoconsumata = consumoTotale - consumoRete;
|
|
const energiaProdottaTotale =
|
|
energiaAutoconsumata + energiaImmessa;
|
|
|
|
// Variable costs only (excluding fixed costs for savings calculation)
|
|
const costoDaRete = consumoRete * costoKwh;
|
|
const risparmioDaFV = energiaAutoconsumata * costoKwh; // Only variable cost savings
|
|
const guadagnoDaImmissione = energiaImmessa * prezzoVendita;
|
|
|
|
// Net financial benefit (excluding fixed costs which don't change)
|
|
const bilancioFinale =
|
|
risparmioDaFV + guadagnoDaImmissione - costoDaRete;
|
|
|
|
// Total cost without PV (for comparison)
|
|
const costoTotaleSenzaFV =
|
|
consumoTotale * costoKwh + costoFisso;
|
|
|
|
// Average cost per kWh including fixed costs proportionally
|
|
const costoMedioKwhConsumato =
|
|
consumoTotale > 0
|
|
? (costoDaRete + costoFisso) / consumoTotale
|
|
: 0;
|
|
const altriConsumi =
|
|
consumoTotale - totalSpecificConsumption;
|
|
|
|
lastCalculationData = {
|
|
consumoTotale,
|
|
consumoRete,
|
|
energiaImmessa,
|
|
costoKwh,
|
|
costoFisso,
|
|
prezzoVendita,
|
|
energiaAutoconsumata,
|
|
energiaProdottaTotale,
|
|
costoDaRete,
|
|
risparmioDaFV,
|
|
guadagnoDaImmissione,
|
|
bilancioFinale,
|
|
costoMedioKwhConsumato,
|
|
costoTotaleSenzaFV,
|
|
totalSpecificConsumption,
|
|
altriConsumi,
|
|
};
|
|
|
|
// 3. Render Results
|
|
renderResults(lastCalculationData);
|
|
if (allDomRefs.risultatiDiv) {
|
|
allDomRefs.risultatiDiv.classList.remove("hidden");
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- INITIAL RENDER ---
|
|
// Script is at bottom of body, so DOM is already loaded
|
|
loadGroupsFromStorage();
|
|
loadPricesFromStorage();
|
|
|
|
const savedLang = localStorage.getItem(
|
|
"photovoltaicCalculator_language",
|
|
);
|
|
const browserLang = navigator.language.split("-")[0];
|
|
setLanguage(savedLang || browserLang);
|
|
|
|
renderUtilitiesList();
|
|
</script>
|
|
</body>
|
|
</html>
|