feat: implement 52-week annual curriculum system with admin management and automated topic progression

This commit is contained in:
RaymondVerhoef
2026-05-18 19:49:05 +02:00
parent 228d0d7a54
commit 08f5b1fe18
10 changed files with 945 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare } from 'lucide-react';
import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare, CalendarDays } from 'lucide-react';
import Card from '../../components/ui/Card';
import Tag from '../../components/ui/Tag';
import Button from '../../components/ui/Button';
@@ -11,6 +11,7 @@ import KnowledgeGraph from '../../components/admin/KnowledgeGraph';
import ContentManager from '../../components/admin/ContentManager';
import TestManager from '../../components/admin/TestManager';
import TeamManager from '../../components/admin/TeamManager';
import CurriculumManager from '../../components/admin/CurriculumManager';
import { Trash2 } from 'lucide-react';
const Admin = () => {
@@ -54,6 +55,7 @@ const Admin = () => {
{ key: 'sources', icon: Database, label: 'Sources' },
{ key: 'content', icon: Layers, label: 'Content' },
{ key: 'tests', icon: CheckSquare, label: 'Quizzes' },
{ key: 'curriculum', icon: CalendarDays, label: 'Curriculum' },
{ key: 'graph', icon: Network, label: 'Graph' },
{ key: 'team', icon: Users, label: 'Team' },
{ key: 'settings', icon: Settings, label: 'Settings', bottom: true },
@@ -140,6 +142,14 @@ const Admin = () => {
</div>
)}
{activeTab === 'curriculum' && (
<div className="animate-in fade-in duration-300 max-w-5xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Annual Curriculum</h1>
<p className="text-fg-muted mb-8">Plan and manage the 52-week learning schedule. All employees follow the same weekly topic.</p>
<CurriculumManager />
</div>
)}
{activeTab === 'graph' && (
<div className="animate-in fade-in duration-300 h-full flex flex-col">
<h1 className="text-3xl text-teal mb-2">Knowledge Graph</h1>

View File

@@ -6,6 +6,7 @@ import Button from '../components/ui/Button';
import Tag from '../components/ui/Tag';
import * as db from '../lib/db';
import { getAssignedTopic } from '../lib/learningService';
import { getYearProgress, getQuarterName, getQuarterForWeek, hasCurriculum as checkHasCurriculum } from '../lib/curriculumService';
const Dashboard = () => {
const { state } = useApp();
@@ -19,6 +20,8 @@ const Dashboard = () => {
myRank: 0,
myPoints: 0,
activity: [],
yearProgress: null,
hasCurriculum: false,
});
useEffect(() => {
@@ -50,27 +53,103 @@ const Dashboard = () => {
if (pastLearn) activity.push({ type: 'learn', week: w, topic: pastTopic?.label });
}
setDashData({ topic, learnDone, testResult, top3, myRank, myPoints, activity });
// Load curriculum progress
let yearProgress = null;
let curriculumExists = false;
try {
curriculumExists = await checkHasCurriculum();
if (curriculumExists) {
yearProgress = await getYearProgress(currentUser.id);
}
} catch (e) {
console.warn('[Dashboard] Could not load curriculum data:', e.message);
}
setDashData({ topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumExists });
};
load();
}, [currentUser, weekNumber]);
const { topic, learnDone, testResult, top3, myRank, myPoints, activity } = dashData;
const { topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumActive } = dashData;
const currentQuarter = getQuarterForWeek(weekNumber);
const quarterName = getQuarterName(weekNumber);
return (
<div className="p-6 md:p-10 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<header>
<h1 className="text-3xl md:text-5xl mb-2">Welcome, {currentUser?.name}</h1>
<p className="text-fg-muted text-lg">Here is your overview for week {weekNumber}.</p>
<p className="text-fg-muted text-lg">
{curriculumActive
? `Week ${weekNumber} · ${quarterName}`
: `Here is your overview for week ${weekNumber}.`}
</p>
</header>
{/* Annual Progress Bar (only when curriculum exists) */}
{curriculumActive && yearProgress && (
<Card className="border border-bg-warm">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="relative w-14 h-14 flex-shrink-0">
<svg viewBox="0 0 36 36" className="w-full h-full -rotate-90">
<circle cx="18" cy="18" r="15.5" fill="none" stroke="var(--color-bg-warm)" strokeWidth="3" />
<circle
cx="18" cy="18" r="15.5" fill="none"
stroke="var(--color-teal)" strokeWidth="3"
strokeDasharray={`${yearProgress.percentage} 100`}
strokeLinecap="round"
className="transition-all duration-700"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-xs font-bold">
{yearProgress.percentage}%
</span>
</div>
<div>
<h3 className="font-bold text-lg">Annual Progress</h3>
<p className="text-sm text-fg-muted">{yearProgress.completed} of {yearProgress.total} weeks completed</p>
</div>
</div>
<div className="flex items-center gap-3">
<Tag variant="dark" className="text-xs">Q{currentQuarter}</Tag>
<span className="text-sm text-fg-muted">{52 - weekNumber} weeks remaining</span>
</div>
</div>
{/* Visual week progress bar */}
<div className="mt-4 flex gap-[2px] h-2 rounded-full overflow-hidden">
{Array.from({ length: 52 }, (_, i) => {
const w = i + 1;
const isCurrent = w === weekNumber;
const isPast = w < weekNumber;
return (
<div
key={w}
className="flex-1 rounded-sm transition-all"
style={{
backgroundColor: isCurrent
? 'var(--color-teal)'
: isPast
? 'var(--color-teal)'
: 'var(--color-bg-warm)',
opacity: isPast ? 0.4 : 1,
}}
title={`Week ${w}`}
/>
);
})}
</div>
</Card>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="flex flex-col border border-bg-warm" hoverable>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl">Learning</h3>
<p className="text-fg-muted text-sm mt-1">Your topic this week:</p>
<p className="text-fg-muted text-sm mt-1">
{curriculumActive ? `Week ${weekNumber} topic:` : 'Your topic this week:'}
</p>
</div>
{learnDone ? <Tag variant="success">Completed</Tag> : <Tag variant="accent">To Do</Tag>}
</div>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { BookOpen, CheckCircle, Loader, ArrowRight, Plus, Search, ChevronLeft, MessageSquare } from 'lucide-react';
import { BookOpen, CheckCircle, Loader, ArrowRight, Plus, Search, ChevronLeft, MessageSquare, Calendar, TrendingUp } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Link } from 'react-router-dom';
import Card from '../components/ui/Card';
@@ -9,6 +9,7 @@ import Input from '../components/ui/Input';
import LearningContentViewer from '../components/ui/LearningContentViewer';
import { useApp } from '../store/AppContext';
import { getAssignedTopic, generateLearningContent, getCachedContent, generateCustomTopic } from '../lib/learningService';
import { getUpcomingWeeks, getQuarterProgress, getYearProgress, getQuarterName, getQuarterForWeek, hasCurriculum as checkHasCurriculum } from '../lib/curriculumService';
import * as db from '../lib/db';
const Leren = () => {
@@ -37,6 +38,12 @@ const Leren = () => {
const [feedbackText, setFeedbackText] = useState('');
const [feedbackPrompted, setFeedbackPrompted] = useState(false);
// Curriculum state
const [hasCurriculum, setHasCurriculum] = useState(false);
const [upcoming, setUpcoming] = useState([]);
const [quarterProgress, setQuarterProgress] = useState(null);
const [yearProgress, setYearProgress] = useState(null);
useEffect(() => {
if (state.currentUser) {
const load = async () => {
@@ -48,6 +55,24 @@ const Leren = () => {
setAssignedTopic(assigned);
setAllTopics(topics);
if (done) setWeeklyDone(true);
// Load curriculum data
try {
const currExists = await checkHasCurriculum();
setHasCurriculum(currExists);
if (currExists) {
const [upcomingData, qProgress, yProgress] = await Promise.all([
getUpcomingWeeks(state.weekNumber, 4),
getQuarterProgress(state.currentUser.id, getQuarterForWeek(state.weekNumber)),
getYearProgress(state.currentUser.id),
]);
setUpcoming(upcomingData);
setQuarterProgress(qProgress);
setYearProgress(yProgress);
}
} catch (e) {
console.warn('[Learn] Could not load curriculum data:', e.message);
}
};
load();
}
@@ -271,13 +296,17 @@ const Leren = () => {
// ── Overview ──────────────────────────────────────────────
const otherTopics = allTopics.filter(t => t.id !== assignedTopic?.id && t.type !== 'fact');
const currentQuarter = getQuarterForWeek(state.weekNumber);
const currentQuarterName = getQuarterName(state.weekNumber);
return (
<div className="p-4 md:p-8 max-w-5xl mx-auto pb-24 md:pb-8 animate-in fade-in duration-300">
<div className="mb-10">
<h1 className="text-3xl md:text-4xl font-bold text-teal mb-3">Learning Station</h1>
<p className="text-fg-muted text-lg">
You must complete at least 1 topic per week. Feel free to explore more from the library!
{hasCurriculum
? `Week ${state.weekNumber} · ${currentQuarterName}`
: 'Complete at least 1 topic per week. Explore more from the library!'}
</p>
</div>
@@ -287,11 +316,74 @@ const Leren = () => {
</div>
)}
{/* Progress Cards (only shown when curriculum exists) */}
{hasCurriculum && yearProgress && quarterProgress && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{/* Year Progress */}
<Card className="border border-bg-warm text-center p-4">
<div className="relative w-16 h-16 mx-auto mb-2">
<svg viewBox="0 0 36 36" className="w-full h-full -rotate-90">
<circle cx="18" cy="18" r="15.5" fill="none" stroke="var(--color-bg-warm)" strokeWidth="3" />
<circle
cx="18" cy="18" r="15.5" fill="none"
stroke="var(--color-teal)" strokeWidth="3"
strokeDasharray={`${yearProgress.percentage} 100`}
strokeLinecap="round"
className="transition-all duration-700"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-sm font-bold">
{yearProgress.percentage}%
</span>
</div>
<div className="text-xs text-fg-muted">Annual Progress</div>
<div className="text-[10px] text-fg-muted">{yearProgress.completed}/{yearProgress.total} weeks</div>
</Card>
{/* Quarter Progress */}
<Card className="border border-bg-warm text-center p-4">
<div className="relative w-16 h-16 mx-auto mb-2">
<svg viewBox="0 0 36 36" className="w-full h-full -rotate-90">
<circle cx="18" cy="18" r="15.5" fill="none" stroke="var(--color-bg-warm)" strokeWidth="3" />
<circle
cx="18" cy="18" r="15.5" fill="none"
stroke="#7c3aed" strokeWidth="3"
strokeDasharray={`${quarterProgress.percentage} 100`}
strokeLinecap="round"
className="transition-all duration-700"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-sm font-bold">
{quarterProgress.percentage}%
</span>
</div>
<div className="text-xs text-fg-muted">Q{currentQuarter} Progress</div>
<div className="text-[10px] text-fg-muted">{quarterProgress.completed}/{quarterProgress.total} weeks</div>
</Card>
{/* Current Week */}
<Card className="border border-bg-warm text-center p-4 flex flex-col items-center justify-center">
<Calendar size={24} className="text-teal mb-1" />
<div className="text-2xl font-bold text-teal">{state.weekNumber}</div>
<div className="text-xs text-fg-muted">Current Week</div>
</Card>
{/* Status */}
<Card className="border border-bg-warm text-center p-4 flex flex-col items-center justify-center">
<TrendingUp size={24} className={`mb-1 ${weeklyDone ? 'text-teal' : 'text-fg-muted'}`} />
<div className={`text-lg font-bold ${weeklyDone ? 'text-teal' : 'text-fg-muted'}`}>
{weeklyDone ? 'Complete' : 'In Progress'}
</div>
<div className="text-xs text-fg-muted">This Week</div>
</Card>
</div>
)}
{/* Required Topic */}
{assignedTopic && (
<div className="mb-12">
<div className="mb-8">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
Weekly Assignment {weeklyDone && <CheckCircle size={20} className="text-teal" />}
This Week's Topic {weeklyDone && <CheckCircle size={20} className="text-teal" />}
</h2>
<Card
hoverable
@@ -300,9 +392,14 @@ const Leren = () => {
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<Tag variant={weeklyDone ? 'success' : 'accent'} className="mb-2">
{weeklyDone ? 'Completed' : 'Required'}
</Tag>
<div className="flex items-center gap-2 mb-2">
<Tag variant={weeklyDone ? 'success' : 'accent'} className="text-xs">
{weeklyDone ? 'Completed' : 'Required'}
</Tag>
{hasCurriculum && (
<Tag variant="dark" className="text-[10px]">Week {state.weekNumber}</Tag>
)}
</div>
<h3 className="text-2xl font-bold text-teal">{assignedTopic.label}</h3>
<p className="text-fg-muted mt-1">{assignedTopic.description}</p>
</div>
@@ -314,7 +411,34 @@ const Leren = () => {
</div>
)}
{/* Upcoming Schedule (only when curriculum exists) */}
{hasCurriculum && upcoming.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<Calendar size={20} className="text-fg-muted" /> Coming Up
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
{upcoming.map(week => (
<Card key={week.week_number} className="border border-bg-warm p-4">
<div className="flex items-center justify-between mb-2">
<Tag variant="dark" className="text-[10px] font-mono">Week {week.week_number}</Tag>
{week.is_review_week && <Tag variant="accent" className="text-[10px]">Review</Tag>}
</div>
{week.topic ? (
<>
<h4 className="font-medium text-sm leading-tight">{week.topic.label}</h4>
<p className="text-xs text-fg-muted mt-1">{week.theme}</p>
</>
) : week.is_review_week ? (
<h4 className="font-medium text-sm text-purple-600">{week.theme}</h4>
) : (
<h4 className="text-sm text-fg-muted italic">Unassigned</h4>
)}
</Card>
))}
</div>
</div>
)}
{/* Other Available Topics */}
{otherTopics.length > 0 && (