File size: 5,221 Bytes
bb5c88a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 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.");
  }
});