feat: implement onboarding process and enrollment status tracking for users
This commit is contained in:
60
pb_migrations/1780900000_team_members_enrollment.js
Normal file
60
pb_migrations/1780900000_team_members_enrollment.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
// Adds per-user curriculum enrollment fields to team_members:
|
||||||
|
// - curriculum_started_at: datetime the user started their 26-week cycle
|
||||||
|
// - enrollment_status: 'not_started' | 'active'
|
||||||
|
// Existing rows are reset to 'not_started' so every user re-enrolls via the
|
||||||
|
// onboarding screen on next login. All progress (micro-learning completions
|
||||||
|
// and leaderboard standings) is hard-reset so everyone restarts from week 1.
|
||||||
|
migrate((app) => {
|
||||||
|
const tm = app.findCollectionByNameOrId("team_members");
|
||||||
|
|
||||||
|
tm.fields.add(new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date_curriculum_started_at",
|
||||||
|
"name": "curriculum_started_at",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
}));
|
||||||
|
|
||||||
|
tm.fields.add(new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text_enrollment_status",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "enrollment_status",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.save(tm);
|
||||||
|
|
||||||
|
// Reset everyone to not_started — they re-enroll via the onboarding screen.
|
||||||
|
const members = app.findRecordsByFilter("team_members", "id != ''", "", 0, 0);
|
||||||
|
for (const rec of members) {
|
||||||
|
rec.set("enrollment_status", "not_started");
|
||||||
|
rec.set("curriculum_started_at", "");
|
||||||
|
app.save(rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard-reset all progress so everyone genuinely restarts from week 1.
|
||||||
|
for (const name of ["micro_learning_completions", "leaderboard"]) {
|
||||||
|
const coll = app.findCollectionByNameOrId(name);
|
||||||
|
if (!coll) continue;
|
||||||
|
const rows = app.findRecordsByFilter(name, "id != ''", "", 0, 0);
|
||||||
|
for (const row of rows) {
|
||||||
|
app.delete(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (app) => {
|
||||||
|
const tm = app.findCollectionByNameOrId("team_members");
|
||||||
|
tm.fields.removeById("date_curriculum_started_at");
|
||||||
|
tm.fields.removeById("text_enrollment_status");
|
||||||
|
app.save(tm);
|
||||||
|
});
|
||||||
@@ -84,6 +84,8 @@ const COLLECTIONS = [
|
|||||||
{ name: 'name', type: 'text', required: true },
|
{ name: 'name', type: 'text', required: true },
|
||||||
{ name: 'pin', type: 'text', required: false },
|
{ name: 'pin', type: 'text', required: false },
|
||||||
{ name: 'role', type: 'text', required: false },
|
{ name: 'role', type: 'text', required: false },
|
||||||
|
{ name: 'curriculum_started_at', type: 'date', required: false },
|
||||||
|
{ name: 'enrollment_status', type: 'text', required: false },
|
||||||
...AUTODATE_FIELDS,
|
...AUTODATE_FIELDS,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
12
src/App.jsx
12
src/App.jsx
@@ -6,6 +6,7 @@ import BuildStamp from './components/ui/BuildStamp'
|
|||||||
import ChatLauncher from './components/chat/ChatLauncher'
|
import ChatLauncher from './components/chat/ChatLauncher'
|
||||||
|
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
|
import Onboarding from './pages/Onboarding'
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
|
|
||||||
import Admin from './pages/Admin'
|
import Admin from './pages/Admin'
|
||||||
@@ -14,7 +15,7 @@ import Testen from './pages/Testen'
|
|||||||
import Leaderboard from './pages/Leaderboard'
|
import Leaderboard from './pages/Leaderboard'
|
||||||
|
|
||||||
// Protected Route Wrapper
|
// Protected Route Wrapper
|
||||||
const ProtectedRoute = ({ children, requireAdmin }) => {
|
const ProtectedRoute = ({ children, requireAdmin, skipEnrollmentGate }) => {
|
||||||
const { state, logout } = useApp()
|
const { state, logout } = useApp()
|
||||||
|
|
||||||
if (state.isLoading) return <div className="p-8">Loading...</div>
|
if (state.isLoading) return <div className="p-8">Loading...</div>
|
||||||
@@ -27,6 +28,14 @@ const ProtectedRoute = ({ children, requireAdmin }) => {
|
|||||||
return <Navigate to="/" replace />
|
return <Navigate to="/" replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block any non-enrolled user from reaching curriculum pages.
|
||||||
|
// Admins are exempt from the gate when they're heading to the admin panel.
|
||||||
|
const needsEnrollment = state.currentUser.enrollment_status !== 'active'
|
||||||
|
const isAdmin = state.currentUser.role === 'admin'
|
||||||
|
if (needsEnrollment && !skipEnrollmentGate && !(requireAdmin && isAdmin)) {
|
||||||
|
return <Navigate to="/onboarding" replace />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-bg text-fg font-sans flex flex-col pb-16 md:pb-0">
|
<div className="min-h-screen bg-bg text-fg font-sans flex flex-col pb-16 md:pb-0">
|
||||||
{/* Top Navigation Bar */}
|
{/* Top Navigation Bar */}
|
||||||
@@ -92,6 +101,7 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/onboarding" element={<Onboarding />} />
|
||||||
<Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
<Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
||||||
<Route path="/learn" element={<ProtectedRoute><Leren /></ProtectedRoute>} />
|
<Route path="/learn" element={<ProtectedRoute><Leren /></ProtectedRoute>} />
|
||||||
<Route path="/test" element={<ProtectedRoute><Testen /></ProtectedRoute>} />
|
<Route path="/test" element={<ProtectedRoute><Testen /></ProtectedRoute>} />
|
||||||
|
|||||||
@@ -3,17 +3,36 @@ import { callLLM, cachedSystem } from './llm';
|
|||||||
import { EMIT_CURRICULUM_SCHEDULE_TOOL, EMIT_TOPIC_ENRICHMENT_TOOL } from './llmTools';
|
import { EMIT_CURRICULUM_SCHEDULE_TOOL, EMIT_TOPIC_ENRICHMENT_TOOL } from './llmTools';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current curriculum week (1-26) based on an ISO week number.
|
* Personal weeks since enrollment, starting at 1.
|
||||||
|
* Cycle is purely relative: week 1 = first 7 days after start_date.
|
||||||
|
* Returns 0 if the user has not yet enrolled.
|
||||||
*/
|
*/
|
||||||
export function getCurriculumWeek(isoWeekNumber) {
|
export function getPersonalWeekNumber(startedAt, now = new Date()) {
|
||||||
return ((isoWeekNumber - 1) % 26) + 1;
|
if (!startedAt) return 0;
|
||||||
|
const start = startedAt instanceof Date ? startedAt : new Date(startedAt);
|
||||||
|
if (Number.isNaN(start.getTime())) return 0;
|
||||||
|
const elapsedMs = now.getTime() - start.getTime();
|
||||||
|
if (elapsedMs < 0) return 0;
|
||||||
|
const days = Math.floor(elapsedMs / 86_400_000);
|
||||||
|
return Math.floor(days / 7) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current curriculum cycle (1, 2, 3...) based on an ISO week number.
|
* Curriculum slot (1-26) for a given personal week number.
|
||||||
|
* weekNumber is the absolute counter from getPersonalWeekNumber — it loops
|
||||||
|
* through the 26-slot schedule indefinitely.
|
||||||
*/
|
*/
|
||||||
export function getCurriculumCycle(isoWeekNumber) {
|
export function getCurriculumWeek(weekNumber) {
|
||||||
return Math.floor((isoWeekNumber - 1) / 26) + 1;
|
if (!weekNumber || weekNumber < 1) return 0;
|
||||||
|
return ((weekNumber - 1) % 26) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle (1, 2, 3...) for a given personal week number.
|
||||||
|
*/
|
||||||
|
export function getCurriculumCycle(weekNumber) {
|
||||||
|
if (!weekNumber || weekNumber < 1) return 0;
|
||||||
|
return Math.floor((weekNumber - 1) / 26) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,16 +303,17 @@ export async function getVersionHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the assigned topics and metadata for a given ISO week number.
|
* Get the assigned topics and metadata for a given personal week number.
|
||||||
*/
|
*/
|
||||||
export async function getCurrentWeekContent(isoWeekNumber) {
|
export async function getCurrentWeekContent(personalWeekNumber) {
|
||||||
const activeVersion = await db.getActiveCurriculumVersion();
|
const activeVersion = await db.getActiveCurriculumVersion();
|
||||||
if (!activeVersion || !activeVersion.schedule) {
|
if (!activeVersion || !activeVersion.schedule) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekNumber = getCurriculumWeek(isoWeekNumber);
|
const weekNumber = getCurriculumWeek(personalWeekNumber);
|
||||||
const cycle = getCurriculumCycle(isoWeekNumber);
|
const cycle = getCurriculumCycle(personalWeekNumber);
|
||||||
|
if (weekNumber < 1) return null;
|
||||||
|
|
||||||
const scheduleWeek = activeVersion.schedule.find(w => w.week_number === weekNumber);
|
const scheduleWeek = activeVersion.schedule.find(w => w.week_number === weekNumber);
|
||||||
if (!scheduleWeek) return null;
|
if (!scheduleWeek) return null;
|
||||||
@@ -317,13 +337,14 @@ export async function getCurrentWeekContent(isoWeekNumber) {
|
|||||||
/**
|
/**
|
||||||
* Track progress for the current cycle based on completed weeks.
|
* Track progress for the current cycle based on completed weeks.
|
||||||
*/
|
*/
|
||||||
export async function getYearProgress(userId, isoWeekNumber) {
|
export async function getYearProgress(userId, personalWeekNumber) {
|
||||||
const activeVersion = await db.getActiveCurriculumVersion();
|
const activeVersion = await db.getActiveCurriculumVersion();
|
||||||
if (!activeVersion) {
|
if (!activeVersion) {
|
||||||
return { completed: 0, total: 26, percentage: 0 };
|
return { completed: 0, total: 26, percentage: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCycle = getCurriculumCycle(isoWeekNumber);
|
const currentCycle = getCurriculumCycle(personalWeekNumber);
|
||||||
|
if (currentCycle < 1) return { completed: 0, total: 26, percentage: 0 };
|
||||||
const cycleStartWeek = (currentCycle - 1) * 26 + 1;
|
const cycleStartWeek = (currentCycle - 1) * 26 + 1;
|
||||||
const cycleEndWeek = currentCycle * 26;
|
const cycleEndWeek = currentCycle * 26;
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ const Dashboard = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl">Learning</h3>
|
<h3 className="text-xl">Learning</h3>
|
||||||
<p className="text-fg-muted text-sm mt-1">
|
<p className="text-fg-muted text-sm mt-1">
|
||||||
{curriculumActive ? `Week ${weekNumber} topic:` : 'Your topic this week:'}
|
{curriculumActive ? `Week ${currWeek} topic:` : 'Your topic this week:'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{learnDone ? <Tag variant="success">Completed</Tag> : <Tag variant="accent">To Do</Tag>}
|
{learnDone ? <Tag variant="success">Completed</Tag> : <Tag variant="accent">To Do</Tag>}
|
||||||
|
|||||||
74
src/pages/Onboarding.jsx
Normal file
74
src/pages/Onboarding.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate, Navigate } from 'react-router-dom';
|
||||||
|
import { BookOpen, ArrowRight, LogOut } from 'lucide-react';
|
||||||
|
import { useApp } from '../store/AppContext';
|
||||||
|
import Card from '../components/ui/Card';
|
||||||
|
import Button from '../components/ui/Button';
|
||||||
|
|
||||||
|
const Onboarding = () => {
|
||||||
|
const { state, enrollCurrentUser, logout } = useApp();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
if (state.isLoading) return <div className="p-8">Loading...</div>;
|
||||||
|
if (!state.currentUser) return <Navigate to="/login" replace />;
|
||||||
|
if (state.currentUser.enrollment_status === 'active') {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
setBusy(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
await enrollCurrentUser();
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message || 'Could not start the curriculum. Please try again.');
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-bg flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-lg">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<img src="/images/icon.png" alt="Respellion Icon" className="h-20 mx-auto mb-4" />
|
||||||
|
<h1 className="text-3xl text-teal font-bold tracking-tight">Welcome, {state.currentUser.name}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="border border-bg-warm">
|
||||||
|
<div className="text-center py-2">
|
||||||
|
<BookOpen size={48} className="mx-auto text-teal mb-4" />
|
||||||
|
<h2 className="text-2xl font-bold mb-3">Start your learning journey</h2>
|
||||||
|
<p className="text-fg-muted mb-2">
|
||||||
|
The curriculum is a 26-week cycle of weekly learning sessions and tests.
|
||||||
|
</p>
|
||||||
|
<p className="text-fg-muted mb-6">
|
||||||
|
When you click <strong>Start</strong>, week 1 begins today. You can pick this up at any moment — the cycle is yours.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-sm font-medium mb-4">{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button onClick={handleStart} disabled={busy} className="text-lg px-8 py-3">
|
||||||
|
{busy ? 'Starting...' : <>Start my journey <ArrowRight size={20} className="ml-2" /></>}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="text-xs text-fg-muted hover:text-fg inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<LogOut size={12} /> Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Onboarding;
|
||||||
@@ -10,6 +10,7 @@ import Button from '../components/ui/Button';
|
|||||||
import Tag from '../components/ui/Tag';
|
import Tag from '../components/ui/Tag';
|
||||||
import { useApp } from '../store/AppContext';
|
import { useApp } from '../store/AppContext';
|
||||||
import { generateWeeklyQuiz, saveTestResult, getTestResult } from '../lib/testService';
|
import { generateWeeklyQuiz, saveTestResult, getTestResult } from '../lib/testService';
|
||||||
|
import { getCurriculumWeek, getCurriculumCycle } from '../lib/curriculumService';
|
||||||
import { storage } from '../lib/storage';
|
import { storage } from '../lib/storage';
|
||||||
|
|
||||||
const TIMER_SECONDS = 300; // 5 minutes
|
const TIMER_SECONDS = 300; // 5 minutes
|
||||||
@@ -180,7 +181,7 @@ const Testen = () => {
|
|||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<CheckSquare size={64} className="mx-auto text-teal/30 mb-6" />
|
<CheckSquare size={64} className="mx-auto text-teal/30 mb-6" />
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-teal mb-4">Weekly Test</h1>
|
<h1 className="text-3xl md:text-4xl font-bold text-teal mb-4">Weekly Test</h1>
|
||||||
<p className="text-fg-muted text-lg mb-2">Week {weekNumber}</p>
|
<p className="text-fg-muted text-lg mb-2">Cycle {getCurriculumCycle(weekNumber)} · Week {getCurriculumWeek(weekNumber)} of 26</p>
|
||||||
<p className="text-fg-muted mb-8 max-w-md mx-auto">
|
<p className="text-fg-muted mb-8 max-w-md mx-auto">
|
||||||
Test your knowledge with 5 AI-generated questions. You have 5 minutes to complete the test. Good luck!
|
Test your knowledge with 5 AI-generated questions. You have 5 minutes to complete the test. Good luck!
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,40 +1,50 @@
|
|||||||
import { createContext, useContext, useReducer, useEffect } from 'react';
|
import { createContext, useContext, useReducer, useEffect } from 'react';
|
||||||
import * as db from '../lib/db';
|
import * as db from '../lib/db';
|
||||||
|
import { pb } from '../lib/pb';
|
||||||
|
import { getPersonalWeekNumber } from '../lib/curriculumService';
|
||||||
|
|
||||||
const AppContext = createContext();
|
const AppContext = createContext();
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
users: [],
|
users: [],
|
||||||
weekNumber: getWeekNumber(new Date()),
|
weekNumber: 0,
|
||||||
isLoading: true
|
isLoading: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function computeWeekNumber(user) {
|
||||||
|
if (!user || !user.curriculum_started_at || user.enrollment_status !== 'active') return 0;
|
||||||
|
return getPersonalWeekNumber(user.curriculum_started_at);
|
||||||
|
}
|
||||||
|
|
||||||
function appReducer(state, action) {
|
function appReducer(state, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'INIT_APP':
|
case 'INIT_APP':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
users: action.payload.users,
|
users: action.payload.users,
|
||||||
weekNumber: action.payload.weekNumber || state.weekNumber,
|
|
||||||
isLoading: false
|
isLoading: false
|
||||||
};
|
};
|
||||||
case 'LOGIN':
|
case 'LOGIN':
|
||||||
return { ...state, currentUser: action.payload };
|
return {
|
||||||
|
...state,
|
||||||
|
currentUser: action.payload,
|
||||||
|
weekNumber: computeWeekNumber(action.payload),
|
||||||
|
};
|
||||||
|
case 'UPDATE_CURRENT_USER':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentUser: action.payload,
|
||||||
|
weekNumber: computeWeekNumber(action.payload),
|
||||||
|
users: state.users.map(u => u.id === action.payload.id ? action.payload : u),
|
||||||
|
};
|
||||||
case 'LOGOUT':
|
case 'LOGOUT':
|
||||||
return { ...state, currentUser: null };
|
return { ...state, currentUser: null, weekNumber: 0 };
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWeekNumber(d) {
|
|
||||||
d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppProvider({ children }) {
|
export function AppProvider({ children }) {
|
||||||
const [state, dispatch] = useReducer(appReducer, initialState);
|
const [state, dispatch] = useReducer(appReducer, initialState);
|
||||||
|
|
||||||
@@ -52,8 +62,6 @@ export function AppProvider({ children }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedWeek = getWeekNumber(new Date());
|
|
||||||
|
|
||||||
const sessionUserId = sessionStorage.getItem('respellion_session');
|
const sessionUserId = sessionStorage.getItem('respellion_session');
|
||||||
if (sessionUserId) {
|
if (sessionUserId) {
|
||||||
const user = members.find(u => u.id === sessionUserId);
|
const user = members.find(u => u.id === sessionUserId);
|
||||||
@@ -62,7 +70,7 @@ export function AppProvider({ children }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'INIT_APP', payload: { users: members, weekNumber: storedWeek } });
|
dispatch({ type: 'INIT_APP', payload: { users: members } });
|
||||||
};
|
};
|
||||||
|
|
||||||
loadState().catch(console.error);
|
loadState().catch(console.error);
|
||||||
@@ -83,8 +91,23 @@ export function AppProvider({ children }) {
|
|||||||
dispatch({ type: 'LOGOUT' });
|
dispatch({ type: 'LOGOUT' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the curriculum for the currently logged-in user.
|
||||||
|
* Records the start timestamp and flips enrollment_status to 'active'.
|
||||||
|
*/
|
||||||
|
const enrollCurrentUser = async () => {
|
||||||
|
if (!state.currentUser) throw new Error('No user is logged in.');
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
const updated = await pb.collection('team_members').update(state.currentUser.id, {
|
||||||
|
curriculum_started_at: startedAt,
|
||||||
|
enrollment_status: 'active',
|
||||||
|
});
|
||||||
|
dispatch({ type: 'UPDATE_CURRENT_USER', payload: updated });
|
||||||
|
return updated;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ state, dispatch, login, logout }}>
|
<AppContext.Provider value={{ state, dispatch, login, logout, enrollCurrentUser }}>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user