import React, { useState, useCallback, useEffect, useRef } from "react";
import { QUESTIONS } from "./data/questions";
import LoginPage from "./LoginPage";
import {
getSession, logout, startTesting, terminateTesting, resetTesting,
getScores, saveScores, clearScores,
saveSessionQuestions, getSessionQuestions, clearSessionQuestions,
} from "./auth";
import {
dbGetAllUsers, dbUpdateRole, dbResetPassword, dbDeleteUser, dbSignup,
dbGetSubmissions, dbSaveSubmission, dbDeleteSubmission,
dbGetProfiles, dbSaveProfile, dbDeleteProfile,
dbGetProducts, dbSaveProduct, dbDeleteProduct,
dbGetUserProducts, dbSetUserProducts,
dbGetTasks, dbSaveTask, dbDeleteTask,
dbGetTickets, dbSaveTicket, dbDeleteTicket,
dbGetNotifications, dbSaveNotification, dbMarkNotificationRead, dbMarkAllNotificationsRead, dbClearNotifications,
dbAppendLog, dbReadLogs, dbPruneLogs,
} from "./db";
import { driveEnabled, uploadPhoto, deleteSubmissionPhotos } from "./drive";
import logo from "./assets/logo.png";
import "./App.css";
// ── Constants ─────────────────────────────────────────────────────────────────
const SESSION_DURATION = 12 * 60 * 60 * 1000;
const TESTER_DURATION = 60 * 60 * 1000;
function getDuration(s) {
return s?.mode === "tester" ? TESTER_DURATION : SESSION_DURATION;
}
const CATEGORY_COLORS = {
FUNC: "#4f86c6", UI: "#6bbf8e", API: "#e08c4a", DATA: "#9b73c8",
PERF: "#e05c5c", SEC: "#c8a73a", INT: "#4ab8c8", REG: "#888",
};
// Fixed "random-looking" preset marks for the default question set
const PRESET_MARKS = [10,15,10,20,15,10,10,15,10,20,15,10,10,20,15,10,15,20,10,15,20,10,15,10,15,20,10,15,10,15];
function getActiveQuestions() {
return QUESTIONS;
}
function blankRow(q) {
return {
id: q.id, standard: q.standard, observation: q.observation,
possibleMarks: q.possibleMarks ?? PRESET_MARKS[q.id - 1] ?? 10,
earnedScore: null, evalNote: "", screenshot: null,
};
}
function parseCSVLine(line) {
const result = []; let cur = ""; let inQ = false;
for (const ch of line) {
if (ch === '"') { inQ = !inQ; }
else if (ch === ',' && !inQ) { result.push(cur); cur = ""; }
else cur += ch;
}
result.push(cur);
return result;
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmtDuration(ms) {
const t = Math.max(0, Math.floor(ms / 1000));
const h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), s = t % 60;
return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
}
function fmtDateTime(ts) {
return new Date(ts).toLocaleString("en-US", {
year:"numeric", month:"long", day:"numeric", hour:"2-digit", minute:"2-digit",
});
}
function fileToBase64(file) {
return new Promise((resolve) => {
const r = new FileReader();
r.onload = (e) => resolve(e.target.result);
r.readAsDataURL(file);
});
}
function dataURLtoBlob(dataURL) {
const [header, data] = dataURL.split(",");
const mime = header.match(/:(.*?);/)[1];
const binary = atob(data);
const arr = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
return new Blob([arr], { type: mime });
}
function getMissingFields(rows) {
return rows.reduce((acc, r) => {
const f = [];
if (r.earnedScore === null) f.push("earned score");
if (!r.screenshot) f.push("screenshot");
if (f.length) acc.push({ id: r.id, standard: r.standard, fields: f });
return acc;
}, []);
}
// ── Report builder ────────────────────────────────────────────────────────────
function buildReportHTML(rows, session) {
const { username, testingStart, testingEnd } = session;
const totalPossible = rows.reduce((s, r) => s + (r.possibleMarks ?? 0), 0);
const totalEarned = rows.reduce((s, r) => s + (r.earnedScore ?? 0), 0);
const pct = totalPossible > 0 ? ((totalEarned / totalPossible) * 100).toFixed(1) : "—";
const passed = rows.filter(r => r.possibleMarks && r.earnedScore !== null && r.earnedScore / r.possibleMarks >= 0.8).length;
const warned = rows.filter(r => { if (!r.possibleMarks || r.earnedScore === null) return false; const p = r.earnedScore / r.possibleMarks; return p >= 0.5 && p < 0.8; }).length;
const failed = rows.filter(r => r.possibleMarks && r.earnedScore !== null && r.earnedScore / r.possibleMarks < 0.5).length;
const rowsHTML = rows.map((row) => {
const rowPct = row.possibleMarks && row.earnedScore !== null ? Math.round((row.earnedScore / row.possibleMarks) * 100) : null;
const status = rowPct !== null ? (rowPct >= 80 ? "PASS" : rowPct < 50 ? "FAIL" : "WARN") : "N/A";
const imgSrc = row.screenshot?.base64 ?? row.screenshot?.url;
const imgHTML = imgSrc
? ``
: `—`;
const color = CATEGORY_COLORS[row.standard.split("-")[0]] ?? "#888";
const noteHTML = row.evalNote?.trim()
? `
| # | Standard | Observation | Possible Marks | Earned Score | Status | Screenshot |
|---|