extractSources(jobs); renderJobs(jobs); updateSourceFilter(); const hasMore = jobs.length === jobsPageSize; pageInfo.textContent = `Showing ${jobsPage * jobsPageSize + 1}–${jobsPage * jobsPageSize + jobs.length} of ${jobs.length + (hasMore ? '+' : '')}`; prevBtn.disabled = jobsPage === 0; nextBtn.disabled = !hasMore; pagination.classList.remove('hidden'); table.classList.remove('hidden'); loading.classList.add('hidden'); } catch (e) { console.error('Failed to load jobs:', e); showToast('Failed to load jobs', 'error'); loading.classList.add('hidden'); empty.classList.remove('hidden'); } } function extractSources(jobs) { jobs.forEach(item => allSources.add(item.job.source)); } function updateSourceFilter() { const select = document.getElementById('jobSourceFilter'); const current = select.value; select.innerHTML = '' + Array.from(allSources).sort().map(s => ``).join(''); select.value = current; } function renderJobs(jobs) { const tbody = document.getElementById('jobsTableBody'); if (jobs.length === 0) { document.getElementById('jobsTable').classList.add('hidden'); document.getElementById('jobsEmpty').classList.remove('hidden'); return; } document.getElementById('jobsTable').classList.remove('hidden'); document.getElementById('jobsEmpty').classList.add('hidden'); tbody.innerHTML = jobs.map(item => { const job = item.job; const app = item.application; const score = job.match_score; let scoreClass = 'score-low'; if (score >= 75) scoreClass = 'score-high'; else if (score >= 50) scoreClass = 'score-medium'; let statusHtml = 'No Application'; if (app) { statusHtml = `${app.status}`; } return ` ${score}% ${escapeHtml(job.title)} ${escapeHtml(job.company)} ${escapeHtml(job.location)} ${escapeHtml(job.source)} ${formatDate(job.found_date)} ${statusHtml}
${app && app.resume_path ? `` : ''} ${job.source_url ? `` : ''}
`; }).join(''); } function prevJobsPage() { if (jobsPage > 0) { jobsPage--; loadJobs(); } } function nextJobsPage() { jobsPage++; loadJobs(); } function sortJobs(field) { const icons = document.querySelectorAll('[id^="sort"]'); icons.forEach(icon => icon.textContent = ''); const icon = document.getElementById('sort' + field.charAt(0).toUpperCase() + field.slice(1) + 'Icon'); if (currentSort.field === field) { currentSort.dir = currentSort.dir === 'desc' ? 'asc' : 'desc'; } else { currentSort.field = field; currentSort.dir = 'desc'; } icon.textContent = currentSort.dir === 'desc' ? '↓' : '↑'; jobsPage = 0; loadJobs(); } async function openJobModal(jobId) { try { const data = await fetchAPI(`/jobs/${jobId}`); renderJobModal(data.job, data.application); document.getElementById('jobModal').classList.remove('hidden'); document.body.style.overflow = 'hidden'; } catch (e) { console.error('Failed to load job:', e); showToast('Failed to load job details', 'error'); } } function closeModal() { document.getElementById('jobModal').classList.add('hidden'); document.body.style.overflow = ''; } function renderJobModal(job, app) { const title = `${job.title} at ${job.company}`; document.getElementById('modalJobTitle').textContent = title; let html = `

Company

${escapeHtml(job.company)}

Location

${escapeHtml(job.location)}

Remote

${escapeHtml(job.remote)}

Source

${escapeHtml(job.source)}

Match Score

${job.match_score}%

Description

${escapeHtml(job.description || 'No description available')}

Requirements

${escapeHtml(job.requirements || 'No requirements listed')}
`; if (app) { html += `

Application Status

Status

${app.status}

ATS Score

${app.ats_score}%

Applied

${app.applied_date || '—'}

Follow-up

${app.follow_up_date || '—'}

${app.notes ? `

Notes

${escapeHtml(app.notes)}

` : ''} ${app.rejection_reason ? `

Rejection Reason

${escapeHtml(app.rejection_reason)}

` : ''} ${app.interview_date ? `

Interview

${app.interview_date} (${app.interview_round || 'Round 1'})

` : ''} ${app.offer_details ? `

Offer

${escapeHtml(app.offer_details)}

` : ''}
`; } if (job.source_url) { html += `
View Original Posting
`; } html += '
'; document.getElementById('modalJobContent').innerHTML = html; } async function loadApplications() { try { const res = await fetch(`${API}/jobs?status=shortlisted&status=prepared&status=submitted&status=interview&status=rejected&limit=200`); const jobs = await res.json(); renderApplications(jobs.filter(j => j.application)); } catch (e) { console.error('Failed to load applications:', e); showToast('Failed to load applications', 'error'); } } function renderApplications(jobs) { const tbody = document.getElementById('appsTableBody'); const empty = document.getElementById('appsEmpty'); const table = document.getElementById('appsTable'); if (jobs.length === 0) { table.classList.add('hidden'); empty.classList.remove('hidden'); return; } table.classList.remove('hidden'); empty.classList.add('hidden'); const statusOrder = { interview: 0, submitted: 1, prepared: 2, shortlisted: 3, rejected: 4 }; jobs.sort((a, b) => statusOrder[a.application.status] - statusOrder[b.application.status]); tbody.innerHTML = jobs.map(item => { const job = item.job; const app = item.application; return ` ${escapeHtml(job.title)} ${escapeHtml(job.company)} ${app.status} ${job.match_score}% ${app.ats_score}% ${app.applied_date || '—'} ${app.follow_up_date || '—'}
${app.resume_path ? `` : ''}
`; }).join(''); } async function loadResumes() { try { const resumes = await fetchAPI('/resumes'); renderResumes(resumes); } catch (e) { console.error('Failed to load resumes:', e); showToast('Failed to load resumes', 'error'); } } function renderResumes(resumes) { const container = document.getElementById('resumesGrid'); const empty = document.getElementById('resumesEmpty'); if (resumes.length === 0) { container.innerHTML = ''; empty.classList.remove('hidden'); return; } empty.classList.add('hidden'); container.innerHTML = resumes.map(r => `
${r.date}

${r.filename.replace('.md', '').replace(/_/g, ' ')}

Click to view

`).join(''); } async function openResumeModal(filename) { try { const data = await fetchAPI(`/resumes/${filename}`); document.getElementById('modalResumeTitle').textContent = filename.replace('.md', '').replace(/_/g, ' '); document.getElementById('modalResumeContent').innerHTML = marked.parse(data.content); document.getElementById('resumeModal').classList.remove('hidden'); document.body.style.overflow = 'hidden'; } catch (e) { console.error('Failed to load resume:', e); showToast('Failed to load resume', 'error'); } } function closeResumeModal() { document.getElementById('resumeModal').classList.add('hidden'); document.body.style.overflow = ''; } async function loadReports() { try { const reports = await fetchAPI('/reports'); renderReports(reports); const badge = document.getElementById('reportsCountBadge'); if (badge) { badge.textContent = reports.length; badge.classList.toggle('hidden', reports.length === 0); } } catch (e) { console.error('Failed to load reports:', e); showToast('Failed to load reports', 'error'); } } function renderReports(reports) { const container = document.getElementById('reportsList'); const empty = document.getElementById('reportsEmpty'); if (reports.length === 0) { container.innerHTML = ''; empty.classList.remove('hidden'); return; } empty.classList.add('hidden'); container.innerHTML = reports.map(r => `

${r.date}

${r.filename}

`).join(''); } async function openReportModal(filename, date) { try { const reports = await fetchAPI('/reports'); const r = reports.find(x => x.filename === filename); if (r) { document.getElementById('modalReportTitle').textContent = date; document.getElementById('modalReportContent').innerHTML = marked.parse(r.content); document.getElementById('reportModal').classList.remove('hidden'); document.body.style.overflow = 'hidden'; } } catch (e) { console.error('Failed to load report:', e); showToast('Failed to load report', 'error'); } } function closeReportModal() { document.getElementById('reportModal').classList.add('hidden'); document.body.style.overflow = ''; } function openSettings() { document.getElementById('settingsModal').classList.remove('hidden'); document.body.style.overflow = 'hidden'; } function closeSettings() { document.getElementById('settingsModal').classList.add('hidden'); document.body.style.overflow = ''; } async function refreshAll() { showToast('Refreshing data...', 'info'); await Promise.all([loadStats(), loadJobs(), loadApplications(), loadResumes(), loadReports()]); showToast('Data refreshed', 'success'); } function exportJobs() { const status = document.getElementById('jobStatusFilter').value; const minScore = document.getElementById('jobScoreFilter').value; const search = document.getElementById('jobSearch').value; const source = document.getElementById('jobSourceFilter').value; const params = new URLSearchParams({ limit: 1000 }); if (status) params.append('status', status); if (minScore) params.append('min_score', minScore); if (search) params.append('search', search); window.open(`${API}/jobs?${params}&format=csv`, '_blank'); showToast('Export started', 'success'); } function exportApplications() { window.open(`${API}/jobs?status=shortlisted&status=prepared&status=submitted&status=interview&status=rejected&limit=1000&format=csv`, '_blank'); showToast('Export started', 'success'); } function renderScoreDistribution() { const filter = document.getElementById('scoreDistFilter').value; let jobs = allJobsCache; if (filter === 'today') { const today = new Date().toISOString().split('T')[0]; jobs = jobs.filter(j => j.job.found_date === today); } else if (filter === 'week') { const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0]; jobs = jobs.filter(j => j.job.found_date >= weekAgo); } const bins = { '0-20': 0, '21-40': 0, '41-60': 0, '61-75': 0, '76-85': 0, '86-100': 0 }; jobs.forEach(item => { const s = item.job.match_score; if (s <= 20) bins['0-20']++; else if (s <= 40) bins['21-40']++; else if (s <= 60) bins['41-60']++; else if (s <= 75) bins['61-75']++; else if (s <= 85) bins['76-85']++; else bins['86-100']++; }); const max = Math.max(...Object.values(bins), 1); const container = document.getElementById('scoreDistributionChart'); container.innerHTML = Object.entries(bins).map(([range, count]) => `
${range} ${count}
`).join(''); } function renderPipelineFunnel() { const stats = { 'Total Jobs': document.getElementById('stat-total_jobs')?.textContent || 0, 'Shortlisted': document.getElementById('stat-shortlisted')?.textContent || 0, 'Prepared': document.getElementById('stat-prepared')?.textContent || 0, 'Submitted': document.getElementById('stat-submitted')?.textContent || 0, 'Interviews': document.getElementById('stat-interviews')?.textContent || 0, }; const max = Math.max(...Object.values(stats).map(Number), 1); const colors = ['blue', 'yellow', 'blue', 'indigo', 'primary']; const container = document.getElementById('pipelineFunnel'); container.innerHTML = Object.entries(stats).map(([label, value], i) => { const v = Number(value); const pct = max > 0 ? (v / max) * 100 : 0; return `
${label}
${v}
`; }).join(''); } function renderTopCompanies() { const companyCounts = {}; allJobsCache.forEach(item => { const c = item.job.company; companyCounts[c] = (companyCounts[c] || 0) + 1; }); const top = Object.entries(companyCounts).sort((a, b) => b[1] - a[1]).slice(0, 10); const container = document.getElementById('topCompanies'); if (top.length === 0) { container.innerHTML = '

No data yet

'; return; } container.innerHTML = top.map(([company, count]) => `
${escapeHtml(company)} ${count}
`).join(''); } function renderRecentActivity() { const recent = allJobsCache.slice(0, 10); const container = document.getElementById('recentActivity'); if (recent.length === 0) { container.innerHTML = '

No recent activity

'; return; } container.innerHTML = recent.map(item => { const job = item.job; const app = item.application; return `

${escapeHtml(job.title)}

${escapeHtml(job.company)} • ${formatDate(job.found_date)}

${app ? `${app.status}` : 'New'}
`; }).join(''); } function formatDate(dateStr) { if (!dateStr) return '—'; try { const d = new Date(dateStr); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } catch { return dateStr; } } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showToast(message, type = 'info') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); const colors = { success: 'border-primary-500 bg-primary-500/10', error: 'border-red-500 bg-red-500/10', info: 'border-blue-500 bg-blue-500/10', warning: 'border-yellow-500 bg-yellow-500/10' }; const icons = { success: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', error: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z', info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', warning: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' }; toast.className = `toast card border p-3 flex items-center gap-3 min-w-[280px] max-w-md ${colors[type]}`; toast.innerHTML = `${escapeHtml(message)}`; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; setTimeout(() => toast.remove(), 300); }, 4000); } document.getElementById('jobModal').addEventListener('click', (e) => { if (e.target.id === 'jobModal') closeModal(); }); document.getElementById('resumeModal').addEventListener('click', (e) => { if (e.target.id === 'resumeModal') closeResumeModal(); }); document.getElementById('reportModal').addEventListener('click', (e) => { if (e.target.id === 'reportModal') closeReportModal(); }); document.getElementById('settingsModal').addEventListener('click', (e) => { if (e.target.id === 'settingsModal') closeSettings(); }); document.getElementById('sidebarOverlay').addEventListener('click', toggleSidebar);