import * as db from './db'; /** * Default quarterly theme structure for auto-generating a curriculum. * Each quarter has a name and a list of thematic blocks. */ const DEFAULT_QUARTERS = [ { quarter: 1, name: 'Foundation & Governance', themes: [ 'Company Purpose & Values', 'Governance', 'People & Culture', 'Recruitment', ], }, { quarter: 2, name: 'Compliance, Legal & Finance', themes: [ 'Privacy', 'Compliance', 'Quality', 'Finance', ], }, { quarter: 3, name: 'Technology & Operations', themes: [ 'Strategy', 'Infrastructure', 'Workplace', 'Service Management', 'Software Delivery', ], }, { quarter: 4, name: 'Business, Marketing & Sustainability', themes: [ 'Marketing', 'Networking', 'Events', 'Sustainability', 'Year Wrap-up', ], }, ]; /** * Get the current curriculum year based on a date. * Uses the calendar year. */ export function getCurriculumYear(date = new Date()) { return date.getFullYear(); } /** * Get the quarter number (1-4) for a given ISO week number. */ export function getQuarterForWeek(weekNumber) { if (weekNumber <= 13) return 1; if (weekNumber <= 26) return 2; if (weekNumber <= 39) return 3; return 4; } /** * Get the quarter name for a given week number. */ export function getQuarterName(weekNumber) { const q = getQuarterForWeek(weekNumber); return DEFAULT_QUARTERS[q - 1]?.name || `Quarter ${q}`; } /** * Get the assigned topic for a given week from the curriculum. * Returns { topic, curriculumEntry } or { topic: null } if no curriculum exists. */ export async function getCurriculumTopic(weekNumber, year) { const currYear = year ?? getCurriculumYear(); const entry = await db.getCurriculumWeek(currYear, weekNumber); if (!entry || !entry.topic_id) { return { topic: null, curriculumEntry: entry || null }; } // Resolve the topic from the topics collection (ensure it is not excluded) const topics = await db.getTopics(); const topic = topics.find(t => t.id === entry.topic_id && t.learning_relevance !== 'exclude') || null; return { topic, curriculumEntry: entry }; } /** * Get the full curriculum for a year, with resolved topic labels. */ export async function getFullCurriculum(year) { const currYear = year ?? getCurriculumYear(); const entries = await db.getCurriculum(currYear); const topics = await db.getTopics(); const topicMap = Object.fromEntries(topics.map(t => [t.id, t])); return entries.map(entry => ({ ...entry, topic: topicMap[entry.topic_id] || null, })); } /** * Get progress for a user in a given quarter. * Returns { completed, total, percentage }. */ export async function getQuarterProgress(userId, quarter, year) { const currYear = year ?? getCurriculumYear(); const curriculum = await db.getCurriculum(currYear); const quarterWeeks = curriculum.filter(w => w.quarter === quarter); let completed = 0; for (const week of quarterWeeks) { const done = await db.getLearnDone(userId, week.week_number); if (done) completed++; } return { completed, total: quarterWeeks.length, percentage: quarterWeeks.length > 0 ? Math.round((completed / quarterWeeks.length) * 100) : 0, }; } /** * Get overall annual progress for a user. * Returns { completed, total, percentage }. */ export async function getYearProgress(userId, year) { const currYear = year ?? getCurriculumYear(); const curriculum = await db.getCurriculum(currYear); if (curriculum.length === 0) { return { completed: 0, total: 52, percentage: 0 }; } let completed = 0; for (const week of curriculum) { const done = await db.getLearnDone(userId, week.week_number); if (done) completed++; } return { completed, total: curriculum.length, percentage: Math.round((completed / curriculum.length) * 100), }; } /** * Get upcoming weeks from the curriculum (next N weeks after current). */ export async function getUpcomingWeeks(currentWeek, count = 4, year) { const currYear = year ?? getCurriculumYear(); const curriculum = await getFullCurriculum(currYear); return curriculum .filter(w => w.week_number > currentWeek && w.week_number <= currentWeek + count) .sort((a, b) => a.week_number - b.week_number); } /** * Auto-generate a 52-week curriculum from available topics. * Distributes topics evenly across quarters, adds review weeks at 13, 26, 39, 52. */ export async function autoGenerateCurriculum(year) { const currYear = year ?? getCurriculumYear(); const topics = await db.getTopics(); // Filter out 'fact' type topics and 'exclude' relevance topics const learningTopics = topics.filter(t => t.type !== 'fact' && t.learning_relevance !== 'exclude'); const weeks = []; const reviewWeeks = [13, 26, 39, 52]; // Distribute topics across the 48 non-review weeks. let topicIndex = 0; for (let w = 1; w <= 52; w++) { const quarter = getQuarterForWeek(w); if (reviewWeeks.includes(w)) { // Review / recap week weeks.push({ week_number: w, topic_id: '', theme: `Q${quarter} Review`, quarter, is_review_week: true, sort_order: w, }); } else if (topicIndex < learningTopics.length) { const topic = learningTopics[topicIndex]; weeks.push({ week_number: w, topic_id: topic.id, theme: topic.type || 'General', quarter, is_review_week: false, sort_order: w, }); topicIndex++; } else if (learningTopics.length > 0) { // If we have more weeks than topics, cycle through topics again const topic = learningTopics[topicIndex % learningTopics.length]; weeks.push({ week_number: w, topic_id: topic.id, theme: `${topic.type || 'General'} (Deep Dive)`, quarter, is_review_week: false, sort_order: w, }); topicIndex++; } else { // No topics at all weeks.push({ week_number: w, topic_id: '', theme: 'Unassigned', quarter, is_review_week: false, sort_order: w, }); } } await db.bulkSetCurriculum(currYear, weeks); return weeks; } /** * Check if a curriculum exists for the given year. */ export async function hasCurriculum(year) { const currYear = year ?? getCurriculumYear(); const entries = await db.getCurriculum(currYear); return entries.length > 0; }