const API = window.location.origin; function getToken() { return localStorage.getItem("joblin_token"); } function apiHeaders() { const h = { "Content-Type": "application/json" }; const t = getToken(); if (t) h["Authorization"] = `Bearer ${t}`; return h; } async function apiFetch(method, path, body) { const opts = { method, headers: apiHeaders() }; if (body) opts.body = JSON.stringify(body); const r = await fetch(API + path, opts); if (r.status === 401 && !path.startsWith("/api/auth/login")) { localStorage.removeItem("joblin_token"); localStorage.removeItem("joblin_user"); window.location.href = "/login.html"; throw new Error("Unauthorized"); } const text = await r.text(); if (!text && !r.ok) throw new Error("Server error (empty response)"); let data; try { data = JSON.parse(text); } catch (e) { throw new Error((r.status === 500 ? "Server error: " : "Request failed: ") + text.slice(0, 200)); } if (!r.ok) { let msg = "Request failed"; if (typeof data.detail === "string") msg = data.detail; else if (Array.isArray(data.detail)) msg = data.detail.map(e => e.msg || e).join("; "); throw new Error(msg); } return data; } function showToast(msg, type = "success") { let t = document.getElementById("toast"); if (!t) { t = document.createElement("div"); t.id = "toast"; t.className = "toast"; document.body.appendChild(t); } t.className = `toast ${type} show`; t.textContent = msg; clearTimeout(t._timeout); t._timeout = setTimeout(() => t.classList.remove("show"), 3000); } function getCurrentUser() { const u = localStorage.getItem("joblin_user"); return u ? JSON.parse(u) : null; } function setCurrentUser(user) { localStorage.setItem("joblin_user", JSON.stringify(user)); } function updateHeader() { const user = getCurrentUser(); const nameEl = document.getElementById("user-name"); const emailEl = document.getElementById("user-email"); const avatarEl = document.getElementById("avatarInitial"); if (nameEl && user) nameEl.textContent = user.name || user.email; if (emailEl && user) emailEl.textContent = user.email || ""; if (avatarEl && user) { const initial = (user.name || user.email || "U")[0].toUpperCase(); avatarEl.textContent = initial; } const path = window.location.pathname.split("/").pop() || "dashboard.html"; document.querySelectorAll(".nav-item").forEach(a => { a.classList.toggle("active", a.getAttribute("href") === path); }); const isAdminUser = user && (user.is_admin === true || user.is_admin === 1); const adminSections = document.querySelectorAll(".admin-only"); const showAdmin = isAdminUser && adminMode(); adminSections.forEach(el => { el.style.display = showAdmin ? "" : "none"; }); const toggleEl = document.getElementById("adminToggle"); const toggleLabel = document.getElementById("adminToggleLabel"); if (toggleEl && isAdminUser) { toggleEl.style.display = ""; if (toggleLabel) toggleLabel.textContent = adminMode() ? "User View" : "Admin View"; } else if (toggleEl) { toggleEl.style.display = "none"; } } function isAdmin() { const u = getCurrentUser(); return u && (u.is_admin === true || u.is_admin === 1); } function adminMode() { return localStorage.getItem("admin_mode") === "true"; } function toggleAdminMode() { const newVal = adminMode() ? "false" : "true"; localStorage.setItem("admin_mode", newVal); location.reload(); } function togglePwd(id, el) { const inp = document.getElementById(id); const isPwd = inp.type === "password"; inp.type = isPwd ? "text" : "password"; el.innerHTML = isPwd ? "👀" : "👁"; } function logout() { localStorage.removeItem("joblin_token"); localStorage.removeItem("joblin_user"); localStorage.removeItem("admin_mode"); window.location.href = "/login.html"; } function escHtml(s) { if (!s) return ""; return s.replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); } function renderQualityScores(scores) { if (!scores || !Object.keys(scores).length) return ""; const labels = { achievement_density: "Achievement Density", bullet_depth: "Bullet Depth", skills: "Skills", summary: "Summary", education: "Education", cover_letter: "Cover Letter", }; return `
Quality Scores
${Object.entries(scores).map(([k, v]) => { const label = labels[k] || k; const pct = v.pass ? 100 : Math.min(Math.max((v.score || 0) * 100, 0), 100); const barColor = v.pass ? "#059669" : (pct > 50 ? "#d97706" : "#dc2626"); const bgColor = v.pass ? "rgba(5,150,105,0.08)" : (pct > 50 ? "rgba(217,119,6,0.08)" : "rgba(220,38,38,0.08)"); const borderColor = v.pass ? "rgba(5,150,105,0.15)" : (pct > 50 ? "rgba(217,119,6,0.15)" : "rgba(220,38,38,0.15)"); return `
${label} ${v.pass ? "PASS" : "FAIL"}
${escHtml(v.reason)}
`; }).join("")}
`; } function renderJobScore(score) { if (!score || score <= 0) return ""; const pct = Math.round(score); let color = "var(--text-subtle)"; if (pct >= 70) color = "var(--color-primary-500)"; else if (pct >= 40) color = "var(--color-warning-500)"; return `${pct}%`; } const CATEGORY_COLORS = { "data-analytics": "#059669", "monitoring-evaluation": "#0284c7", "ai-machine-learning": "#7c3aed", "software-dev": "#dc2626", "public-health": "#e11d48", "graduate-entry": "#d97706", "ngo-development": "#2563eb", "project-management": "#0f766e", "finance-accounting": "#9333ea", "admin-operations": "#64748b", "human-resources": "#db2777", "sales-marketing": "#ea580c", "customer-service": "#f59e0b", "engineering": "#b45309", "procurement-supply": "#0891b2", "legal-compliance": "#475569", "remote": "#0891b2", "other": "#94a3b8", }; const CATEGORY_NAMES = { "data-analytics": "Data & Analytics", "monitoring-evaluation": "M&E", "ai-machine-learning": "AI/ML", "software-dev": "Software & IT", "public-health": "Public Health", "graduate-entry": "Graduate", "ngo-development": "NGO/Dev", "project-management": "Project Mgmt", "finance-accounting": "Finance & Acct", "admin-operations": "Admin & Ops", "human-resources": "HR", "sales-marketing": "Sales/Marketing", "customer-service": "Customer Svc", "engineering": "Engineering", "procurement-supply": "Procurement", "legal-compliance": "Legal", "remote": "Remote", "other": "Other", }; function renderCategoryBadge(cat) { const color = CATEGORY_COLORS[cat] || "#94a3b8"; const name = CATEGORY_NAMES[cat] || cat; return `${escHtml(name)}`; } function renderJobItem(job, isNew = false) { const newClass = isNew ? "job-new" : ""; const now = new Date(); let ageClass = "job-age-old"; let badge = ""; if (isNew) { const found = new Date(job.date_found.replace(" ", "T")); const hoursOld = (now - found) / 3600000; if (hoursOld < 6) { ageClass = "job-age-fresh"; badge = 'Fresh'; } else { ageClass = "job-age-today"; badge = 'New'; } } return `
${escHtml(job.title)} ${badge}
${escHtml(job.company || "Unknown")} \u00b7 ${escHtml(job.source || "")} ${renderCategoryBadge(job.job_category)}
${renderJobScore(job.match_score)}
`; } function isNewJob(job) { if (!job.date_found) return false; const found = new Date(job.date_found.replace(" ", "T")); const now = new Date(); return (now - found) < 86400000; } function timeSince(dateStr) { if (!dateStr) return ""; const ts = new Date(dateStr.replace(" ", "T")); const now = new Date(); const diffMs = now - ts; const mins = Math.floor(diffMs / 60000); if (mins < 1) return "just now"; if (mins < 60) return mins + "m ago"; const hours = Math.floor(mins / 60); if (hours < 24) return hours + "h ago"; const days = Math.floor(hours / 24); return days + "d ago"; }