chowdary1's picture
Deploy AI Interviewer Pro Max v1.1 - Clean build with psutil fix
86d71bd
Raw History Blame Contribute Delete
8.44 kB
/**
* User Data Service - SINGLE SOURCE OF TRUTH
*
* Provides unified data access for Dashboard, Profile, and Analytics pages.
* Uses backend API as primary source with localStorage fallback.
*
* CRITICAL: Both Dashboard and Profile MUST use this service.
* DO NOT fetch data separately in components.
*/
import { analyticsApi, profileApi, resumeApi, userApi } from './api';
import {
getCachedAnalytics,
getStoredInterviews,
getInterviewHistory,
syncWithBackend,
} from './interviewStorage';
// ===========================================
// DATA SHAPE DEFINITIONS
// ===========================================
/**
* @typedef {Object} UserAnalytics
* @property {number} total_interviews
* @property {number} completed_interviews
* @property {number|null} average_score
* @property {number|null} best_score
* @property {string|null} last_interview_at
* @property {string} trend - 'improving' | 'declining' | 'stable'
* @property {number} completion_rate
* @property {number} total_questions_answered
* @property {Object} skill_summary
*/
/**
* @typedef {Object} InterviewRecord
* @property {string} id
* @property {string} target_role
* @property {number|null} score
* @property {string} status
* @property {string} created_at
* @property {string} session_type
* @property {string} difficulty
* @property {number} duration
* @property {Array} strengths
* @property {Array} weaknesses
* @property {boolean} has_recording
*/
// ===========================================
// ANALYTICS COMPUTATION (Used by Dashboard & Profile)
// ===========================================
/**
* Compute analytics from interview array
* SINGLE function used by both Dashboard and Profile
*
* @param {Array} interviews - Array of interview records
* @returns {UserAnalytics}
*/
export function computeAnalytics(interviews = []) {
const completed = interviews.filter(i =>
i.status === 'completed' || i.status === 'completing'
);
const scores = completed
.filter(i => i.score != null && i.score > 0)
.map(i => Number(i.score));
// Calculate trend (compare last 3 vs previous 3)
let trend = 'stable';
if (scores.length >= 6) {
const recent = scores.slice(0, 3).reduce((a, b) => a + b, 0) / 3;
const older = scores.slice(3, 6).reduce((a, b) => a + b, 0) / 3;
if (recent > older + 5) trend = 'improving';
else if (recent < older - 5) trend = 'declining';
}
// Calculate completion rate
const completionRate = interviews.length > 0
? Math.round((completed.length / interviews.length) * 100)
: 0;
// Calculate total questions
const totalQuestions = completed.reduce((sum, i) => {
return sum + (i.total_questions || i.questions_answered || 0);
}, 0);
return {
total_interviews: interviews.length,
completed_interviews: completed.length,
average_score: scores.length > 0
? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
: null,
best_score: scores.length > 0 ? Math.max(...scores) : null,
last_interview_at: interviews.length > 0 ? interviews[0].created_at : null,
trend,
completion_rate: completionRate,
total_questions_answered: totalQuestions,
};
}
// ===========================================
// UNIFIED DATA LOADERS
// ===========================================
/**
* Load all user data for Dashboard/Profile
* Uses backend API with localStorage fallback
*
* @returns {Promise<{analytics: UserAnalytics, interviews: Array, resumes: Array}>}
*/
export async function loadUserData() {
let analytics = getCachedAnalytics(); // Start with cache
let interviews = [];
let resumes = [];
// Try backend first
try {
// Load all data in parallel
const [analyticsRes, historyRes, resumesRes] = await Promise.allSettled([
userApi.getAnalytics(),
profileApi.getInterviewHistory(50, 0),
resumeApi.getAll(),
]);
// Process analytics
if (analyticsRes.status === 'fulfilled' && analyticsRes.value.success) {
analytics = analyticsRes.value.analytics || analytics;
}
// Process interview history
if (historyRes.status === 'fulfilled' && historyRes.value.success) {
interviews = historyRes.value.interviews || [];
// Sync to localStorage for future fallback
syncWithBackend(interviews);
} else {
// Fallback to localStorage
interviews = getStoredInterviews().map(normalizeInterviewRecord);
}
// Process resumes
if (resumesRes.status === 'fulfilled' && resumesRes.value.success) {
resumes = resumesRes.value.resumes || [];
}
// If backend analytics failed but we have interviews, compute locally
if (!analytics.total_interviews && interviews.length > 0) {
analytics = computeAnalytics(interviews);
}
} catch (error) {
console.error('[UserDataService] Failed to load data:', error);
// Full fallback to localStorage
interviews = getStoredInterviews().map(normalizeInterviewRecord);
analytics = computeAnalytics(interviews);
}
return { analytics, interviews, resumes };
}
/**
* Load dashboard-specific data (quick stats + recent interviews)
* Optimized for dashboard which needs less data
*
* @returns {Promise<{analytics: UserAnalytics, recentInterviews: Array}>}
*/
export async function loadDashboardData() {
let analytics = getCachedAnalytics();
let recentInterviews = getInterviewHistory(5); // Start with cache
try {
// Load analytics from backend
const analyticsRes = await userApi.getAnalytics();
if (analyticsRes.success && analyticsRes.analytics) {
analytics = analyticsRes.analytics;
}
// Load recent history
const historyRes = await userApi.getHistory(5, 0);
if (historyRes.success && historyRes.interviews?.length > 0) {
recentInterviews = historyRes.interviews;
syncWithBackend(historyRes.interviews);
}
} catch (error) {
console.warn('[UserDataService] Dashboard API failed, using cache');
// Fallback already set
}
return { analytics, recentInterviews };
}
/**
* Load profile page data (full history + resumes)
*
* @returns {Promise<{analytics: UserAnalytics, interviews: Array, resumes: Array, user: Object}>}
*/
export async function loadProfileData() {
return loadUserData();
}
// ===========================================
// HELPERS
// ===========================================
/**
* Normalize interview record to consistent shape
* Handles variations between backend and localStorage formats
*/
function normalizeInterviewRecord(record) {
return {
id: record.id || record.interviewId || record.session_id,
target_role: record.target_role || record.targetRole || record.job_role || 'Interview',
score: record.score ?? null,
status: record.status || 'completed',
created_at: record.created_at || record.date || record.createdAt,
session_type: record.session_type || 'mixed',
difficulty: record.difficulty || record.difficulty_level || 'medium',
duration: record.duration || record.duration_minutes || 0,
strengths: record.strengths || [],
weaknesses: record.weaknesses || [],
has_recording: record.has_recording || false,
total_questions: record.total_questions || record.statistics?.total_questions || 0,
questions_answered: record.questions_answered || record.statistics?.answered || 0,
};
}
/**
* Get formatted analytics for Profile stats display
*/
export function getFormattedStats(analytics, resumeCount = 0) {
return {
total_interviews: analytics?.total_interviews || 0,
completed_interviews: analytics?.completed_interviews || 0,
average_score: analytics?.average_score || 0,
total_resumes: resumeCount,
best_score: analytics?.best_score || 0,
completion_rate: analytics?.completion_rate || 0,
trend: analytics?.trend || 'stable',
};
}
export default {
computeAnalytics,
loadUserData,
loadDashboardData,
loadProfileData,
getFormattedStats,
};