// BOU 2026 - Best Project voting app // Serves the voting page and logs every vote (Name, UID, Email, Project, Timestamp) // into an Excel file on disk, one row per voter (re-voting updates their row). const path = require("path"); const fs = require("fs"); const express = require("express"); const ExcelJS = require("exceljs"); const PORT = process.env.PORT || 3000; const ADMIN_KEY = process.env.ADMIN_KEY || "change-me"; // set a real secret before deploying const VOTES_FILE = path.join(__dirname, "data", "votes.xlsx"); const PROJECTS = [ { id: "1", name: "Global Account – DAS (Adecco)", who: "Sathiaseelan Arumugam" }, { id: "2", name: "PCB & IC Package Team", who: "Syam" }, { id: "3", name: "Product Engineering / Embedded", who: "Munajat Abdul Hadi" }, { id: "4", name: "CIS", who: "Shunmuganainar Chockalingam" }, { id: "5", name: "ATE", who: "Kother" }, { id: "6", name: "Manufacturing POD", who: "Rohan Chowdhary" }, { id: "7", name: "Manufacturing", who: "Hasrul Hasni" }, ]; const PROJECT_IDS = new Set(PROJECTS.map((p) => p.id)); const HEADERS = ["Name", "UID", "Email", "Project ID", "Project Name", "Timestamp"]; // --- tiny write queue so concurrent votes never corrupt the Excel file --- let writeQueue = Promise.resolve(); function withWriteLock(fn) { const result = writeQueue.then(fn, fn); writeQueue = result.catch(() => {}); return result; } async function loadWorkbook() { const wb = new ExcelJS.Workbook(); if (fs.existsSync(VOTES_FILE)) { await wb.xlsx.readFile(VOTES_FILE); } else { fs.mkdirSync(path.dirname(VOTES_FILE), { recursive: true }); const ws = wb.addWorksheet("Votes"); ws.addRow(HEADERS); ws.getRow(1).font = { bold: true }; } const ws = wb.getWorksheet("Votes") || wb.addWorksheet("Votes"); if (ws.rowCount === 0) { ws.addRow(HEADERS); ws.getRow(1).font = { bold: true }; } return { wb, ws }; } async function recordVote({ name, uid, email, projectId }) { return withWriteLock(async () => { const { wb, ws } = await loadWorkbook(); const project = PROJECTS.find((p) => p.id === projectId); // find an existing row for this UID (column 2) so re-voting updates it let existingRow = null; ws.eachRow((row, rowNumber) => { if (rowNumber === 1) return; if (String(row.getCell(2).value) === String(uid)) existingRow = row; }); const values = [name, uid, email, project.id, project.name, new Date().toISOString()]; if (existingRow) { existingRow.values = [, ...values]; // keep 1-based column alignment } else { ws.addRow(values); } await wb.xlsx.writeFile(VOTES_FILE); }); } async function tallyResults() { const { ws } = await loadWorkbook(); const counts = {}; let total = 0; const voters = []; ws.eachRow((row, rowNumber) => { if (rowNumber === 1) return; const projectId = String(row.getCell(4).value || ""); if (!projectId) return; counts[projectId] = (counts[projectId] || 0) + 1; total += 1; voters.push({ name: row.getCell(1).value, uid: row.getCell(2).value, email: row.getCell(3).value, projectId, }); }); return { counts, total, voters }; } // --- app --- const app = express(); app.use(express.json()); app.use(express.static(path.join(__dirname, "public"))); app.get("/api/projects", (req, res) => res.json(PROJECTS)); app.post("/api/vote", async (req, res) => { const { name, uid, email, projectId } = req.body || {}; if (!name || !uid || !email || !projectId) { return res.status(400).json({ ok: false, error: "Name, UID, email and a project choice are all required." }); } if (!/^\S+@\S+\.\S+$/.test(email)) { return res.status(400).json({ ok: false, error: "That email address doesn't look valid." }); } if (!PROJECT_IDS.has(String(projectId))) { return res.status(400).json({ ok: false, error: "Unknown project." }); } try { await recordVote({ name: String(name).trim(), uid: String(uid).trim(), email: String(email).trim(), projectId: String(projectId) }); res.json({ ok: true }); } catch (err) { console.error(err); res.status(500).json({ ok: false, error: "Could not save your vote. Please try again." }); } }); function requireAdmin(req, res, next) { const key = req.query.key || req.headers["x-admin-key"]; if (key !== ADMIN_KEY) return res.status(401).json({ ok: false, error: "Invalid admin key." }); next(); } app.get("/api/results", requireAdmin, async (req, res) => { try { const { counts, total } = await tallyResults(); res.json({ ok: true, projects: PROJECTS, counts, total }); } catch (err) { console.error(err); res.status(500).json({ ok: false, error: "Could not load results." }); } }); app.get("/api/export", requireAdmin, async (req, res) => { if (!fs.existsSync(VOTES_FILE)) { return res.status(404).json({ ok: false, error: "No votes recorded yet." }); } res.download(VOTES_FILE, "bou2026-votes.xlsx"); }); app.listen(PORT, () => { console.log(`BOU 2026 voting app listening on port ${PORT}`); if (ADMIN_KEY === "change-me") { console.warn("WARNING: set a real ADMIN_KEY environment variable before sharing this app."); } });