| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Flood Risk and Resource Allocator</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"> <style> body { font-family: 'Inter', sans-serif; background-color: #f7fafc; } .container { max-width: 1024px; } .card { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .custom-scrollbar::-webkit-scrollbar { width: 8px; } .custom-scrollbar::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; } .custom-scrollbar::-webkit-scrollbar-track { background-color: #f1f5f9; } </style> </head> <body class="p-4 sm:p-8"> <div id="app" class="container mx-auto"> <h1 class="text-3xl sm:text-4xl font-extrabold text-gray-800 mb-6 border-b-4 border-blue-500 pb-2"> Flood Risk & Resource Allocator </h1> <p id="user-id-display" class="text-sm text-gray-600 mb-4"></p> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> <div class="card bg-white p-5 rounded-xl lg:col-span-2"> <h2 class="text-xl font-semibold text-gray-700 mb-4">Scenario Parameters</h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="scenarioName" class="block text-sm font-medium text-gray-700">Scenario Name</label> <input type="text" id="scenarioName" value="Coastal Zone 2025" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border"> </div> <div> <label for="totalBudget" class="block text-sm font-medium text-gray-700">Total Mitigation Budget (USD)</label> <input type="number" id="totalBudget" value="1000000" min="0" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border"> </div> </div> <h3 class="text-lg font-semibold text-gray-700 mt-6 mb-3 border-t pt-4">Risk Areas</h3> <div id="riskAreasContainer" class="space-y-3 custom-scrollbar max-h-96 overflow-y-auto pr-2"> </div> <button onclick="addArea()" class="mt-4 w-full sm:w-auto bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-lg transition duration-150"> + Add New Risk Area </button> </div> <div class="card bg-white p-5 rounded-xl"> <h2 class="text-xl font-semibold text-gray-700 mb-4">Controls & Storage</h2> <div class="space-y-3"> <button onclick="calculateAllocation()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition duration-150 transform hover:scale-[1.02]"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v5a1 1 0 102 0V7z" clip-rule="evenodd" /> </svg> Calculate Allocation </button> <button onclick="saveScenario()" class="w-full bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 rounded-lg transition duration-150"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" viewBox="0 0 20 20" fill="currentColor"> <path d="M5 4a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2V6a2 2 0 00-2-2H5zm0 2h10v6H5V6zm1 1h8v1H6V7zm0 2h8v1H6V9z" /> </svg> Save Scenario </button> </div> <h3 class="text-lg font-semibold text-gray-700 mt-6 mb-3 border-t pt-4">Saved Scenarios</h3> <div id="savedScenariosList" class="space-y-2 custom-scrollbar max-h-72 overflow-y-auto pr-2"> <p class="text-sm text-gray-500">Loading saved data...</p> </div> </div> </div> <div id="resultsSection" class="card bg-white p-5 rounded-xl hidden"> <h2 class="text-2xl font-bold text-gray-800 mb-4">Allocation Results</h2> <div class="overflow-x-auto"> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-blue-50"> <tr> <th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Area</th> <th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Risk Level</th> <th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Risk Score (Weight)</th> <th class="px-3 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Allocated Budget</th> <th class="px-3 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Percentage</th> </tr> </thead> <tbody id="resultsBody" class="bg-white divide-y divide-gray-200"> </tbody> <tfoot id="resultsFooter" class="bg-blue-100 font-bold"> </tfoot> </table> </div> <div id="summaryText" class="mt-4 text-sm text-gray-700"></div> </div> <div id="messageModal" class="fixed inset-0 bg-gray-600 bg-opacity-75 hidden flex items-center justify-center p-4 z-50"> <div class="bg-white rounded-xl p-6 w-full max-w-sm card"> <h3 id="modalTitle" class="text-lg font-bold mb-3 text-gray-800"></h3> <p id="modalMessage" class="text-sm text-gray-600 mb-4"></p> <button onclick="hideModal()" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 rounded-lg transition duration-150"> OK </button> </div> </div> </div> <script type="module"> import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"; import { getFirestore, doc, addDoc, setDoc, onSnapshot, collection, query, orderBy, deleteDoc, getDoc, updateDoc } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"; import { setLogLevel } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"; // Global Firebase variables const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}'); const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; let app, db, auth, userId = null; let isAuthReady = false; setLogLevel('Debug'); // Risk score weights const RISK_WEIGHTS = { 'Low': 1, 'Medium': 3, 'High': 5 }; // --- Utility Functions --- /** Shows the custom modal with a message. */ window.showModal = function(title, message) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalMessage').textContent = message; document.getElementById('messageModal').classList.remove('hidden'); } /** Hides the custom modal. */ window.hideModal = function() { document.getElementById('messageModal').classList.add('hidden'); } /** Formats a number as currency. */ const formatCurrency = (amount) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(amount); } // --- Core App Logic: UI Management --- /** Generates the HTML for a single risk area input row. */ const createAreaInputHTML = (index, name = Area ${index + 1}, risk = 'Medium') => { return <div id="area-${index}" class="flex flex-col sm:flex-row gap-3 items-end p-3 border border-gray-200 rounded-lg bg-gray-50 area-input-row" data-index="${index}"> <div class="flex-grow w-full"> <label for="areaName-${index}" class="block text-xs font-medium text-gray-500">Area Name</label> <input type="text" id="areaName-${index}" value="${name}" placeholder="e.g., Downtown Core" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 border text-sm"> </div> <div class="w-full sm:w-40"> <label for="areaRisk-${index}" class="block text-xs font-medium text-gray-500">Perceived Risk</label> <select id="areaRisk-${index}" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 border text-sm"> <option value="Low" ${risk === 'Low' ? 'selected' : ''}>Low (1)</option> <option value="Medium" ${risk === 'Medium' ? 'selected' : ''}>Medium (3)</option> <option value="High" ${risk === 'High' ? 'selected' : ''}>High (5)</option> </select> </div> <button onclick="removeArea(${index})" class="flex-shrink-0 w-full sm:w-auto bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-3 rounded-lg transition duration-150"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 10-2 0v6a1 1 0 102 0V8z" clip-rule="evenodd" /> </svg> </button> </div> ; }; /** Adds a new area input row to the container. */ window.addArea = function(name, risk) { const container = document.getElementById('riskAreasContainer'); const newIndex = container.querySelectorAll('.area-input-row').length; container.insertAdjacentHTML('beforeend', createAreaInputHTML(newIndex, name, risk)); // Re-index all rows after adding reIndexAreas(); } /** Removes an area input row. */ window.removeArea = function(indexToRemove) { const element = document.getElementById(area-${indexToRemove}); if (element) { element.remove(); reIndexAreas(); calculateAllocation(); // Recalculate immediately } } /** Ensures all area rows have correct, sequential indices. */ const reIndexAreas = () => { const rows = document.querySelectorAll('#riskAreasContainer .area-input-row'); rows.forEach((row, index) => { row.id = area-${index}; row.setAttribute('data-index', index); const nameInput = row.querySelector('input[type="text"]'); nameInput.id = areaName-${index}; const riskSelect = row.querySelector('select'); riskSelect.id = areaRisk-${index}; const removeButton = row.querySelector('button'); removeButton.setAttribute('onclick', removeArea(${index})); }); }; /** Reads all data from the input fields. */ const getScenarioData = () => { const scenarioName = document.getElementById('scenarioName').value || 'Untitled Scenario'; const totalBudget = parseFloat(document.getElementById('totalBudget').value) || 0; const areas = []; const rows = document.querySelectorAll('#riskAreasContainer .area-input-row'); rows.forEach(row => { const index = row.getAttribute('data-index'); const name = document.getElementById(areaName-${index}).value || Area ${parseInt(index) + 1}; const risk = document.getElementById(areaRisk-${index}).value || 'Medium'; areas.push({ name, risk, score: RISK_WEIGHTS[risk] }); }); return { scenarioName, totalBudget, areas }; }; /** Populates the input fields with data from a loaded scenario. */ const loadScenarioData = (scenario) => { document.getElementById('scenarioName').value = scenario.scenarioName; document.getElementById('totalBudget').value = scenario.totalBudget; const container = document.getElementById('riskAreasContainer'); container.innerHTML = ''; // Clear existing areas scenario.areas.forEach((area, index) => { addArea(area.name, area.risk); }); calculateAllocation(); showModal("Scenario Loaded", Successfully loaded scenario: ${scenario.scenarioName}); }; // --- Core App Logic: Calculation --- /** Calculates the weighted resource allocation. */ window.calculateAllocation = function() { const { scenarioName, totalBudget, areas } = getScenarioData(); const resultsBody = document.getElementById('resultsBody'); const resultsFooter = document.getElementById('resultsFooter'); const resultsSection = document.getElementById('resultsSection'); const summaryText = document.getElementById('summaryText'); resultsBody.innerHTML = ''; resultsFooter.innerHTML = ''; if (areas.length === 0) { resultsSection.classList.add('hidden'); showModal("Input Required", "Please add at least one risk area to calculate allocation."); return; } if (totalBudget <= 0) { resultsSection.classList.add('hidden'); showModal("Budget Required", "Please enter a total mitigation budget greater than zero."); return; } // 1. Calculate Total Risk Score (Sum of Weights) const totalRiskScore = areas.reduce((sum, area) => sum + area.score, 0); let allocatedResults = []; let totalAllocated = 0; // 2. Calculate Allocation for Each Area areas.forEach(area => { const allocationRatio = area.score / totalRiskScore; const allocatedBudget = totalBudget * allocationRatio; totalAllocated += allocatedBudget; allocatedResults.push({ ...area, ratio: allocationRatio, allocatedBudget: allocatedBudget }); }); // 3. Display Results allocatedResults.forEach(result => { const percentage = (result.ratio * 100).toFixed(1); const row = <tr class="hover:bg-gray-50 transition duration-100"> <td class="px-3 py-3 whitespace-nowrap text-sm font-medium text-gray-900">${result.name}</td> <td class="px-3 py-3 whitespace-nowrap text-sm text-gray-500"> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${result.risk === 'High' ? 'bg-red-100 text-red-800' : result.risk === 'Medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'}"> ${result.risk} </span> </td> <td class="px-3 py-3 whitespace-nowrap text-sm text-gray-500">${result.score} / ${totalRiskScore}</td> <td class="px-3 py-3 whitespace-nowrap text-sm font-semibold text-right text-blue-600">${formatCurrency(result.allocatedBudget)}</td> <td class="px-3 py-3 whitespace-nowrap text-sm text-right text-gray-700">${percentage}%</td> </tr> ; resultsBody.insertAdjacentHTML('beforeend', row); }); // 4. Display Summary Footer const footerRow = <tr> <td colspan="3" class="px-3 py-3 text-right text-base">Total Budget / Allocated:</td> <td class="px-3 py-3 text-base text-right text-green-700">${formatCurrency(totalBudget)}</td> <td class="px-3 py-3 text-base text-right text-gray-700">100.0%</td> </tr> ; resultsFooter.insertAdjacentHTML('beforeend', footerRow); summaryText.textContent = The total budget of ${formatCurrency(totalBudget)} has been allocated across ${areas.length} areas based on a total weighted risk score of ${totalRiskScore}.; resultsSection.classList.remove('hidden'); } // --- Firestore Integration --- /** Saves the current scenario to Firestore. */ window.saveScenario = async function() { if (!userId) { showModal("Error", "Authentication is not ready. Please wait a moment."); return; } const scenario = getScenarioData(); if (scenario.areas.length === 0 || scenario.totalBudget <= 0) { showModal("Input Error", "Cannot save: Please ensure you have at least one area and a valid total budget."); return; } const scenarioData = { scenarioName: scenario.scenarioName, totalBudget: scenario.totalBudget, areas: scenario.areas, totalRiskScore: scenario.areas.reduce((sum, area) => sum + area.score, 0), createdAt: new Date().toISOString(), }; const path = artifacts/${appId}/users/${userId}/flood_scenarios; const scenariosCollection = collection(db, path); try { const docRef = await addDoc(scenariosCollection, scenarioData); showModal("Save Successful", Scenario "${scenario.scenarioName}" saved with ID: ${docRef.id}); } catch (error) { console.error("Error saving document: ", error); showModal("Save Error", "Could not save scenario. Check console for details."); } } /** Sets up real-time listener for saved scenarios. */ const setupScenarioListener = () => { if (!db || !userId) return; const path = artifacts/${appId}/users/${userId}/flood_scenarios; const scenariosCollection = collection(db, path); // Sort by creation date descending const q = query(scenariosCollection); // Removed orderBy('createdAt') to avoid index requirements const scenariosList = document.getElementById('savedScenariosList'); scenariosList.innerHTML = ''; onSnapshot(q, (snapshot) => { scenariosList.innerHTML = ''; if (snapshot.empty) { scenariosList.innerHTML = '<p class="text-sm text-gray-500">No scenarios saved yet. Save one above!</p>'; return; } snapshot.docs.forEach(doc => { const scenario = doc.data(); const docId = doc.id; const date = new Date(scenario.createdAt).toLocaleDateString(); const item = <div class="flex justify-between items-center p-3 bg-gray-100 rounded-lg hover:bg-gray-200 transition duration-150"> <div class="text-sm"> <p class="font-semibold text-gray-800">${scenario.scenarioName}</p> <p class="text-xs text-gray-500">${formatCurrency(scenario.totalBudget)} | Saved: ${date}</p> </div> <div class="flex space-x-2"> <button onclick="window.loadScenarioById('${docId}')" title="Load Scenario" class="text-blue-600 hover:text-blue-800 transition duration-150 p-1"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <path d="M10 2a8 8 0 100 16 8 8 0 000-16zm-3.707 9.293a1 1 0 000 1.414L8.586 14a1 1 0 001.414 0l3.293-3.293a1 1 0 00-1.414-1.414L10 11.586 7.414 9.293a1 1 0 00-1.414 0z" /> </svg> </button> <button onclick="window.deleteScenarioById('${docId}')" title="Delete Scenario" class="text-red-500 hover:text-red-700 transition duration-150 p-1"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 10-2 0v6a1 1 0 102 0V8z" clip-rule="evenodd" /> </svg> </button> </div> </div> ; scenariosList.insertAdjacentHTML('beforeend', item); }); }, (error) => { console.error("Firestore Snapshot Error: ", error); scenariosList.innerHTML = '<p class="text-sm text-red-500">Error loading scenarios. Check console.</p>'; }); }; /** Loads a scenario by its Firestore document ID. */ window.loadScenarioById = async function(docId) { if (!userId) return; const path = artifacts/${appId}/users/${userId}/flood_scenarios/${docId}; const docRef = doc(db, path); try { const docSnap = await getDoc(docRef); if (docSnap.exists()) { loadScenarioData(docSnap.data()); } else { showModal("Error", "Scenario not found."); } } catch (error) { console.error("Error loading document: ", error); showModal("Load Error", "Could not load scenario. Check console for details."); } } /** Deletes a scenario by its Firestore document ID. */ window.deleteScenarioById = async function(docId) { if (!userId) return; const path = artifacts/${appId}/users/${userId}/flood_scenarios/${docId}; const docRef = doc(db, path); // Using modal for confirmation instead of window.confirm const confirmDelete = () => { deleteDoc(docRef) .then(() => { showModal("Deleted", "Scenario successfully deleted."); }) .catch((error) => { console.error("Error removing document: ", error); showModal("Delete Error", "Could not delete scenario. Check console for details."); }); document.getElementById('messageModal').classList.add('hidden'); // Hide modal after confirmation logic }; const modal = document.getElementById('messageModal'); document.getElementById('modalTitle').textContent = "Confirm Deletion"; document.getElementById('modalMessage').textContent = "Are you sure you want to delete this scenario? This action cannot be undone."; // Change modal button to 'Confirm Delete' and add a Cancel button const okButton = modal.querySelector('button'); okButton.textContent = 'Confirm Delete'; okButton.onclick = confirmDelete; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.className = 'w-full mt-2 bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 rounded-lg transition duration-150'; cancelButton.onclick = window.hideModal; modal.querySelector('.bg-white').appendChild(cancelButton); modal.classList.remove('hidden'); // Reset modal on hide (to remove the extra cancel button for next time) const originalHideModal = window.hideModal; window.hideModal = () => { // Clean up the temporary cancel button and reset the OK button's function if(cancelButton && modal.contains(cancelButton)) { cancelButton.remove(); } okButton.textContent = 'OK'; okButton.onclick = originalHideModal; originalHideModal(); }; } // --- Firebase Initialization and Auth --- document.addEventListener('DOMContentLoaded', () => { // Add initial areas addArea("Neighborhood A", "High"); addArea("Industrial Park", "Medium"); addArea("Coastal Road", "Low"); calculateAllocation(); // Calculate initial state // Check if Firebase config is available if (Object.keys(firebaseConfig).length === 0) { console.error("Firebase configuration is missing. App cannot save/load data."); document.getElementById('savedScenariosList').innerHTML = '<p class="text-sm text-red-500">Local Firebase config missing. Saving/loading is disabled.</p>'; return; } // Initialize Firebase App app = initializeApp(firebaseConfig); db = getFirestore(app); auth = getAuth(app); // Authentication Listener onAuthStateChanged(auth, (user) => { if (user) { userId = user.uid; document.getElementById('user-id-display').textContent = User ID: ${userId}; isAuthReady = true; // Start listening for scenarios once authenticated setupScenarioListener(); } else { console.log("User logged out or failed authentication. Attempting sign-in."); } }); // Sign In Function const signIn = async () => { try { if (initialAuthToken) { await signInWithCustomToken(auth, initialAuthToken); } else { await signInAnonymously(auth); } } catch (error) { console.error("Firebase Auth Error:", error); showModal("Authentication Failed", "Could not sign in to the database. Data saving is disabled."); } }; signIn(); // Make functions available globally window.addArea = addArea; window.removeArea = removeArea; window.calculateAllocation = calculateAllocation; window.saveScenario = saveScenario; window.loadScenarioById = loadScenarioById; window.deleteScenarioById = deleteScenarioById; }); </script> </body> </html> |