How to create a Voting (Election) Portal App using Google AI for free

Prompt used

‎You are an expert full-stack developer specializing in creating production-ready web applications using vanilla JavaScript, HTML, CSS, and Firebase for the backend.
‎Your task is to generate a complete implementation for a university election voting portal web app. The application must be fully responsive for both mobile and desktop use and must adhere strictly to the constraints outlined below.
‎--- Core Tech Stack and Constraints ---
‎Frontend: Pure vanilla JavaScript (ES6 Modules). You must not use any frameworks or libraries like React, Vue, Angular, or jQuery. All DOM manipulation, event handling, and asynchronous operations must be handled with standard browser APIs.
‎Structure: Use HTML5 for structure and CSS3 for styling. All CSS and JavaScript code must be inline within the respective HTML files (i.e., inside <style> and <script type="module"> tags). Do not generate external .css or .js files.
‎Styling: The design should be clean, professional, and fully responsive using modern CSS like Flexbox and Media Queries.
‎Backend: Firebase for Web (SDK v10+).
‎Authentication: Firebase Authentication (Email/Password provider).
‎Database: Firestore.
‎Prohibited Services: You must not use Firebase Cloud Functions, Firebase Storage, or Firebase Cloud Messaging. All logic must be client-side, secured by Firestore Security Rules.
‎--- Application Structure and Features ---
‎The application consists of two main files: index.html for voters and admin.html for administrators.
‎1. Voter Portal (index.html)
‎Login Screen:
‎Inputs for Matriculation Number and Password.
‎Login logic should convert the matric number (e.g., 2023001) into an email format (2023001@uni.local) to authenticate with Firebase Auth.
‎Include a small, unobtrusive icon that links to admin.html.
‎Password Change Screen:
‎Users must be forced to change their password on first login. This is controlled by a passwordChanged flag in their Firestore document.
‎The screen must have "New Password" and "Confirm New Password" fields with validation (min. 8 characters).
‎Dashboard:
‎Display a list of all active polls.
‎For each poll, show a live countdown timer indicating the time remaining until the poll closes.
‎For each poll, show the total number of votes cast in real-time (e.g., "4,321 Votes Cast").
‎Voting Ballot:
‎When a user clicks "Vote Now," show a detailed view with the poll title, an optional Base64 image, and a button for each candidate/option.
‎The user's vote must be confirmed with a browser confirm() dialog before submission. A user can only vote once per poll.
‎Profile Page:
‎Display the user's name, matriculation number, and department.
‎Display an "I Voted!" badge if the user has voted in at least one election.
‎List the user's voting history in an anonymized way (e.g., "Voted in Poll X").
‎Results Page:
‎Display results only for polls where the published flag is true.
‎Use the HTML Canvas API to draw a pie chart for each published result, showing the breakdown of votes.
‎2. Admin Portal (admin.html)
‎Login Screen:
‎Secure login for users with an role: 'admin' field in their Firestore document.
‎Include a grey button that links to index.html for easy switching.
‎User Management Tab:
‎A form to add a single user (matric, name, department, eligibility). This must generate a temporary password and create both the Firebase Auth user and the Firestore user document. Crucially, this process must not log the admin out.
‎A table that displays all registered users (except other admins) with their matric number, name, and eligibility status.
‎In the user table, include an eligibility toggle button for each user that allows the admin to instantly make a user eligible or ineligible to vote.
‎Poll Management Tab:
‎A comprehensive form to Create and Edit polls (title, start/end dates, options, active/published flags, and a file input for an optional Base64 banner image).
‎The image must be resized and compressed client-side using the HTML Canvas API before being stored as a Base64 string.
‎A table displaying all existing polls. Each poll must have Edit, Delete, and Clone buttons. The "Clone" button should pre-fill the form with the selected poll's data.
‎Results Tab:
‎Display the results and charts for all polls, regardless of their published status. This is for admin review.
‎Audit Tab:
‎Display a real-time table of every vote cast across all polls, showing the Vote ID, Poll ID, the voter's Matriculation Number, their choice, and the timestamp.
‎Include "Export to CSV" and "Export to JSON" buttons that download the complete audit trail.
‎--- Firebase Setup and Data Models ---
‎Firestore Collections:
‎users: Document ID should be the matriculation number. Fields: uid (from Auth), name, department, eligible (boolean), passwordChanged (boolean), role (string, optional).
‎polls: Fields: title, options (array of strings), imageBase64, active (boolean), published (boolean), startDate (Timestamp), endDate (Timestamp).
‎votes: Document ID should be pollId_matric. Fields: pollId, matric, choice, timestamp.
‎Offline Persistence: Firestore persistence must be enabled for the voter portal (index.html).
‎--- Firebase Security Rules ---
‎Generate a complete and secure set of Firestore rules that enforce the following:
‎Only admins can write to the users and polls collections.
‎A regular user can read their own users document but cannot list the entire collection.
‎A regular user can update their own users document if and only if they are changing the passwordChanged field.
‎Any authenticated user can read the polls and votes collections (this is necessary for results and history).
‎A user can only create a votes document if they are eligible, have passwordChanged, the poll is active, and a vote for that poll by them does not already exist.
‎--- Output Format ---
‎Please provide the output in the following structured format using markdown code blocks:
‎Firebase Project Setup Guide: A brief, step-by-step guide for setting up the Firebase project, enabling services, and getting the config object.
‎Complete Firebase Security Rules: The full firestore.rules content.
‎Complete index.html Code: The full, self-contained HTML file for the voter portal.
‎Complete admin.html Code: The full, self-contained HTML file for the admin panel.
‎

Code used (Index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Election Portal</title>
    <style>
        :root {
            --primary-color: #003366;
            --secondary-color: #f4f4f4;
            --accent-color: #007bff;
            --text-color: #333;
            --danger-color: #dc3545;
            --success-color: #28a745;
            --border-radius: 8px;
            --box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            margin: 0;
            background-color: var(--secondary-color);
            color: var(--text-color);
            display: flex;
            flex-direction: column;
            min-height: 100vh;
        }
        .container {
            width: 90%;
            max-width: 1200px;
            margin: 20px auto;
            padding: 20px;
            background-color: #fff;
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
            flex-grow: 1;
        }
        header {
            background-color: var(--primary-color);
            color: white;
            padding: 1rem;
            text-align: center;
            position: relative;
        }
        header h1 { margin: 0; }
        .admin-link {
            position: absolute;
            top: 5px;
            right: 5px;
            color: rgba(255,255,255,0.5);
            text-decoration: none;
            font-size: 24px;
            transition: color 0.3s;
        }
        .admin-link:hover { color: white; }
        .auth-form {
            max-width: 400px;
            margin: 50px auto;
            padding: 2rem;
            border: 1px solid #ddd;
            border-radius: var(--border-radius);
        }
        .form-group { margin-bottom: 1rem; }
        label { display: block; margin-bottom: 0.5rem; }
        input[type="text"], input[type="password"], input[type="email"] {
            width: 100%;
            padding: 0.75rem;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
        }
        button {
            width: 100%;
            padding: 0.75rem;
            border: none;
            border-radius: 4px;
            background-color: var(--accent-color);
            color: white;
            font-size: 1rem;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        button:hover { background-color: #0056b3; }
        button:disabled { background-color: #aaa; cursor: not-allowed; }
        nav {
            display: flex;
            justify-content: center;
            background-color: #fff;
            border-bottom: 2px solid var(--primary-color);
            margin-bottom: 20px;
        }
        nav button {
            background: none;
            border: none;
            color: var(--primary-color);
            padding: 1rem 1.5rem;
            cursor: pointer;
            font-size: 1rem;
            border-bottom: 3px solid transparent;
            border-radius: 0;
            width: auto;
        }
        nav button.active { border-bottom-color: var(--accent-color); font-weight: bold; }
        .page { display: none; }
        .page.active { display: block; }
        .poll-card, .result-card {
            border: 1px solid #ddd;
            border-radius: var(--border-radius);
            padding: 1.5rem;
            margin-bottom: 1.5rem;
            box-shadow: var(--box-shadow);
        }
        .poll-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 1rem;
        }
        .poll-title { font-size: 1.5rem; margin: 0; }
        .poll-countdown, .poll-votes { text-align: right; }
        .poll-countdown span { font-weight: bold; color: var(--danger-color); }
        .poll-votes span { font-weight: bold; color: var(--primary-color); }
        .vote-now-btn {
            background-color: var(--success-color);
            margin-top: 1rem;
            width: auto;
            padding: 0.5rem 1.5rem;
        }
        .vote-now-btn:hover { background-color: #218838; }
        #ballot-view img { max-width: 100%; height: auto; margin-bottom: 1rem; border-radius: var(--border-radius); }
        .candidate-btn {
            display: block;
            margin-bottom: 1rem;
            background-color: var(--primary-color);
        }
        .candidate-btn:hover { background-color: #002244; }
        #profile-info p { font-size: 1.2rem; }
        #voted-badge {
            display: inline-block;
            background-color: var(--success-color);
            color: white;
            padding: 0.5rem 1rem;
            border-radius: 15px;
            font-weight: bold;
        }
        #voting-history ul { list-style-type: none; padding: 0; }
        #voting-history li {
            background: #f9f9f9;
            padding: 10px;
            margin-bottom: 5px;
            border-left: 4px solid var(--accent-color);
        }
        #loader {
            position: fixed;
            top: 0; left: 0;
            width: 100%; height: 100%;
            background-color: rgba(255,255,255,0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        }
        .spinner {
            border: 8px solid #f3f3f3;
            border-top: 8px solid var(--primary-color);
            border-radius: 50%;
            width: 60px;
            height: 60px;
            animation: spin 1s linear infinite;
        }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .error-message {
            color: var(--danger-color);
            background: #f8d7da;
            padding: 1rem;
            border: 1px solid #f5c6cb;
            border-radius: var(--border-radius);
            margin-top: 1rem;
            text-align: center;
            display: none;
        }
        .pie-chart-container {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 20px;
            flex-wrap: wrap;
        }
        .chart-legend { list-style: none; padding-left: 0; }
        .chart-legend li { display: flex; align-items: center; margin-bottom: 5px; }
        .legend-color-box { width: 20px; height: 20px; margin-right: 10px; }
        @media (max-width: 768px) {
            nav { flex-wrap: wrap; }
            nav button { padding: 0.75rem 1rem; font-size: 0.9rem; }
            .poll-header { flex-direction: column; align-items: flex-start; }
            .poll-countdown, .poll-votes { text-align: left; }
        }
    </style>
</head>
<body>

    <div id="loader">
        <div class="spinner"></div>
    </div>

    <div id="app-root">
        <!-- Login Screen -->
        <div id="login-screen">
            <header><h1>Voter Login</h1></header>
            <div class="auth-form">
                <h2>Welcome</h2>
                <p>Please enter your credentials to proceed.</p>
                <div class="form-group">
                    <label for="matric">Matriculation Number</label>
                    <input type="text" id="matric" placeholder="e.g., 2023001">
                </div>
                <div class="form-group">
                    <label for="password">Password</label>
                    <input type="password" id="password">
                </div>
                <button id="login-btn">Login</button>
                <div id="login-error" class="error-message"></div>
                 <a href="admin.html" class="admin-link" title="Admin Portal">&#9881;</a>
            </div>
        </div>

        <!-- Password Change Screen -->
        <div id="password-change-screen" style="display: none;">
            <header><h1>Change Your Password</h1></header>
            <div class="auth-form">
                <h2>Security Update Required</h2>
                <p>For your security, you must change your password on first login.</p>
                <div class="form-group">
                    <label for="new-password">New Password</label>
                    <input type="password" id="new-password" placeholder="Min. 8 characters">
                </div>
                <div class="form-group">
                    <label for="confirm-password">Confirm New Password</label>
                    <input type="password" id="confirm-password">
                </div>
                <button id="change-password-btn">Update Password</button>
                <div id="password-change-error" class="error-message"></div>
            </div>
        </div>

        <!-- Main App (Dashboard, Profile, Results) -->
        <div id="main-app" style="display: none;">
            <header>
                <h1>University Election Portal</h1>
                <button id="logout-btn" style="position: absolute; top: 10px; right: 10px; width: auto; background: var(--danger-color); padding: 5px 10px;">Logout</button>
            </header>
            <main class="container">
                <nav>
                    <button class="nav-btn active" data-page="dashboard-page">Dashboard</button>
                    <button class="nav-btn" data-page="profile-page">Profile</button>
                    <button class="nav-btn" data-page="results-page">Results</button>
                </nav>

                <!-- Dashboard Page -->
                <div id="dashboard-page" class="page active">
                    <h2>Active Polls</h2>
                    <div id="polls-container"></div>
                </div>

                <!-- Ballot View (Modal-like) -->
                <div id="ballot-view" class="page">
                     <button id="back-to-dashboard-btn">&larr; Back to Dashboard</button>
                     <h2 id="ballot-title"></h2>
                     <img id="ballot-image" src="" alt="Poll Banner" style="display:none;"/>
                     <div id="ballot-options"></div>
                </div>

                <!-- Profile Page -->
                <div id="profile-page" class="page">
                    <h2>Your Profile</h2>
                    <div id="profile-info"></div>
                    <h3>Voting History</h3>
                    <div id="voting-history"></div>
                </div>

                <!-- Results Page -->
                <div id="results-page" class="page">
                    <h2>Published Results</h2>
                    <div id="results-container"></div>
                </div>
            </main>
        </div>
    </div>

    <script type="module">
        // IMPORTANT: Paste your Firebase config object here
        const firebaseConfig = {
            apiKey: "AIzaSy...",
            authDomain: "your-project-id.firebaseapp.com",
            projectId: "your-project-id",
            storageBucket: "your-project-id.appspot.com",
            messagingSenderId: "...",
            appId: "..."
        };

        // Import statements
        import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
        import {
            getAuth,
            signInWithEmailAndPassword,
            signOut,
            onAuthStateChanged,
            updatePassword
        } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
        import {
            getFirestore,
            doc,
            getDoc,
            getDocs, // <-- FIX: Added missing getDocs import
            collection,
            onSnapshot,
            setDoc,
            updateDoc,
            query,
            where,
            enableIndexedDbPersistence,
            Timestamp
        } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js";

        // Firebase Initialization
        const app = initializeApp(firebaseConfig);
        const auth = getAuth(app);
        const db = getFirestore(app);
        
        enableIndexedDbPersistence(db)
          .catch((err) => {
            if (err.code == 'failed-precondition') {
                console.warn("Multiple tabs open, persistence can only be enabled in one. Not a critical error.");
            } else if (err.code == 'unimplemented') {
                console.log("The current browser does not support all of the features required to enable persistence.");
            }
          });

        // Global state
        let currentUser = null;
        let userData = null;
        let countdownIntervals = {};
        let allPollsMap = new Map(); // <-- OPTIMIZATION: Cache for poll data

        // DOM Elements
        const loader = document.getElementById('loader');
        const loginScreen = document.getElementById('login-screen');
        const mainApp = document.getElementById('main-app');
        const passwordChangeScreen = document.getElementById('password-change-screen');

        const showLoader = (show) => {
            loader.style.display = show ? 'flex' : 'none';
        };
        const showError = (elementId, message) => {
            const errorEl = document.getElementById(elementId);
            if (errorEl) {
                errorEl.textContent = message;
                errorEl.style.display = 'block';
            }
        };
        const hideError = (elementId) => {
            const errorEl = document.getElementById(elementId);
            if (errorEl) errorEl.style.display = 'none';
        };
        
        // --- AUTHENTICATION ---
        onAuthStateChanged(auth, async (user) => {
            if (user) {
                currentUser = user;
                const matric = user.email.split('@')[0];
                const userDocRef = doc(db, "users", matric);
                const userDocSnap = await getDoc(userDocRef);

                if (userDocSnap.exists()) {
                    userData = { id: userDocSnap.id, ...userDocSnap.data() };
                    if (!userData.passwordChanged) {
                        showScreen('password-change');
                    } else {
                        showScreen('main-app');
                        await initMainApp(); // <-- FIX: Added await to ensure data loads before rendering
                    }
                } else {
                    console.error("User document not found in Firestore.");
                    await signOut(auth);
                }
            } else {
                currentUser = null;
                userData = null;
                showScreen('login');
                Object.values(countdownIntervals).forEach(clearInterval);
            }
            showLoader(false);
        });

        document.getElementById('login-btn').addEventListener('click', async () => {
            const matric = document.getElementById('matric').value.trim();
            const password = document.getElementById('password').value;
            if (!matric || !password) {
                showError('login-error', 'Please fill in all fields.');
                return;
            }
            hideError('login-error');
            showLoader(true);
            try {
                const email = `${matric}@uni.local`;
                await signInWithEmailAndPassword(auth, email, password);
            } catch (error) {
                console.error("Login failed:", error);
                showError('login-error', 'Invalid matriculation number or password.');
                showLoader(false);
            }
        });

        document.getElementById('logout-btn').addEventListener('click', () => signOut(auth));
        
        document.getElementById('change-password-btn').addEventListener('click', async () => {
            const newPassword = document.getElementById('new-password').value;
            const confirmPassword = document.getElementById('confirm-password').value;
            if (newPassword.length < 8) {
                showError('password-change-error', 'Password must be at least 8 characters long.');
                return;
            }
            if (newPassword !== confirmPassword) {
                showError('password-change-error', 'Passwords do not match.');
                return;
            }
            hideError('password-change-error');
            showLoader(true);
            try {
                await updatePassword(auth.currentUser, newPassword);
                const userDocRef = doc(db, "users", userData.id);
                await updateDoc(userDocRef, { passwordChanged: true });
            } catch (error) {
                console.error("Password update failed:", error);
                showError('password-change-error', 'Failed to update password. Please try again.');
                showLoader(false);
            }
        });

        // --- UI NAVIGATION ---
        const screens = { 'login': loginScreen, 'password-change': passwordChangeScreen, 'main-app': mainApp };
        const showScreen = (screenName) => {
            Object.values(screens).forEach(s => s.style.display = 'none');
            if (screens[screenName]) screens[screenName].style.display = 'block';
        };

        const pages = document.querySelectorAll('.page');
        const navBtns = document.querySelectorAll('.nav-btn');
        navBtns.forEach(btn => {
            btn.addEventListener('click', () => {
                const pageId = btn.getAttribute('data-page');
                pages.forEach(p => p.classList.remove('active'));
                navBtns.forEach(b => b.classList.remove('active'));
                document.getElementById(pageId).classList.add('active');
                btn.classList.add('active');
            });
        });

        const showBallotView = (show) => {
            document.getElementById('dashboard-page').style.display = show ? 'none' : 'block';
            document.getElementById('ballot-view').style.display = show ? 'block' : 'none';
        };
        
        document.getElementById('back-to-dashboard-btn').addEventListener('click', () => showBallotView(false));
        
        // --- FIX: Made function async and pre-fetch polls ---
        async function initMainApp() {
            // Fetch all polls once to create a lookup map for titles
            const pollsSnapshot = await getDocs(collection(db, "polls"));
            allPollsMap.clear(); // Clear previous data
            pollsSnapshot.forEach(doc => {
                allPollsMap.set(doc.id, doc.data());
            });

            loadDashboard();
            loadProfile();
            loadResults();
        }

        // --- DASHBOARD & VOTING ---
        function loadDashboard() {
            const pollsContainer = document.getElementById('polls-container');
            const q = query(collection(db, "polls"), where("active", "==", true));

            onSnapshot(q, (snapshot) => {
                if(snapshot.empty) {
                    pollsContainer.innerHTML = '<p>No active polls at the moment.</p>';
                    return;
                }
                pollsContainer.innerHTML = '';
                snapshot.forEach(doc => {
                    const poll = { id: doc.id, ...doc.data() };
                    
                    const votesQuery = query(collection(db, "votes"), where("pollId", "==", poll.id));
                    onSnapshot(votesQuery, (votesSnapshot) => {
                        const existingCard = document.getElementById(`poll-${poll.id}`);
                        if (existingCard) {
                             const voteCountEl = existingCard.querySelector('.vote-count');
                             if(voteCountEl) voteCountEl.textContent = `${votesSnapshot.size} Votes Cast`;
                        }
                    });

                    const pollCard = document.createElement('div');
                    pollCard.className = 'poll-card';
                    pollCard.id = `poll-${poll.id}`;
                    const endDate = poll.endDate.toDate();
                    const countdownId = `countdown-${poll.id}`;
                    pollCard.innerHTML = `
                        <div class="poll-header">
                            <div><h3 class="poll-title">${poll.title}</h3></div>
                            <div>
                                <div class="poll-countdown">Time Remaining: <span id="${countdownId}"></span></div>
                                <div class="poll-votes"><span class="vote-count">Loading votes...</span></div>
                            </div>
                        </div>
                        <button class="vote-now-btn" data-poll-id="${poll.id}">Vote Now</button>
                    `;
                    pollsContainer.appendChild(pollCard);

                    updateCountdown(countdownId, endDate);
                    if (countdownIntervals[poll.id]) clearInterval(countdownIntervals[poll.id]);
                    countdownIntervals[poll.id] = setInterval(() => updateCountdown(countdownId, endDate), 1000);
                });
            });
        }
        
        document.getElementById('polls-container').addEventListener('click', async e => {
            if(e.target.classList.contains('vote-now-btn')) {
                const pollId = e.target.dataset.pollId;
                const voteDocRef = doc(db, "votes", `${pollId}_${userData.id}`);
                const voteDocSnap = await getDoc(voteDocRef);

                if (voteDocSnap.exists()) {
                    alert("You have already voted in this poll.");
                    return;
                }
                
                showLoader(true);
                const pollDocRef = doc(db, "polls", pollId);
                const pollDocSnap = await getDoc(pollDocRef);
                if(pollDocSnap.exists()) {
                   renderBallot(pollDocSnap.data(), pollId);
                }
                showLoader(false);
            }
        });

        function renderBallot(poll, pollId) {
            document.getElementById('ballot-title').textContent = poll.title;
            const ballotImage = document.getElementById('ballot-image');
            if (poll.imageBase64) {
                ballotImage.src = poll.imageBase64;
                ballotImage.style.display = 'block';
            } else {
                ballotImage.style.display = 'none';
            }
            const optionsContainer = document.getElementById('ballot-options');
            optionsContainer.innerHTML = '';
            poll.options.forEach(option => {
                const button = document.createElement('button');
                button.className = 'candidate-btn';
                button.textContent = option;
                button.dataset.pollId = pollId;
                button.dataset.choice = option;
                optionsContainer.appendChild(button);
            });
            showBallotView(true);
        }
        
        document.getElementById('ballot-options').addEventListener('click', async e => {
            if (e.target.classList.contains('candidate-btn')) {
                const { pollId, choice } = e.target.dataset;
                const confirmed = confirm(`Are you sure you want to vote for "${choice}"?\nThis action cannot be undone.`);
                if (confirmed) {
                    showLoader(true);
                    const voteId = `${pollId}_${userData.id}`;
                    const voteDocRef = doc(db, "votes", voteId);
                    try {
                        await setDoc(voteDocRef, {
                            pollId: pollId, matric: userData.id, choice: choice, timestamp: Timestamp.now()
                        });
                        alert("Your vote has been cast successfully!");
                        showBallotView(false);
                    } catch (error) {
                        console.error("Error casting vote:", error);
                        alert(`Failed to cast vote. Reason: ${error.message}`);
                    } finally {
                        showLoader(false);
                    }
                }
            }
        });
        
        function updateCountdown(elementId, endDate) {
            const countdownElement = document.getElementById(elementId);
            if (!countdownElement) return;
            const now = new Date().getTime();
            const distance = endDate - now;
            if (distance < 0) {
                countdownElement.innerHTML = "CLOSED";
                const pollId = elementId.split('-')[1];
                const voteButton = document.querySelector(`.vote-now-btn[data-poll-id="${pollId}"]`);
                if(voteButton) {
                    voteButton.disabled = true;
                    voteButton.textContent = 'Poll Closed';
                }
                return;
            }
            const days = Math.floor(distance / (1000 * 60 * 60 * 24));
            const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
            const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
            const seconds = Math.floor((distance % (1000 * 60)) / 1000);
            countdownElement.innerHTML = `${days}d ${hours}h ${minutes}m ${seconds}s`;
        }

        // --- FIX: Rewritten for correctness and efficiency ---
        async function loadProfile() {
            const profileInfoContainer = document.getElementById('profile-info');
            const historyContainer = document.getElementById('voting-history');
            
            profileInfoContainer.innerHTML = `
                <p><strong>Name:</strong> ${userData.name}</p>
                <p><strong>Matriculation No:</strong> ${userData.id}</p>
                <p><strong>Department:</strong> ${userData.department}</p>
            `;
            
            const q = query(collection(db, "votes"), where("matric", "==", userData.id));
            const querySnapshot = await getDocs(q); // Correct way to query multiple docs

            if (!querySnapshot.empty) {
                profileInfoContainer.innerHTML += `<p><span id="voted-badge">I Voted!</span></p>`;
                
                // Use the pre-fetched map instead of another complex query
                const historyHtml = querySnapshot.docs.map(doc => {
                    const pollId = doc.data().pollId;
                    const pollTitle = allPollsMap.get(pollId)?.title || 'a past poll';
                    return `<li>Voted in <strong>${pollTitle}</strong></li>`;
                }).join('');
                historyContainer.innerHTML = `<ul>${historyHtml}</ul>`;
            } else {
                 historyContainer.innerHTML = '<p>You have not voted in any polls yet.</p>';
            }
        }
        
        // --- RESULTS ---
        function loadResults() {
             const resultsContainer = document.getElementById('results-container');
             const q = query(collection(db, "polls"), where("published", "==", true));
             
             onSnapshot(q, (pollsSnapshot) => {
                if (pollsSnapshot.empty) {
                    resultsContainer.innerHTML = '<p>No results have been published yet.</p>';
                    return;
                }
                resultsContainer.innerHTML = '';
                pollsSnapshot.forEach(async (pollDoc) => {
                    const poll = { id: pollDoc.id, ...pollDoc.data() };
                    const resultCard = document.createElement('div');
                    resultCard.className = 'result-card';
                    resultCard.innerHTML = `<h3>${poll.title}</h3><div id="result-content-${poll.id}"><p>Loading results...</p></div>`;
                    resultsContainer.appendChild(resultCard);
                    
                    const votesQuery = query(collection(db, "votes"), where("pollId", "==", poll.id));
                    const votesSnapshot = await getDocs(votesQuery);
                    
                    const results = {};
                    poll.options.forEach(opt => results[opt] = 0);
                    votesSnapshot.forEach(voteDoc => {
                        const choice = voteDoc.data().choice;
                        if(results.hasOwnProperty(choice)) {
                            results[choice]++;
                        }
                    });
                    const resultContentContainer = document.getElementById(`result-content-${poll.id}`);
                    renderPieChart(resultContentContainer, results);
                });
             });
        }

        function renderPieChart(container, data) {
            container.innerHTML = '';
            const totalVotes = Object.values(data).reduce((sum, val) => sum + val, 0);
            if (totalVotes === 0) {
                container.innerHTML = '<p>No votes have been cast for this poll yet.</p>';
                return;
            }
            const canvas = document.createElement('canvas');
            canvas.width = 200; canvas.height = 200;
            const ctx = canvas.getContext('2d');
            const chartAndLegendContainer = document.createElement('div');
            chartAndLegendContainer.className = 'pie-chart-container';
            const legend = document.createElement('ul');
            legend.className = 'chart-legend';
            let startAngle = 0;
            const colors = ['#003366', '#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8', '#6610f2', '#6f42c1'];
            let colorIndex = 0;
            for (const [option, votes] of Object.entries(data)) {
                const sliceAngle = (votes / totalVotes) * 2 * Math.PI;
                const endAngle = startAngle + sliceAngle;
                const color = colors[colorIndex++ % colors.length];
                ctx.beginPath();
                ctx.moveTo(canvas.width / 2, canvas.height / 2);
                ctx.arc(canvas.width / 2, canvas.height / 2, Math.min(canvas.width / 2, canvas.height / 2), startAngle, endAngle);
                ctx.closePath();
                ctx.fillStyle = color;
                ctx.fill();
                startAngle = endAngle;
                const percentage = ((votes / totalVotes) * 100).toFixed(1);
                const legendItem = document.createElement('li');
                legendItem.innerHTML = `<div class="legend-color-box" style="background-color: ${color};"></div> ${option}: ${votes} votes (${percentage}%)`;
                legend.appendChild(legendItem);
            }
            chartAndLegendContainer.appendChild(canvas);
            chartAndLegendContainer.appendChild(legend);
            container.appendChild(chartAndLegendContainer);
        }
    </script>
</body>
</html>

Code (Admin.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Portal</title>
    <style>
        :root {
            --primary-color: #343a40;
            --secondary-color: #f8f9fa;
            --accent-color: #007bff;
            --text-color: #212529;
            --danger-color: #dc3545;
            --success-color: #28a745;
            --warning-color: #ffc107;
            --border-radius: 8px;
            --box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            margin: 0;
            background-color: var(--secondary-color);
            color: var(--text-color);
        }
        .container {
            width: 95%;
            max-width: 1400px;
            margin: 20px auto;
            padding: 20px;
            background-color: #fff;
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
        }
        header {
            background-color: var(--primary-color);
            color: white;
            padding: 1rem;
            text-align: center;
        }
        header h1 { margin: 0; }
        .auth-form {
            max-width: 400px;
            margin: 50px auto;
            padding: 2rem;
            border: 1px solid #ddd;
            border-radius: var(--border-radius);
        }
        .form-group, .form-field { margin-bottom: 1rem; }
        label { display: block; margin-bottom: 0.5rem; font-weight: 500;}
        input[type="text"], input[type="password"], input[type="email"], input[type="datetime-local"], select, input[type="file"] {
            width: 100%;
            padding: 0.75rem;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
        }
        button, .btn {
            padding: 0.75rem 1.5rem;
            border: none;
            border-radius: 4px;
            color: white;
            font-size: 1rem;
            cursor: pointer;
            transition: background-color 0.3s;
            text-align: center;
        }
        .btn-primary { background-color: var(--accent-color); }
        .btn-primary:hover { background-color: #0056b3; }
        .btn-secondary { background-color: #6c757d; }
        .btn-secondary:hover { background-color: #5a6268; }
        .btn-danger { background-color: var(--danger-color); }
        .btn-danger:hover { background-color: #c82333; }
        .btn-success { background-color: var(--success-color); }
        .btn-success:hover { background-color: #218838; }
        .btn-warning { background-color: var(--warning-color); color: #212529; }
        .btn-warning:hover { background-color: #e0a800; }
        .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
        button:disabled { background-color: #aaa; cursor: not-allowed; }
        .d-flex { display: flex; }
        .gap-1 { gap: 0.5rem; }
        
        nav {
            display: flex;
            justify-content: flex-start;
            background-color: #e9ecef;
            border-bottom: 2px solid var(--primary-color);
            margin-bottom: 20px;
            flex-wrap: wrap;
        }
        nav button {
            background: none;
            border: none;
            color: var(--primary-color);
            padding: 1rem 1.5rem;
            cursor: pointer;
            font-size: 1rem;
            border-bottom: 3px solid transparent;
            border-radius: 0;
            width: auto;
        }
        nav button.active { background-color: #fff; border-bottom-color: var(--accent-color); font-weight: bold; }
        .page { display: none; }
        .page.active { display: block; }
        .grid-container {
            display: grid;
            grid-template-columns: 1fr 2fr;
            gap: 2rem;
        }
        .form-card { padding: 1.5rem; border: 1px solid #ddd; border-radius: var(--border-radius); }
        .table-wrapper { overflow-x: auto; }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 1rem;
        }
        th, td {
            padding: 0.75rem;
            text-align: left;
            border-bottom: 1px solid #dee2e6;
        }
        th { background-color: #f2f2f2; }
        tr:hover { background-color: #f1f1f1; }
        .toggle-switch {
            position: relative;
            display: inline-block;
            width: 50px;
            height: 24px;
        }
        .toggle-switch input { opacity: 0; width: 0; height: 0; }
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0; left: 0; right: 0; bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 24px;
        }
        .slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        input:checked + .slider { background-color: var(--success-color); }
        input:checked + .slider:before { transform: translateX(26px); }
        .error-message {
            color: var(--danger-color);
            background: #f8d7da;
            padding: 1rem;
            border: 1px solid #f5c6cb;
            border-radius: var(--border-radius);
            margin-top: 1rem;
            text-align: center;
            display: none;
        }
        .pie-chart-container {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 20px;
            flex-wrap: wrap;
        }
        .chart-legend { list-style: none; padding-left: 0; }
        .chart-legend li { display: flex; align-items: center; margin-bottom: 5px; }
        .legend-color-box { width: 20px; height: 20px; margin-right: 10px; }
        #loader {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background-color: rgba(255,255,255,0.8);
            display: flex; justify-content: center; align-items: center;
            z-index: 9999;
        }
        .spinner {
            border: 8px solid #f3f3f3; border-top: 8px solid var(--primary-color);
            border-radius: 50%; width: 60px; height: 60px;
            animation: spin 1s linear infinite;
        }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .feedback { margin-top: 1rem; padding: 1rem; border-radius: var(--border-radius); }
        .feedback.success { background-color: #d4edda; color: #155724; }
        .feedback.error { background-color: #f8d7da; color: #721c24; }
        .feedback ul { padding-left: 20px; margin-top: 10px; }

        @media (max-width: 992px) {
            .grid-container { grid-template-columns: 1fr; }
        }
        @media (max-width: 768px) {
            nav { justify-content: space-around; }
            nav button { padding: 0.75rem 0.5rem; font-size: 0.9rem; }
        }
    </style>
</head>
<body>

    <div id="loader" style="display: none;">
        <div class="spinner"></div>
    </div>

    <div id="app-root">
        <!-- Login Screen -->
        <div id="login-screen">
            <header><h1>Admin Portal Login</h1></header>
            <div class="auth-form">
                <h2>Admin Access</h2>
                <div class="form-group">
                    <label for="admin-matric">Admin Matriculation No.</label>
                    <input type="text" id="admin-matric">
                </div>
                <div class="form-group">
                    <label for="admin-password">Password</label>
                    <input type="password" id="admin-password">
                </div>
                <button id="admin-login-btn" class="btn btn-primary" style="width:100%;">Login</button>
                <a href="index.html" class="btn btn-secondary" style="width:100%; margin-top: 10px; text-decoration: none;">Go to Voter Portal</a>
                <div id="login-error" class="error-message"></div>
            </div>
        </div>

        <!-- Main App -->
        <div id="main-app" style="display: none;">
            <header>
                <h1>Admin Dashboard</h1>
                <button id="logout-btn" style="position: absolute; top: 10px; right: 10px; width: auto;" class="btn btn-danger">Logout</button>
            </header>
            <main class="container">
                <nav>
                    <button class="nav-btn active" data-page="users-page">User Management</button>
                    <button class="nav-btn" data-page="polls-page">Poll Management</button>
                    <button class="nav-btn" data-page="results-page">Results</button>
                    <button class="nav-btn" data-page="audit-page">Audit Trail</button>
                </nav>

                <!-- User Management -->
                <div id="users-page" class="page active">
                    <div class="grid-container">
                        <div class="form-card">
                            <h3>Add Single User</h3>
                            <div class="form-field"><label for="user-matric">Matriculation No.</label><input type="text" id="user-matric"></div>
                            <div class="form-field"><label for="user-name">Full Name</label><input type="text" id="user-name"></div>
                            <div class="form-field"><label for="user-department">Department</label><input type="text" id="user-department"></div>
                            <div class="form-field"><label for="user-eligible">Eligible to Vote</label><select id="user-eligible"><option value="true">Yes</option><option value="false">No</option></select></div>
                            <button id="add-user-btn" class="btn btn-primary">Add User</button>
                            <div id="add-user-feedback" class="feedback" style="display:none;"></div>

                            <hr style="margin: 2rem 0;">

                            <h3>Bulk Register Users via CSV</h3>
                            <div class="form-field">
                                <label for="csv-file-input">Upload CSV File</label>
                                <input type="file" id="csv-file-input" accept=".csv">
                                <small>Format: One user per line, no header. Columns: `matric,name,department`</small>
                            </div>
                            <button id="upload-csv-btn" class="btn btn-success">Upload and Register</button>
                            <div id="bulk-upload-feedback" class="feedback" style="display:none;"></div>
                        </div>
                        <div>
                            <h3>Registered Users</h3>
                            <div class="table-wrapper">
                                <table id="users-table">
                                    <thead><tr><th>Matric No.</th><th>Name</th><th>Eligible</th></tr></thead>
                                    <tbody></tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Poll Management -->
                <div id="polls-page" class="page">
                     <div class="grid-container">
                        <div class="form-card">
                            <h3 id="poll-form-title">Create New Poll</h3>
                            <input type="hidden" id="poll-id">
                            <div class="form-field"><label for="poll-title">Title</label><input type="text" id="poll-title"></div>
                            <div class="form-field"><label for="poll-start">Start Date</label><input type="datetime-local" id="poll-start"></div>
                            <div class="form-field"><label for="poll-end">End Date</label><input type="datetime-local" id="poll-end"></div>
                            <div class="form-field"><label for="poll-options">Options (one per line)</label><textarea id="poll-options" rows="5" style="width: 100%; resize: vertical;"></textarea></div>
                            <div class="form-field"><label for="poll-image">Banner Image (Optional)</label><input type="file" id="poll-image" accept="image/*"><img id="image-preview" src="" style="max-width: 100%; margin-top: 10px;"/></div>
                            <div class="form-field d-flex gap-1" style="align-items: center;"><input type="checkbox" id="poll-active" style="width:auto;"><label for="poll-active" style="margin:0;">Active</label></div>
                            <div class="form-field d-flex gap-1" style="align-items: center;"><input type="checkbox" id="poll-published" style="width:auto;"><label for="poll-published" style="margin:0;">Publish Results</label></div>
                            <div class="d-flex gap-1">
                                <button id="save-poll-btn" class="btn btn-primary">Save Poll</button>
                                <button id="clear-poll-form-btn" class="btn btn-secondary">Clear</button>
                            </div>
                        </div>
                        <div>
                            <h3>Existing Polls</h3>
                            <div class="table-wrapper"><table id="polls-table"><thead><tr><th>Title</th><th>Active</th><th>Published</th><th>Actions</th></tr></thead><tbody></tbody></table></div>
                        </div>
                    </div>
                </div>

                <!-- Results Page -->
                <div id="results-page" class="page"><h2>All Poll Results</h2><div id="results-container"></div></div>
                
                <!-- Audit Page -->
                <div id="audit-page" class="page">
                    <h2>Vote Audit Trail</h2>
                    <div class="d-flex gap-1">
                        <button id="export-csv-btn" class="btn btn-success">Export to CSV</button>
                        <button id="export-json-btn" class="btn btn-warning">Export to JSON</button>
                    </div>
                    <div class="table-wrapper"><table id="audit-table"><thead><tr><th>Vote ID</th><th>Poll ID</th><th>Matric No.</th><th>Choice</th><th>Timestamp</th></tr></thead><tbody></tbody></table></div>
                </div>

            </main>
        </div>
    </div>

    <script type="module">
        // IMPORTANT: Paste your Firebase config object here
        const firebaseConfig = {
            apiKey: "AIzaSy...",
            authDomain: "your-project-id.firebaseapp.com",
            projectId: "your-project-id",
            storageBucket: "your-project-id.appspot.com",
            messagingSenderId: "...",
            appId: "..."
        };

        import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
        import { getAuth, signInWithEmailAndPassword, signOut, onAuthStateChanged, createUserWithEmailAndPassword } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
        import { getFirestore, doc, getDoc, collection, onSnapshot, setDoc, addDoc, updateDoc, deleteDoc, query, where, Timestamp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js";

        // Initialize Primary Firebase App
        const mainApp = initializeApp(firebaseConfig, "main");
        const mainAuth = getAuth(mainApp);
        const db = getFirestore(mainApp);
        
        // Initialize a secondary, temporary Firebase app for user creation
        const secondaryApp = initializeApp(firebaseConfig, "secondary");
        const secondaryAuth = getAuth(secondaryApp);

        // Global State
        let allPollsData = {}; let auditData = [];

        // DOM Elements
        const loader = document.getElementById('loader');
        const loginScreen = document.getElementById('login-screen');
        const appContainer = document.getElementById('main-app');

        const showLoader = (show) => { loader.style.display = show ? 'flex' : 'none'; };
        const showError = (message) => {
            const errorEl = document.getElementById('login-error');
            errorEl.textContent = message;
            errorEl.style.display = 'block';
        };
        const showFeedback = (el, message, isSuccess) => {
            el.innerHTML = message;
            el.className = `feedback ${isSuccess ? 'success' : 'error'}`;
            el.style.display = 'block';
        };

        // --- AUTHENTICATION ---
        onAuthStateChanged(mainAuth, async (user) => {
            if (user) {
                const matric = user.email.split('@')[0];
                const userDocSnap = await getDoc(doc(db, "users", matric));
                if (userDocSnap.exists() && userDocSnap.data().role === 'admin') {
                    loginScreen.style.display = 'none';
                    appContainer.style.display = 'block';
                    initAdminApp();
                } else {
                    await signOut(mainAuth);
                    showError("Access denied. Not an admin user.");
                }
            } else {
                loginScreen.style.display = 'block';
                appContainer.style.display = 'none';
            }
            showLoader(false);
        });

        document.getElementById('admin-login-btn').addEventListener('click', async () => {
            const matric = document.getElementById('admin-matric').value.trim();
            const password = document.getElementById('admin-password').value;
            if (!matric || !password) return;
            showLoader(true);
            try {
                await signInWithEmailAndPassword(mainAuth, `${matric}@uni.local`, password);
            } catch (error) {
                showError("Invalid credentials or not an admin.");
                showLoader(false);
            }
        });

        document.getElementById('logout-btn').addEventListener('click', () => signOut(mainAuth));
        
        // --- NAVIGATION ---
        document.querySelectorAll('.nav-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const pageId = btn.getAttribute('data-page');
                document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
                document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
                document.getElementById(pageId).classList.add('active');
                btn.classList.add('active');
            });
        });

        function initAdminApp() {
            loadUsers(); loadPolls(); loadAdminResults(); loadAuditTrail();
        }

        // --- USER MANAGEMENT (SINGLE & BULK) ---
        function loadUsers() {
            const usersTableBody = document.querySelector('#users-table tbody');
            onSnapshot(query(collection(db, "users"), where("role", "!=", "admin")), (snapshot) => {
                usersTableBody.innerHTML = '';
                snapshot.forEach(doc => {
                    const user = { id: doc.id, ...doc.data() };
                    usersTableBody.innerHTML += `<tr><td>${user.id}</td><td>${user.name}</td><td><label class="toggle-switch"><input type="checkbox" class="eligibility-toggle" data-id="${user.id}" ${user.eligible ? 'checked' : ''}><span class="slider"></span></label></td></tr>`;
                });
            });
        }
        
        document.querySelector('#users-table').addEventListener('change', async e => {
            if (e.target.classList.contains('eligibility-toggle')) {
                const matric = e.target.dataset.id;
                const isEligible = e.target.checked;
                try {
                    await updateDoc(doc(db, "users", matric), { eligible: isEligible });
                } catch (error) {
                    alert("Error updating eligibility status.");
                    e.target.checked = !isEligible;
                }
            }
        });

        document.getElementById('add-user-btn').addEventListener('click', async () => {
            const matric = document.getElementById('user-matric').value.trim();
            const name = document.getElementById('user-name').value.trim();
            const dept = document.getElementById('user-department').value.trim();
            const eligible = document.getElementById('user-eligible').value === 'true';
            const feedbackEl = document.getElementById('add-user-feedback');
            if (!matric || !name || !dept) {
                showFeedback(feedbackEl, 'Please fill all fields.', false);
                return;
            }
            showLoader(true);
            const tempPassword = Math.random().toString(36).slice(-8);
            try {
                const userCredential = await createUserWithEmailAndPassword(secondaryAuth, `${matric}@uni.local`, tempPassword);
                await setDoc(doc(db, "users", matric), { uid: userCredential.user.uid, name, department: dept, eligible, passwordChanged: false });
                showFeedback(feedbackEl, `User ${name} created successfully! Temp Password: <strong>${tempPassword}</strong>`, true);
                document.getElementById('user-matric').value = '';
                document.getElementById('user-name').value = '';
                document.getElementById('user-department').value = '';
            } catch (error) {
                showFeedback(feedbackEl, `Error: ${error.message}`, false);
            } finally {
                await signOut(secondaryAuth);
                showLoader(false);
            }
        });

        document.getElementById('upload-csv-btn').addEventListener('click', () => {
            const fileInput = document.getElementById('csv-file-input');
            const file = fileInput.files[0];
            const feedbackEl = document.getElementById('bulk-upload-feedback');
            if (!file) {
                showFeedback(feedbackEl, 'Please select a CSV file to upload.', false);
                return;
            }
            showLoader(true);
            const reader = new FileReader();
            reader.onload = async (e) => {
                const lines = e.target.result.split(/\r\n|\n/);
                const userPromises = [];
                for (const line of lines) {
                    if (!line.trim()) continue;
                    const [matric, name, department] = line.split(',').map(s => s.trim());
                    if (!matric || !name || !department) continue;
                    
                    const tempPassword = Math.random().toString(36).slice(-8);
                    
                    userPromises.push(
                        createUserWithEmailAndPassword(secondaryAuth, `${matric}@uni.local`, tempPassword)
                            .then(userCredential => setDoc(doc(db, "users", matric), {
                                uid: userCredential.user.uid, name, department, eligible: true, passwordChanged: false
                            }))
                            .then(() => ({ status: 'fulfilled', matric }))
                            .catch(error => ({ status: 'rejected', matric, reason: error.message }))
                            .finally(() => signOut(secondaryAuth))
                    );
                }

                const results = await Promise.all(userPromises);
                let successCount = 0;
                let errorList = [];
                results.forEach(res => {
                    if (res.status === 'fulfilled') successCount++;
                    else errorList.push(`<li>Matric ${res.matric}: ${res.reason}</li>`);
                });
                
                let feedbackMessage = `<strong>Bulk Upload Complete.</strong><br>Successfully registered: ${successCount} users.`;
                if (errorList.length > 0) {
                    feedbackMessage += `<br>Failed to register: ${errorList.length} users.<ul>${errorList.join('')}</ul>`;
                }
                showFeedback(feedbackEl, feedbackMessage, errorList.length === 0);
                fileInput.value = ''; // Clear file input
                showLoader(false);
            };
            reader.readAsText(file);
        });

        // --- POLL MANAGEMENT ---
        const pollForm = { id: document.getElementById('poll-id'), title: document.getElementById('poll-title'), start: document.getElementById('poll-start'), end: document.getElementById('poll-end'), options: document.getElementById('poll-options'), image: document.getElementById('poll-image'), preview: document.getElementById('image-preview'), active: document.getElementById('poll-active'), published: document.getElementById('poll-published'), titleLabel: document.getElementById('poll-form-title') };
        let imageBase64 = null;

        pollForm.image.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (!file) { imageBase64 = null; pollForm.preview.src = ""; return; }
            const MAX_WIDTH = 800;
            const reader = new FileReader();
            reader.onload = (e) => {
                const img = new Image();
                img.onload = () => {
                    let { width, height } = img;
                    if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; }
                    const canvas = document.createElement('canvas');
                    canvas.width = width; canvas.height = height;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(img, 0, 0, width, height);
                    imageBase64 = canvas.toDataURL('image/jpeg', 0.7);
                    pollForm.preview.src = imageBase64;
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        });
        
        const toLocalISOString = (date) => (new Date(date.getTime() - (date.getTimezoneOffset() * 60000))).toISOString().slice(0, 16);

        function clearPollForm() {
            pollForm.id.value = ''; pollForm.title.value = ''; pollForm.start.value = ''; pollForm.end.value = ''; pollForm.options.value = ''; pollForm.image.value = ''; pollForm.preview.src = '';
            pollForm.active.checked = false; pollForm.published.checked = false; pollForm.titleLabel.textContent = 'Create New Poll'; imageBase64 = null;
        }
        document.getElementById('clear-poll-form-btn').addEventListener('click', clearPollForm);

        document.getElementById('save-poll-btn').addEventListener('click', async () => {
            const pollData = {
                title: pollForm.title.value.trim(),
                startDate: Timestamp.fromDate(new Date(pollForm.start.value)),
                endDate: Timestamp.fromDate(new Date(pollForm.end.value)),
                options: pollForm.options.value.split('\n').map(opt => opt.trim()).filter(Boolean),
                active: pollForm.active.checked, published: pollForm.published.checked,
            };
            if(imageBase64) pollData.imageBase64 = imageBase64;

            if (!pollData.title || !pollForm.start.value || !pollForm.end.value || pollData.options.length < 2) {
                alert("Please fill all required fields (Title, Dates, at least 2 Options)."); return;
            }
            showLoader(true);
            try {
                if (pollForm.id.value) await updateDoc(doc(db, "polls", pollForm.id.value), pollData);
                else await addDoc(collection(db, "polls"), pollData);
                clearPollForm();
            } catch (error) { alert("Failed to save poll."); } 
            finally { showLoader(false); }
        });

        function loadPolls() {
            onSnapshot(collection(db, "polls"), (snapshot) => {
                const pollsTableBody = document.querySelector('#polls-table tbody');
                allPollsData = {}; pollsTableBody.innerHTML = '';
                snapshot.forEach(doc => {
                    const poll = { id: doc.id, ...doc.data() };
                    allPollsData[poll.id] = poll;
                    pollsTableBody.innerHTML += `<tr><td>${poll.title}</td><td>${poll.active ? 'Yes' : 'No'}</td><td>${poll.published ? 'Yes' : 'No'}</td><td class="d-flex gap-1"><button class="btn btn-sm btn-primary edit-poll-btn" data-id="${poll.id}">Edit</button><button class="btn btn-sm btn-warning clone-poll-btn" data-id="${poll.id}">Clone</button><button class="btn btn-sm btn-danger delete-poll-btn" data-id="${poll.id}">Delete</button></td></tr>`;
                });
            });
        }
        
        document.querySelector('#polls-table').addEventListener('click', async e => {
             const pollId = e.target.dataset.id; if (!pollId) return;
             if (e.target.classList.contains('delete-poll-btn')) {
                if (confirm('Are you sure you want to delete this poll? This is irreversible.')) await deleteDoc(doc(db, "polls", pollId));
             } else if (e.target.classList.contains('edit-poll-btn') || e.target.classList.contains('clone-poll-btn')) {
                 const poll = allPollsData[pollId]; if (!poll) return;
                 const isClone = e.target.classList.contains('clone-poll-btn');
                 clearPollForm();
                 pollForm.id.value = isClone ? '' : poll.id;
                 pollForm.title.value = isClone ? `${poll.title} (Clone)` : poll.title;
                 pollForm.start.value = toLocalISOString(poll.startDate.toDate());
                 pollForm.end.value = toLocalISOString(poll.endDate.toDate());
                 pollForm.options.value = poll.options.join('\n');
                 pollForm.active.checked = poll.active; pollForm.published.checked = poll.published;
                 imageBase64 = poll.imageBase64 || null; pollForm.preview.src = imageBase64 || "";
                 pollForm.titleLabel.textContent = isClone ? `Cloning: ${poll.title}` : `Editing: ${poll.title}`;
                 window.scrollTo(0,0);
             }
        });
        
        // --- RESULTS (ADMIN) & AUDIT TRAIL ---
        function loadAdminResults() {
             onSnapshot(collection(db, "polls"), (pollsSnapshot) => {
                const resultsContainer = document.getElementById('results-container');
                if (pollsSnapshot.empty) { resultsContainer.innerHTML = '<p>No polls created yet.</p>'; return; }
                resultsContainer.innerHTML = '';
                pollsSnapshot.forEach(async (pollDoc) => {
                    const poll = { id: pollDoc.id, ...pollDoc.data() };
                    resultsContainer.innerHTML += `<div class="form-card" style="margin-bottom: 1rem;"><h3>${poll.title}</h3><div id="admin-result-content-${poll.id}"><p>Loading results...</p></div></div>`;
                    onSnapshot(query(collection(db, "votes"), where("pollId", "==", poll.id)), (votesSnapshot) => {
                        const results = poll.options.reduce((acc, opt) => ({...acc, [opt]: 0}), {});
                        votesSnapshot.forEach(voteDoc => { if(results.hasOwnProperty(voteDoc.data().choice)) results[voteDoc.data().choice]++; });
                        renderPieChart(document.getElementById(`admin-result-content-${poll.id}`), results);
                    });
                });
             });
        }
        
        function renderPieChart(container, data) {
            container.innerHTML = '';
            const totalVotes = Object.values(data).reduce((s, v) => s + v, 0);
            if (totalVotes === 0) { container.innerHTML = '<p>No votes have been cast for this poll yet.</p>'; return; }
            container.innerHTML = `<div class="pie-chart-container"><canvas width="200" height="200"></canvas><ul class="chart-legend"></ul></div>`;
            const [canvas, legend] = [container.querySelector('canvas'), container.querySelector('ul')];
            const ctx = canvas.getContext('2d');
            let startAngle = -0.5 * Math.PI;
            const colors = ['#003366', '#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8', '#6f42c1'];
            Object.entries(data).forEach(([option, votes], i) => {
                const sliceAngle = (votes / totalVotes) * 2 * Math.PI;
                const endAngle = startAngle + sliceAngle;
                const color = colors[i % colors.length];
                ctx.beginPath(); ctx.moveTo(100, 100); ctx.arc(100, 100, 100, startAngle, endAngle); ctx.closePath(); ctx.fillStyle = color; ctx.fill();
                startAngle = endAngle;
                legend.innerHTML += `<li><div class="legend-color-box" style="background-color: ${color};"></div> ${option}: ${votes} votes (${(votes/totalVotes*100).toFixed(1)}%)</li>`;
            });
        }

        function loadAuditTrail() {
            onSnapshot(collection(db, "votes"), (snapshot) => {
                const auditTableBody = document.querySelector('#audit-table tbody');
                auditData = []; auditTableBody.innerHTML = '';
                snapshot.forEach(doc => {
                    const vote = { id: doc.id, ...doc.data() };
                    auditData.push(vote);
                    auditTableBody.insertAdjacentHTML('afterbegin', `<tr><td>${vote.id}</td><td>${vote.pollId}</td><td>${vote.matric}</td><td>${vote.choice}</td><td>${vote.timestamp.toDate().toLocaleString()}</td></tr>`);
                });
            });
        }
        
        const downloadFile = (content, fileName, contentType) => {
            const a = document.createElement("a");
            a.href = URL.createObjectURL(new Blob([content], { type: contentType }));
            a.download = fileName; a.click(); URL.revokeObjectURL(a.href);
        };
        const escapeCsvValue = (val) => {
            const str = String(val);
            if (/[",\n]/.test(str)) {
                return `"${str.replace(/"/g, '""')}"`;
            }
            return str;
        };

        document.getElementById('export-csv-btn').addEventListener('click', () => {
            if (auditData.length === 0) return;
            const headers = "Vote ID,Poll ID,Matric No.,Choice,Timestamp\n";
            const rows = auditData.map(v => [v.id, v.pollId, v.matric, v.choice, v.timestamp.toDate().toISOString()].map(escapeCsvValue).join(',')).join('\n');
            downloadFile(headers + rows, 'audit_trail.csv', 'text/csv;charset=utf-8;');
        });

        document.getElementById('export-json-btn').addEventListener('click', () => {
             if (auditData.length === 0) return;
             const jsonData = auditData.map(v => ({...v, timestamp: v.timestamp.toDate().toISOString()}));
             downloadFile(JSON.stringify(jsonData, null, 2), 'audit_trail.json', 'application/json');
        });

    </script>
</body>
</html>

Firebase Security Rule

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    // Helper function to check for admin role
    function isAdmin() {
      return exists(/databases/$(database)/documents/users/$(request.auth.token.email.split('@')[0])) &&
             get(/databases/$(database)/documents/users/$(request.auth.token.email.split('@')[0])).data.role == 'admin';
    }

    // Helper function to get user's matriculation number
    function getMatric() {
      return request.auth.token.email.split('@')[0];
    }

    // USERS COLLECTION
    match /users/{matric} {
      // **THE FIX IS HERE:** Corrected getMetric() to getMatric()
      // This allows a logged-in user to read their own data to verify their status.
      allow read: if request.auth != null && (matric == getMatric() || isAdmin());
      
      // Admins can create/delete/write any user
      allow write: if isAdmin();
      
      // A user can update their OWN document only to change the passwordChanged flag
      allow update: if request.auth != null && matric == getMatric()
                    && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['passwordChanged']);
    }

    // POLLS COLLECTION
    match /polls/{pollId} {
      allow read: if request.auth != null;
      allow create, update, delete: if isAdmin();
    }

    // VOTES COLLECTION
    match /votes/{voteId} {
      // Any authenticated user can read vote data for results and history
      allow read: if request.auth != null;

      // Strict rule for creating a new vote
      allow create: if request.auth != null &&
                      request.resource.data.matric == getMatric() &&
                      get(/databases/$(database)/documents/users/$(getMatric())).data.passwordChanged == true &&
                      get(/databases/$(database)/documents/users/$(getMatric())).data.eligible == true &&
                      get(/databases/$(database)/documents/polls/$(request.resource.data.pollId)).data.active == true &&
                      !exists(/databases/$(database)/documents/votes/$(request.resource.data.pollId + '_' + getMatric()));
    }
  }
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *