const express = require('express'); const fs = require('fs'); const path = require('path'); const sass = require('sass'); const app = express(); const BASE = __dirname; const DATA_FILE = path.join(BASE, 'data', 'responses.json'); const CSV_FILE = path.join(BASE, 'data', 'responses.csv'); // Compile SCSS on startup try { const cssDir = path.join(BASE, 'public', 'css'); fs.mkdirSync(cssDir, { recursive: true }); const result = sass.compile(path.join(BASE, 'src', 'styles', 'main.scss'), { style: 'compressed' }); fs.writeFileSync(path.join(cssDir, 'main.css'), result.css); console.log('SCSS compiled successfully'); } catch (err) { console.error('SCSS compile error:', err.message); } // Ensure data directory & files exist (data/ is gitignored, so a fresh clone has neither) fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true }); if (!fs.existsSync(DATA_FILE)) fs.writeFileSync(DATA_FILE, JSON.stringify([], null, 2)); if (!fs.existsSync(CSV_FILE)) fs.writeFileSync(CSV_FILE, buildCSVHeader() + '\n'); // ────────────────────────────────────────── // CSV helpers // ────────────────────────────────────────── function csvEscape(val) { const s = String(val ?? ''); if (s.includes(',') || s.includes('"') || s.includes('\n')) { return '"' + s.replace(/"/g, '""') + '"'; } return s; } function buildCSVHeader() { const cols = ['timestamp', 'grade']; for (let i = 1; i <= 15; i++) cols.push(`case_${i}_likert`); for (let i = 1; i <= 15; i++) cols.push(`case_${i}_order`); for (let i = 1; i <= 15; i++) cols.push(`case_${i}_ms`); cols.push('ahp_w_stability','ahp_w_revenue','ahp_w_capital','ahp_w_plan','ahp_w_social'); cols.push('ahp_lambda_max','ahp_CI','ahp_CR','ahp_cr_warning'); for (let i = 1; i <= 15; i++) cols.push(`match_case_${i}`); cols.push('post_q1','post_q2','post_q3','post_q4','open_response'); return cols.join(','); } function buildCSVRow(payload) { const fields = []; fields.push(payload.meta.timestamp, csvEscape(payload.meta.grade)); // Case judgments ordered by case_id 1-15 const byCase = {}; (payload.intuitive_judgments || []).forEach(j => { byCase[j.case_id] = j; }); for (let i = 1; i <= 15; i++) fields.push(byCase[i]?.judgment ?? ''); for (let i = 1; i <= 15; i++) fields.push(byCase[i]?.presented_order ?? ''); for (let i = 1; i <= 15; i++) fields.push(byCase[i]?.response_time_ms ?? ''); // AHP const ahp = payload.ahp || {}; (ahp.weights || [0,0,0,0,0]).forEach(w => fields.push(Number(w).toFixed(4))); fields.push(Number(ahp.lambda_max).toFixed(4)); fields.push(Number(ahp.CI).toFixed(4)); fields.push(Number(ahp.CR).toFixed(4)); fields.push(ahp.cr_warning_shown ? 1 : 0); // Match by case_id const matchByCase = {}; (payload.comparison || []).forEach(c => { matchByCase[c.case_id] = c.match; }); for (let i = 1; i <= 15; i++) { const m = matchByCase[i]; fields.push(m === true ? 1 : m === false ? 0 : ''); } // Post survey const ps = payload.post_survey || {}; fields.push(ps.q1 ?? '', ps.q2 ?? '', ps.q3 ?? '', ps.q4 ?? ''); fields.push(csvEscape(ps.open_response ?? '')); return fields.join(','); } // ────────────────────────────────────────── // Routes // ────────────────────────────────────────── app.use(express.json({ limit: '1mb' })); app.use(express.static(path.join(BASE, 'public'))); app.post('/api/submit', (req, res) => { try { // Save JSON const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); const submission = { ...req.body, server_timestamp: Date.now() }; data.push(submission); fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)); // Append CSV row const row = buildCSVRow(submission); fs.appendFileSync(CSV_FILE, row + '\n'); res.json({ success: true, id: data.length }); } catch (err) { console.error('Submit error:', err); res.status(500).json({ error: err.message }); } }); app.get('/api/responses', (req, res) => { try { res.json(JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'))); } catch (err) { res.status(500).json({ error: err.message }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));