diff --git a/AI_AGENT.md b/AI_AGENT.md
index 90567d7..412cead 100644
--- a/AI_AGENT.md
+++ b/AI_AGENT.md
@@ -129,17 +129,17 @@ The platform ships a global chatbot avatar called **R42**, rendered as the Respe
* **AI token budget.** If you see `[Pipeline] AI returned non-JSON response` in the logs, the response was truncated. Increase the topic cap prompt constraint before raising `max_tokens`.
* **PocketBase auto-cancellation is OFF.** `pb.autoCancellation(false)` is set globally in `src/lib/pb.js`. Never re-enable it — it causes abort errors during concurrent fetches in React StrictMode.
-## 12. Annual Curriculum System
-The platform uses a **52-week annual curriculum** so every employee covers all knowledge-base topics in one calendar year.
+## 12. 26-Week Versioned Curriculum System
+The platform uses a **26-week perpetual curriculum cycle** so every employee covers the knowledge base in focused, thematic weekly blocks. Cycle 1 covers weeks 1-26, Cycle 2 replays the same schedule, and so on.
-* **Service:** `src/lib/curriculumService.js` — curriculum engine with topic lookup, progress tracking, and auto-generation.
-* **DB functions:** `db.getCurriculum(year)`, `db.getCurriculumWeek(year, week)`, `db.setCurriculumWeek(...)`, `db.bulkSetCurriculum(year, weeks[])`.
-* **Admin UI:** `src/components/admin/CurriculumManager.jsx` — accessed via the "Curriculum" tab in the admin panel. Admins can auto-generate a schedule from KB topics or manually assign topics per week.
-* **Same topic for everyone:** All employees study the same topic each week — this is by design to enable team discussion and shared quizzes.
-* **Quarterly structure:** Weeks 1–13 (Q1), 14–26 (Q2), 27–39 (Q3), 40–52 (Q4). Review/recap weeks at 13, 26, 39, 52.
-* **Fallback:** If no curriculum exists for the current year, `getAssignedTopic()` falls back to the legacy hash-based assignment for backward compatibility.
-* **Progress tracking:** `getYearProgress(userId)` and `getQuarterProgress(userId, quarter)` compute completion from the `learn_progress` collection against the curriculum.
-* **Auto-generate:** `autoGenerateCurriculum(year)` distributes all non-fact topics across 48 content weeks + 4 review weeks. If there are fewer topics than weeks, they cycle. If more, excess topics remain in the self-service library.
-* **Do not remove the hash fallback** — it ensures the platform works even without a configured curriculum.
+* **Service:** `src/lib/curriculumService.js` — curriculum engine, week resolution, AI generation, version lifecycle.
+* **DB Collections:** `curriculum_versions` holds the generated JSON schedules. `topics` includes `theme`, `complexity_weight`, and `difficulty` for generation input.
+* **Version Lifecycle:** `draft` -> `active` -> `superseded`. Only one active version exists at a time.
+* **Admin UI:** `src/components/admin/CurriculumManager.jsx` — accessed via the "Curriculum" tab. Admins trigger an AI-generated draft, preview the 26-week schedule, and confirm it to activate it globally.
+* **Calendar-Driven Weeks:** The current week (1-26) and cycle (1, 2, 3...) are derived via modulo arithmetic from the absolute ISO calendar week in `curriculumService.js`. All employees share the same schedule.
+* **Topic Enrichment:** The Curriculum Manager includes a one-off AI enrichment step to assign `theme` and `complexity_weight` to topics missing them before curriculum generation.
+* **Progress tracking:** `getYearProgress(userId, isoWeekNumber)` computes completion for the *current 26-week cycle*.
+* **Fallback:** If no active curriculum version exists, `getAssignedTopic()` falls back to the legacy hash-based assignment. Do not remove the hash fallback.
+* **Deferred Features (V2):** Per-employee staggered start dates, cycle 2+ personalization, and prerequisite DAG sorting are deferred to future iterations to keep the MVP lean.
Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged.
diff --git a/curriculum-spec-agnostic.md b/curriculum-spec-agnostic.md
new file mode 100644
index 0000000..4d238d6
--- /dev/null
+++ b/curriculum-spec-agnostic.md
@@ -0,0 +1,334 @@
+# Curriculum specification
+
+## Purpose
+
+This document defines the functional behaviour of a perpetual AI-generated
+learning curriculum. It is implementation-agnostic — no assumptions are made
+about programming language, framework, database, or AI provider. Any system
+that satisfies these functional requirements is a valid implementation.
+
+---
+
+## Concepts
+
+**Knowledge base (KB)**
+The source of truth for all learning content. Organised as a two-level
+hierarchy: Themes (broad subject areas) containing Topics (specific concepts).
+Topics carry metadata that informs curriculum sequencing.
+
+**Theme**
+A broad subject area. One Theme = one weekly session. A Theme contains one
+or more Topics.
+
+**Topic**
+An atomic unit of knowledge within a Theme. Carries:
+- a complexity weight (1–5 scale, 1 = introductory, 5 = advanced)
+- a difficulty label (introductory / intermediate / advanced)
+- relationships to other topics: prerequisite, related, contrast
+
+**Curriculum**
+A versioned 26-week schedule mapping one Theme per week. The curriculum is
+generated by AI from the KB, not manually authored. Admins review and
+finetune it before it is applied.
+
+**Curriculum version**
+Each generation produces a new version. Only one version is active at a time.
+Previous versions are retained for audit purposes.
+
+**Cycle**
+One full pass through the 26-week curriculum. After completing week 26,
+an employee begins cycle 2. Cycles continue indefinitely.
+
+**Employee curriculum state**
+Tracks each employee's position: current cycle, current week, start date,
+and which curriculum version they are on. Employees have independent start
+dates — there are no cohorts.
+
+**Completed week**
+A week where the employee has recorded at least one session completion.
+Completed weeks are immutable — no regeneration or version change may alter
+a completed week's record.
+
+---
+
+## Functional requirements
+
+### 1. Curriculum generation
+
+The system must be able to generate a 26-week curriculum from the current
+published KB.
+
+**Trigger conditions:**
+- Manually by an admin
+- Automatically when new Topics are published to the KB
+
+**Inputs to the generator:**
+- All published Themes, each with their ordered Topic list
+- Per Topic: title, complexity weight, difficulty, prerequisite relationships
+- If regenerating: reason for regeneration
+
+**Generation process:**
+
+Step 1 — Pre-process topic order within each Theme
+Before any AI involvement, resolve the order of Topics within each Theme
+using their prerequisite relationships. Build a directed graph of
+prerequisite edges and apply a topological sort. The result is an ordered
+topic list per Theme where prerequisites always precede dependent Topics.
+If a cycle is detected in the graph (which should not occur in a well-formed
+KB), fall back to ordering by complexity weight ascending.
+
+This pre-processing offloads prerequisite reasoning from the AI — the AI
+receives already-ordered topic lists and focuses only on Theme sequencing
+across weeks.
+
+Step 2 — AI sequences Themes across 26 weeks
+The AI receives the pre-processed KB snapshot and produces a 26-week
+schedule. The AI must follow these sequencing rules:
+
+- Every Theme must appear at least once across the 26 weeks
+- Themes with more Topics may span multiple weeks or recur within the 26 weeks
+- Foundational Themes precede dependent ones
+- Complexity increases progressively: introductory Themes in the first half,
+ advanced Themes in the second half
+- If the KB contains more Topics than 26 weeks can cover in depth, prioritise
+ breadth in cycle 1 — every Theme covered, key Topics per Theme
+- Each week slot has an estimated session duration (15–45 minutes)
+- Each week slot includes a one-sentence rationale explaining its position
+
+**Output:**
+A draft 26-week schedule with one Theme and an ordered Topic subset per week,
+estimated duration per week, and a rationale per week.
+
+**Validation:**
+All Theme and Topic references in the generated schedule must resolve to
+existing published KB entries. Any reference that does not resolve must
+cause the generation to be retried or rejected — never persisted.
+
+---
+
+### 2. Admin review and confirmation
+
+A generated curriculum is always a draft first. It is not applied to any
+employee until an admin explicitly confirms it.
+
+**Admin capabilities before confirmation:**
+- Preview the full proposed 26-week schedule
+- View the rationale for each week's Theme assignment
+- Reorder weeks (swap Theme assignments between weeks)
+- Add or remove Topics from a week's Topic subset
+- Annotate any week with free-text notes
+- Compare the proposed schedule against the currently active version
+
+**Confirmation:**
+When admin confirms, the new version becomes active and is applied to all
+employees whose current week falls within the regenerated range (see
+section 4 — versioning).
+
+**Rejection:**
+If admin rejects the draft, it is discarded and no changes are applied.
+
+---
+
+### 3. Perpetual cycling
+
+The curriculum runs indefinitely. After an employee completes week 26, cycle 2
+begins automatically on the latest active curriculum version.
+
+**Cycle 2 and beyond are not identical to cycle 1.**
+The generator must vary subsequent cycles to reinforce rather than repeat.
+When generating a cycle 2+ schedule for an employee, the generator receives
+additional input:
+
+- Which micro learning types the employee has used across their history
+- Which micro learning types the employee has not yet used
+- Which Topics the employee engaged with minimally (completed only the
+ minimum required interaction)
+
+The generator uses this input to:
+- Vary the Theme sequence from the previous cycle
+- Surface underused micro learning types as recommended formats
+- Schedule low-engagement Topics earlier in the new cycle to give them
+ more visibility
+
+---
+
+### 4. Versioning and version transitions
+
+**One active version at all times.**
+Previous versions are archived, not deleted.
+
+**Completed weeks are frozen.**
+When a new curriculum version is applied, only future unstarted weeks are
+updated. Any week the employee has already started or completed retains its
+original content. The boundary is the employee's current week at the moment
+the new version is applied.
+
+**Version transition logic:**
+When a new version is confirmed:
+- For each active employee: identify their current week N
+- Weeks 1 through N-1 are already completed — untouched
+- Week N onward is replaced with the new version's schedule
+- The employee continues from week N on the new version seamlessly
+
+**Regeneration triggers:**
+Two events should prompt a regeneration:
+1. New Topics are published to the KB (new content available for scheduling)
+2. Admin manually requests regeneration
+
+In both cases, the new version must be previewed and confirmed by an admin
+before being applied. Regeneration is never automatic.
+
+---
+
+### 5. Employee curriculum state
+
+Each employee has an independent curriculum state:
+
+| Attribute | Description |
+|---|---|
+| Current cycle | Which pass through the 26-week schedule (starts at 1) |
+| Current week | Which week within the cycle (1–26) |
+| Start date | When this employee began their curriculum (rolling) |
+| Active version | Which curriculum version the employee's future weeks draw from |
+
+**State transitions:**
+- Week completion: current week increments by 1
+- Week 26 completion: cycle increments by 1, current week resets to 1,
+ active version updates to latest, cycle variant generated
+- Version applied: active version updates for weeks not yet started
+
+---
+
+### 6. Coverage guarantee
+
+The curriculum must cover the entire published KB within one cycle.
+Every published Theme must appear at least once in the 26-week schedule.
+Coverage is verified after generation and before the draft is made available
+for admin review. A schedule that fails coverage verification is rejected
+and regenerated.
+
+Coverage statistics must be surfaced to the admin during preview:
+- Total Themes in KB vs Themes scheduled
+- Total Topics in KB vs Topics scheduled across all weeks
+
+---
+
+### 7. Estimated session duration
+
+Each week slot carries an estimated duration in minutes (15–45 minute range).
+This is an AI estimate based on Topic count and complexity weights for that
+week. It is informational — it guides employees on time commitment but does
+not enforce a time limit.
+
+---
+
+## Data model (logical)
+
+These are logical entities. Implementation may map them to any storage system.
+
+**CurriculumVersion**
+```
+id
+version_number integer, increments on each generation
+status draft | active | superseded
+generated_at datetime
+generation_reason string
+confirmed_by user reference
+confirmed_at datetime
+```
+
+**CurriculumWeek**
+```
+id
+curriculum_version → CurriculumVersion
+week_number 1–26
+theme → Theme
+topics ordered list of → Topic
+estimated_duration minutes (15–45)
+week_rationale string (AI-generated, one sentence)
+admin_notes string (free text, admin-authored)
+```
+
+**EmployeeCurriculumState**
+```
+id
+employee → User
+current_cycle integer (starts at 1)
+current_week integer (1–26)
+start_date datetime
+active_version → CurriculumVersion
+```
+
+---
+
+## AI prompt requirements
+
+The following requirements apply to any AI model used for generation,
+regardless of provider.
+
+**Input the AI must receive:**
+- Full KB snapshot with Themes and pre-ordered Topic lists
+- Per Topic: complexity weight, difficulty, relationship types
+- Cycle number (1 for first cycle, 2+ for subsequent)
+- For cycle 2+: employee history (types used, types not used, low-engagement topics)
+
+**Output the AI must produce:**
+- Exactly 26 week slots
+- Each slot: theme reference, ordered topic references, estimated duration, rationale
+- Structured format suitable for machine parsing (JSON or equivalent)
+- No explanatory prose outside the structured output
+
+**Constraints on AI behaviour:**
+- Temperature should be set to deterministic or near-deterministic (0 or equivalent)
+- Output must be validated before persistence — never persist unvalidated AI output
+- On validation failure: retry once with a stricter prompt, then surface an error
+ to the operator rather than persisting a partial or malformed schedule
+- The AI must not invent Theme or Topic references — all references must exist
+ in the provided KB snapshot
+
+---
+
+## Admin capabilities summary
+
+| Capability | When available |
+|---|---|
+| Trigger manual regeneration | Any time |
+| Preview proposed schedule | After generation, before confirmation |
+| View week rationale | During preview |
+| Reorder weeks | During preview |
+| Edit topic subset per week | During preview |
+| Add admin notes per week | During preview and after confirmation |
+| Confirm version | During preview |
+| Reject draft | During preview |
+| View coverage statistics | During preview |
+| View active schedule | Any time |
+| View version history | Any time |
+
+---
+
+## Behaviours that must never occur
+
+- A completed week's content changes after the employee has completed it
+- A curriculum version is applied without admin confirmation
+- AI output is persisted without validation against the KB
+- An employee's cycle increments without week 26 being completed
+- Two versions are active simultaneously
+- A Theme is absent from a 26-week schedule when it exists in the published KB
+- The admin is not shown a preview before a new version is applied
+
+---
+
+## Acceptance criteria
+
+1. Generate a curriculum from a KB with 5 or more Themes → exactly 26 weeks produced
+2. All published Themes appear at least once in the schedule
+3. Topic order within each week respects prerequisite relationships
+4. Coverage statistics correctly reflect KB vs schedule coverage
+5. A draft version is available for preview before any employee is affected
+6. Admin can reorder two weeks and confirm → reordered schedule applied
+7. New Topics published to KB → regeneration triggered → admin sees preview →
+ confirms → employees at week N see weeks 1 to N unchanged, weeks N+1 onward updated
+8. Employee completes week 26 → cycle 2 begins → sequence differs from cycle 1
+9. Cycle 2 schedule surfaces Topics the employee engaged with minimally
+10. Two employees with different start dates and current weeks both receive
+ correct version transitions independently when a new version is applied
diff --git a/pb_migrations/1780600000_curriculum_v2.js b/pb_migrations/1780600000_curriculum_v2.js
new file mode 100644
index 0000000..f5a4d9e
--- /dev/null
+++ b/pb_migrations/1780600000_curriculum_v2.js
@@ -0,0 +1,202 @@
+///
+migrate((app) => {
+ // ── 1. Create curriculum_versions collection ──────────────────────────────
+ const curriculumVersions = new Collection({
+ "createRule": "",
+ "deleteRule": "",
+ "fields": [
+ {
+ "autogeneratePattern": "[a-z0-9]{15}",
+ "hidden": false,
+ "id": "text3208210256",
+ "max": 15,
+ "min": 15,
+ "name": "id",
+ "pattern": "^[a-z0-9]+$",
+ "presentable": false,
+ "primaryKey": true,
+ "required": true,
+ "system": true,
+ "type": "text"
+ },
+ {
+ "hidden": false,
+ "id": "number_version_number",
+ "max": null,
+ "min": 1,
+ "name": "version_number",
+ "onlyInt": true,
+ "presentable": true,
+ "required": true,
+ "system": false,
+ "type": "number"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text_status",
+ "max": 20,
+ "min": 0,
+ "name": "status",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": true,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text_generation_reason",
+ "max": 0,
+ "min": 0,
+ "name": "generation_reason",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text_confirmed_by",
+ "max": 0,
+ "min": 0,
+ "name": "confirmed_by",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text_confirmed_at",
+ "max": 0,
+ "min": 0,
+ "name": "confirmed_at",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ },
+ {
+ "hidden": false,
+ "id": "json_schedule",
+ "maxSize": 0,
+ "name": "schedule",
+ "presentable": false,
+ "required": true,
+ "system": false,
+ "type": "json"
+ },
+ {
+ "hidden": false,
+ "id": "json_coverage_stats",
+ "maxSize": 0,
+ "name": "coverage_stats",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "json"
+ },
+ {
+ "hidden": false,
+ "id": "autodate2990389176",
+ "name": "created",
+ "onCreate": true,
+ "onUpdate": false,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ },
+ {
+ "hidden": false,
+ "id": "autodate3332085495",
+ "name": "updated",
+ "onCreate": true,
+ "onUpdate": true,
+ "presentable": false,
+ "system": false,
+ "type": "autodate"
+ }
+ ],
+ "id": "pbc_curriculum_v2",
+ "indexes": [],
+ "listRule": "",
+ "name": "curriculum_versions",
+ "system": false,
+ "type": "base",
+ "updateRule": "",
+ "viewRule": ""
+ });
+
+ app.save(curriculumVersions);
+
+ // ── 2. Add theme, complexity_weight, difficulty to topics ─────────────────
+ const topics = app.findCollectionByNameOrId("topics");
+
+ topics.fields.add(new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text_theme",
+ "max": 0,
+ "min": 0,
+ "name": "theme",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }));
+
+ topics.fields.add(new Field({
+ "hidden": false,
+ "id": "number_complexity_weight",
+ "max": 5,
+ "min": 1,
+ "name": "complexity_weight",
+ "onlyInt": true,
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "number"
+ }));
+
+ topics.fields.add(new Field({
+ "autogeneratePattern": "",
+ "hidden": false,
+ "id": "text_difficulty",
+ "max": 20,
+ "min": 0,
+ "name": "difficulty",
+ "pattern": "",
+ "presentable": false,
+ "primaryKey": false,
+ "required": false,
+ "system": false,
+ "type": "text"
+ }));
+
+ app.save(topics);
+
+}, (app) => {
+ // ── Rollback: remove curriculum_versions collection ────────────────────────
+ const curriculumVersions = app.findCollectionByNameOrId("curriculum_versions");
+ app.delete(curriculumVersions);
+
+ // ── Rollback: remove new fields from topics ───────────────────────────────
+ const topics = app.findCollectionByNameOrId("topics");
+ topics.fields.removeById("text_theme");
+ topics.fields.removeById("number_complexity_weight");
+ topics.fields.removeById("text_difficulty");
+ app.save(topics);
+});
diff --git a/src/components/admin/CurriculumManager.jsx b/src/components/admin/CurriculumManager.jsx
index 93d3a10..912b94c 100644
--- a/src/components/admin/CurriculumManager.jsx
+++ b/src/components/admin/CurriculumManager.jsx
@@ -1,114 +1,184 @@
-import { useState, useEffect, useMemo } from 'react';
-import { Calendar, Wand2, ChevronDown, ChevronRight, RotateCcw, CheckCircle2, Loader, AlertTriangle } from 'lucide-react';
+import { useState, useEffect } from 'react';
+import { Calendar, Wand2, CheckCircle2, Loader, AlertTriangle, Play, Sparkles, X, Clock, Target } from 'lucide-react';
import Card from '../ui/Card';
import Button from '../ui/Button';
import Tag from '../ui/Tag';
import * as db from '../../lib/db';
import {
- autoGenerateCurriculum,
- getCurriculumYear,
- getQuarterForWeek,
- getQuarterName,
- getFullCurriculum,
+ getCurriculumWeek,
+ generateCurriculumDraft,
+ confirmVersion,
+ rejectVersion,
+ enrichTopicsForCurriculum,
+ getActiveVersion,
+ getDraftVersion
} from '../../lib/curriculumService';
-const QUARTER_COLORS = {
- 1: { bg: 'bg-teal-50', border: 'border-teal-200', text: 'text-teal-700', accent: 'var(--color-teal)' },
- 2: { bg: 'bg-purple-50', border: 'border-purple-200', text: 'text-purple-700', accent: '#7c3aed' },
- 3: { bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-700', accent: '#2563eb' },
- 4: { bg: 'bg-amber-50', border: 'border-amber-200', text: 'text-amber-700', accent: '#d97706' },
-};
-
const CurriculumManager = () => {
- const [year, setYear] = useState(getCurriculumYear());
- const [curriculum, setCurriculum] = useState([]);
+ const [activeVersion, setActiveVersion] = useState(null);
+ const [draftVersion, setDraftVersion] = useState(null);
const [topics, setTopics] = useState([]);
+
const [isLoading, setIsLoading] = useState(true);
const [isGenerating, setIsGenerating] = useState(false);
- const [expandedQuarters, setExpandedQuarters] = useState({ 1: true, 2: true, 3: true, 4: true });
- const [editingWeek, setEditingWeek] = useState(null);
- const [saveStatus, setSaveStatus] = useState(null);
+ const [isEnriching, setIsEnriching] = useState(false);
+ const [statusMessage, setStatusMessage] = useState('');
+
+ const [generationReason, setGenerationReason] = useState('');
- const currentWeek = useMemo(() => {
+ const currentIsoWeek = (() => {
const d = new Date();
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
- }, []);
+ })();
+
+ const currentWeek = getCurriculumWeek(currentIsoWeek);
const load = async () => {
setIsLoading(true);
- const [currData, topicData] = await Promise.all([
- getFullCurriculum(year),
- db.getTopics(),
- ]);
- setCurriculum(currData);
- setTopics(topicData.filter(t => t.type !== 'fact' && t.learning_relevance !== 'exclude'));
- setIsLoading(false);
+ try {
+ const [active, draft, allTopics] = await Promise.all([
+ getActiveVersion(),
+ getDraftVersion(),
+ db.getTopics(),
+ ]);
+ setActiveVersion(active);
+ setDraftVersion(draft);
+ setTopics(allTopics);
+ } catch (e) {
+ console.error('Failed to load curriculum state:', e);
+ } finally {
+ setIsLoading(false);
+ }
};
- useEffect(() => { load(); }, [year]);
+ useEffect(() => { load(); }, []);
- const handleAutoGenerate = async () => {
- if (curriculum.length > 0 && !confirm('This will replace the existing curriculum for ' + year + '. Continue?')) return;
+ const showStatus = (msg) => {
+ setStatusMessage(msg);
+ setTimeout(() => setStatusMessage(''), 4000);
+ };
+
+ const handleEnrichTopics = async () => {
+ setIsEnriching(true);
+ try {
+ const res = await enrichTopicsForCurriculum();
+ showStatus(`Enrichment complete! Enriched: ${res.enriched}, Skipped: ${res.skipped}`);
+ await load();
+ } catch (e) {
+ showStatus(`Enrichment failed: ${e.message}`);
+ } finally {
+ setIsEnriching(false);
+ }
+ };
+
+ const handleGenerate = async () => {
setIsGenerating(true);
try {
- await autoGenerateCurriculum(year);
+ await generateCurriculumDraft(generationReason || 'Admin request');
+ setGenerationReason('');
+ showStatus('Draft generated successfully!');
await load();
- setSaveStatus('Curriculum generated!');
- setTimeout(() => setSaveStatus(null), 3000);
} catch (e) {
- console.error('Failed to generate curriculum:', e);
- setSaveStatus('Generation failed: ' + e.message);
+ showStatus(`Generation failed: ${e.message}`);
} finally {
setIsGenerating(false);
}
};
- const handleWeekTopicChange = async (weekNumber, topicId) => {
- const topic = topics.find(t => t.id === topicId);
- await db.setCurriculumWeek(year, weekNumber, {
- topic_id: topicId,
- theme: topic?.type || 'General',
- quarter: getQuarterForWeek(weekNumber),
- is_review_week: false,
- sort_order: weekNumber,
- });
- setEditingWeek(null);
- await load();
- setSaveStatus('Week ' + weekNumber + ' updated');
- setTimeout(() => setSaveStatus(null), 2000);
+ const handleConfirmDraft = async () => {
+ if (!draftVersion) return;
+ if (!confirm('This will replace the currently active curriculum for all employees. Proceed?')) return;
+
+ try {
+ // Typically we'd pass the actual admin user ID here, hardcoded to 'admin' for MVP
+ await confirmVersion(draftVersion.id, 'admin');
+ showStatus('Curriculum activated!');
+ await load();
+ } catch (e) {
+ showStatus(`Confirmation failed: ${e.message}`);
+ }
};
- const handleToggleReview = async (weekNumber, currentEntry) => {
- await db.setCurriculumWeek(year, weekNumber, {
- topic_id: currentEntry?.topic_id || '',
- theme: !currentEntry?.is_review_week ? `Q${getQuarterForWeek(weekNumber)} Review` : currentEntry?.theme || '',
- quarter: getQuarterForWeek(weekNumber),
- is_review_week: !currentEntry?.is_review_week,
- sort_order: weekNumber,
- });
- await load();
+ const handleRejectDraft = async () => {
+ if (!draftVersion) return;
+ if (!confirm('Are you sure you want to discard this draft?')) return;
+
+ try {
+ await rejectVersion(draftVersion.id);
+ showStatus('Draft discarded.');
+ await load();
+ } catch (e) {
+ showStatus(`Rejection failed: ${e.message}`);
+ }
};
- const toggleQuarter = (q) => {
- setExpandedQuarters(prev => ({ ...prev, [q]: !prev[q] }));
+ // --- Rendering Helpers ---
+
+ const renderCoverageStats = (stats) => {
+ if (!stats) return null;
+ return (
+
{hasCurriculum
- ? `Week ${state.weekNumber} · ${currentQuarterName}`
+ ? `Cycle ${currentCycle} · Week ${currWeek} of 26`
: 'Complete at least 1 topic per week. Explore more from the library!'}