diff --git "a/ap.jsx" "b/ap.jsx" new file mode 100644--- /dev/null +++ "b/ap.jsx" @@ -0,0 +1,4118 @@ +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
+ +