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 ? `${row.screenshot.name}` : ``; const color = CATEGORY_COLORS[row.standard.split("-")[0]] ?? "#888"; const noteHTML = row.evalNote?.trim() ? `Observation: ${row.evalNote.trim()}` : ""; return ` ${row.id} ${row.standard} ${row.observation} ${row.possibleMarks ?? "—"} ${row.earnedScore !== null ? `${row.earnedScore} / ${row.possibleMarks ?? "—"}
${rowPct ?? "—"}%` : "—"} ${status} ${imgHTML} ${noteHTML}`; }).join("\n"); return ` QA Report — ${username}

QA Automation Test Report

Evaluator: ${username}
Testing Started: ${fmtDateTime(testingStart)}
Terminated: ${fmtDateTime(testingEnd)}
Duration: ${fmtDuration(testingEnd - testingStart)}

Summary

${rows.length}
Questions
${totalEarned}/${totalPossible}
Marks Earned
${pct}%
Overall Score
${passed}
Passed (≥80%)
${failed}
Failed (<50%)

Question Results

${rowsHTML}
#StandardObservationPossible MarksEarned ScoreStatusScreenshot