/* global Chart */
/**
* Fetches initial job applications from a JSON data file.
* @async
* @returns {Promise<Object[]>} Array of job application objects (empty array on error).
*/
async function fetchApplications() {
try {
// Try absolute path first (for Netlify), then relative path (for local/test)
let response = await fetch('/data/applications.json');
if (!response.ok) {
response = await fetch('../data/applications.json');
}
if (!response.ok) throw new Error('Failed to load applications.json');
return await response.json();
} catch (error) {
console.error('Error loading applications:', error);
return [];
}
}
/**
* Main function that initializes the dashboard and renders all charts and statistics
* Loads application data from localStorage and displays relevant metrics
*/
document.addEventListener('DOMContentLoaded', async () => {
console.log('Dashboard: DOMContentLoaded fired');
// Check if Chart.js is available
if (typeof Chart === 'undefined') {
console.error('Chart.js is not loaded!');
return;
}
console.log('Dashboard: Chart.js is available');
// Seed localStorage from JSON file if empty
if (!localStorage.getItem('applications')) {
console.log('Dashboard: Loading sample data...');
const jobs = await fetchApplications();
localStorage.setItem('applications', JSON.stringify(jobs));
console.log('Dashboard: Sample data loaded:', jobs.length, 'applications');
}
// Load applications from localStorage or use empty array if none exists
const applications = JSON.parse(localStorage.getItem('applications')) || [];
console.log('Dashboard: Working with', applications.length, 'applications');
// === DOM References ===
const totalApplicationsEl = document.querySelector(
'.content-card:nth-child(1) .content-card-stat'
);
const totalApplicationsDetailsEl = document.querySelector(
'.content-card:nth-child(1) .content-card-details p'
);
const interviewsScheduledEl = document.querySelector(
'.content-card:nth-child(2) .content-card-stat'
);
const interviewsScheduledDetailsEl = document.querySelector(
'.content-card:nth-child(2) .content-card-details p'
);
const activeProcessesEl = document.querySelector(
'.content-card:nth-child(3) .content-card-stat'
);
const activeProcessesDetailsEl = document.querySelector(
'.content-card:nth-child(3) .content-card-details p'
);
const offersReceivedEl = document.querySelector(
'.content-card:nth-child(4) .content-card-stat'
);
const offersReceivedDetailsEl = document.querySelector(
'.content-card:nth-child(4) .content-card-details p'
);
const applicationsChartEl = document.getElementById('applicationsChart');
const statusChartEl = document.getElementById('statusChart');
console.log('Dashboard: DOM elements found:', {
totalApplicationsEl: !!totalApplicationsEl,
applicationsChartEl: !!applicationsChartEl,
statusChartEl: !!statusChartEl
});
/**
* Returns applications submitted during the current week
* @returns {Array} Applications from the current week
*/
const getThisWeeksApplications = () => {
const today = new Date();
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay()); // Set to Sunday
startOfWeek.setHours(0, 0, 0, 0);
return applications.filter((app) => {
if (!app.dateApplied) return false;
const appDate = new Date(app.dateApplied);
return appDate >= startOfWeek;
});
};
/**
* Returns applications submitted during the previous week
* @returns {Array} Applications from the previous week
*/
const getPreviousWeeksApplications = () => {
const today = new Date();
const startOfThisWeek = new Date(today);
startOfThisWeek.setDate(today.getDate() - today.getDay());
startOfThisWeek.setHours(0, 0, 0, 0);
const startOfPrevWeek = new Date(startOfThisWeek);
startOfPrevWeek.setDate(startOfThisWeek.getDate() - 7);
const endOfPrevWeek = new Date(startOfThisWeek);
endOfPrevWeek.setMilliseconds(-1);
return applications.filter((app) => {
if (!app.dateApplied) return false;
const appDate = new Date(app.dateApplied);
return appDate >= startOfPrevWeek && appDate <= endOfPrevWeek;
});
};
/**
* Returns upcoming interviews (scheduled for today or later)
* @returns {Array} Applications with upcoming interviews
*/
const getUpcomingInterviews = () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return applications.filter((app) => {
if (app.status !== 'Interviewing') return false;
// Check if interview date exists in importantDates
if (app.importantDates) {
const interviewDateStr =
app.importantDates['Technical Interview'] ||
app.importantDates['Phone Interview'] ||
app.importantDates['Interview'];
if (interviewDateStr) {
const interviewDate = new Date(interviewDateStr);
return interviewDate >= today;
}
}
return false;
});
};
/**
* Returns applications awaiting feedback (Applied or Screening status)
* @returns {Array} Applications awaiting feedback
*/
const getAwaitingFeedback = () => {
return applications.filter(
(app) => app.status === 'Applied' || app.status === 'Screening'
);
};
/**
* Returns offers received in the last 7 days
* @returns {Array} Recently received offers
*/
const getNewOffers = () => {
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
return applications.filter((app) => {
if (app.status !== 'Offer' && app.status !== 'Offered') return false;
// If we don't have status update date, check the application date
const dateToCheck = app.statusUpdateDate || app.dateApplied;
if (!dateToCheck) return false;
const date = new Date(dateToCheck);
return date >= oneWeekAgo;
});
};
/**
* Organizes applications data by month for time-series chart
* @returns {Object} Object containing months labels and datasets
*/
const getApplicationsByMonth = () => {
// Define all months
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
// Initialize counters for each month
const appsByMonth = Array(12).fill(0);
const interviewsByMonth = Array(12).fill(0);
// Current year
const currentYear = new Date().getFullYear();
applications.forEach((app) => {
if (app.dateApplied) {
const appDate = new Date(app.dateApplied);
// Only count applications from current year
if (appDate.getFullYear() === currentYear) {
const month = appDate.getMonth();
appsByMonth[month]++;
// Count interviews and offers
if (
app.status === 'Interviewing' ||
app.status === 'Offer' ||
app.status === 'Offered'
) {
interviewsByMonth[month]++;
}
}
}
});
// Find the first and last month with data
let firstMonthWithData = 0;
let lastMonthWithData = 11;
for (let i = 0; i < 12; i++) {
if (appsByMonth[i] > 0) {
firstMonthWithData = i;
break;
}
}
for (let i = 11; i >= 0; i--) {
if (appsByMonth[i] > 0) {
lastMonthWithData = i;
break;
}
}
// Include at least 6 months or up to current month
const currentMonth = new Date().getMonth();
firstMonthWithData = Math.min(
firstMonthWithData,
Math.max(0, currentMonth - 5)
);
lastMonthWithData = Math.max(lastMonthWithData, Math.min(currentMonth, 11));
// Extract relevant months
const relevantMonths = [];
const applicationsData = [];
const interviewsData = [];
for (let i = firstMonthWithData; i <= lastMonthWithData; i++) {
relevantMonths.push(months[i]);
applicationsData.push(appsByMonth[i]);
interviewsData.push(interviewsByMonth[i]);
}
// If no data, provide at least 3 months of empty data
if (relevantMonths.length === 0) {
const currentMonth = new Date().getMonth();
for (let i = Math.max(0, currentMonth - 2); i <= currentMonth; i++) {
relevantMonths.push(months[i]);
applicationsData.push(0);
interviewsData.push(0);
}
}
return {
months: relevantMonths,
applications: applicationsData,
interviews: interviewsData,
};
};
// === Update Dashboard Statistics ===
// Check if elements exist before updating content
if (totalApplicationsEl) {
totalApplicationsEl.textContent = applications.length;
}
if (interviewsScheduledEl) {
const interviewCount = applications.filter(
(app) => app.status === 'Interviewing'
).length;
interviewsScheduledEl.textContent = interviewCount;
}
if (activeProcessesEl) {
const activeCount = applications.filter((app) =>
['Applied', 'Screening', 'Interviewing'].includes(app.status)
).length;
activeProcessesEl.textContent = activeCount;
}
if (offersReceivedEl) {
const offerCount = applications.filter(
(app) => app.status === 'Offer' || app.status === 'Offered'
).length;
offersReceivedEl.textContent = offerCount;
}
// Update dynamic stats below the main numbers
if (totalApplicationsDetailsEl) {
const thisWeekApps = getThisWeeksApplications();
const prevWeekApps = getPreviousWeeksApplications();
const weekOverWeekChange = thisWeekApps.length - prevWeekApps.length;
const changeText =
weekOverWeekChange >= 0
? `+${weekOverWeekChange}`
: weekOverWeekChange;
totalApplicationsDetailsEl.textContent = `${changeText} this week`;
}
if (interviewsScheduledDetailsEl) {
const upcomingInterviews = getUpcomingInterviews();
interviewsScheduledDetailsEl.textContent = `${upcomingInterviews.length} upcoming`;
}
if (activeProcessesDetailsEl) {
const awaitingFeedback = getAwaitingFeedback();
activeProcessesDetailsEl.textContent = `Awaiting feedback for ${awaitingFeedback.length}`;
}
if (offersReceivedDetailsEl) {
const newOffers = getNewOffers();
offersReceivedDetailsEl.textContent =
newOffers.length > 0
? `${newOffers.length} new offer${
newOffers.length > 1 ? 's' : ''
}!`
: 'No new offers';
}
// === Chart Colors ===
/**
* Gets CSS color from root variables
* @param {string} varName - CSS variable name
* @returns {string} Color value or fallback
*/
const getCSSColor = (varName) =>
getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
const chartColors = {
primary: getCSSColor('--md-sys-color-primary') || '#4f46e5',
secondary: getCSSColor('--md-sys-color-secondary') || '#06b6d4',
surface: getCSSColor('--md-sys-color-surface') || '#fff',
outline: getCSSColor('--md-sys-color-outline') || '#e5e7eb',
onSurfaceVariant:
getCSSColor('--md-sys-color-on-surface-variant') || '#6b7280',
pieSliceColors: [
'#4f46e5',
'#06b6d4',
'#10b981',
'#f59e0b',
'#ef4444',
'#9ca3af',
],
};
// === Applications Over Time Chart ===
if (applicationsChartEl) {
const monthlyData = getApplicationsByMonth();
console.log('Dashboard: Monthly chart data:', monthlyData);
// Destroy any existing chart instance before creating a new one
// Use try-catch for compatibility with different Chart.js versions
try {
const existingChart = Chart.getChart ? Chart.getChart(applicationsChartEl) : null;
if (existingChart) {
existingChart.destroy();
}
} catch (error) {
// Fallback for older Chart.js versions - check for existing chart instance
if (applicationsChartEl._chartjs) {
applicationsChartEl._chartjs.destroy();
}
}
try {
new Chart(applicationsChartEl.getContext('2d'), {
type: 'line',
data: {
labels: monthlyData.months,
datasets: [
{
label: 'Applications Sent',
data: monthlyData.applications,
borderColor: chartColors.primary,
backgroundColor: chartColors.primary + '33',
tension: 0.3,
fill: true,
},
{
label: 'Interviews Scheduled',
data: monthlyData.interviews,
borderColor: chartColors.secondary,
backgroundColor: chartColors.secondary + '33',
tension: 0.3,
fill: true,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
grid: { color: chartColors.outline || '#e5e7eb' },
ticks: {
color: chartColors.onSurfaceVariant || '#6b7280',
stepSize: 1
},
},
x: {
grid: { color: chartColors.outline || '#e5e7eb' },
ticks: { color: chartColors.onSurfaceVariant || '#6b7280' },
},
},
plugins: {
legend: { labels: { color: chartColors.onSurfaceVariant || '#6b7280' } },
},
},
});
console.log('Dashboard: Applications chart created successfully');
} catch (error) {
console.error('Error creating applications chart:', error);
// Fallback: show a message in the chart container
applicationsChartEl.parentElement.innerHTML = '<p style="text-align: center; color: #6b7280; margin-top: 100px;">Chart data will appear after adding applications</p>';
}
}
// === Dynamic Status Breakdown Chart ===
if (statusChartEl) {
const statusLabels = [
'Applied',
'Screening',
'Interviewing',
'Offer',
'Wishlist',
'Withdrawn',
'Rejected',
'Ghosted',
];
const statusCounts = {
Applied: 0,
Screening: 0,
Interviewing: 0,
Offer: 0,
Wishlist: 0,
Withdrawn: 0,
Rejected: 0,
Ghosted: 0,
};
// Handle both 'Offer' and 'Offered' statuses
applications.forEach((app) => {
let status = app.status || 'Applied'; // fallback if missing
// Map 'Offered' to 'Offer' for consistency
if (status === 'Offered') status = 'Offer';
if (statusCounts[status] !== undefined) {
statusCounts[status]++;
}
});
console.log('Dashboard: Status counts:', statusCounts);
// Filter out statuses with 0 counts for better visualization
const activeLabels = [];
const activeData = [];
const activeColors = [];
const colorMap = [
'#4f46e5', // Applied - Primary blue
'#06b6d4', // Screening - Cyan
'#10b981', // Interviewing - Green
'#f59e0b', // Offer - Amber
'#8b5cf6', // Wishlist - Purple
'#6b7280', // Withdrawn - Gray
'#ef4444', // Rejected - Red
'#9ca3af', // Ghosted - Light gray
];
statusLabels.forEach((label, index) => {
if (statusCounts[label] > 0) {
activeLabels.push(label);
activeData.push(statusCounts[label]);
activeColors.push(colorMap[index]);
}
});
try {
new Chart(statusChartEl.getContext('2d'), {
type: 'doughnut',
data: {
labels: activeLabels.length > 0 ? activeLabels : ['No Data'],
datasets: [
{
label: 'Application Status',
data: activeData.length > 0 ? activeData : [1],
backgroundColor: activeColors.length > 0 ? activeColors : ['#e5e7eb'],
borderColor: chartColors.surface || '#fff',
borderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: chartColors.onSurfaceVariant || '#6b7280' },
},
tooltip: {
callbacks: {
label: function (context) {
if (activeData.length === 0) return 'No applications yet';
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage =
total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
},
},
},
},
},
});
console.log('Dashboard: Status chart created successfully');
} catch (error) {
console.error('Error creating status chart:', error);
// Fallback: show a message in the chart container
statusChartEl.parentElement.innerHTML = '<p style="text-align: center; color: #6b7280; margin-top: 100px;">Status breakdown will appear after adding applications</p>';
}
}
// === Reset Button ===
const resetBtn = document.getElementById('resetDataBtn');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
const confirmReset = confirm(
'Are you sure you want to clear all application data?'
);
if (confirmReset) {
try {
localStorage.removeItem('applications');
location.reload();
} catch (error) {
console.error('Error clearing application data:', error);
alert('Failed to clear application data. Please try again.');
}
}
});
}
// Add dashboard-loaded class to signal that initialization is complete
document.body.classList.add('dashboard-loaded');
console.log('Dashboard: Initialization complete, dashboard-loaded class added');
// Listen for localStorage changes (when user adds/edits applications)
window.addEventListener('storage', (e) => {
if (e.key === 'applications') {
console.log('Dashboard: Applications data changed, reloading...');
location.reload();
}
});
// Also check for focus events (when user returns to dashboard)
window.addEventListener('focus', () => {
const currentApps = JSON.parse(localStorage.getItem('applications')) || [];
if (currentApps.length !== applications.length) {
console.log('Dashboard: Application count changed, reloading...');
location.reload();
}
});
});