-
Member Settings
+
+
+
+
+ Member Settings
+
+
+ Manage your profile and account settings
+
+
+
+
+
);
}
diff --git a/components/SideNav.tsx b/components/SideNav.tsx
index 6d38d41..9ede2c1 100644
--- a/components/SideNav.tsx
+++ b/components/SideNav.tsx
@@ -8,7 +8,6 @@ import {
HomeIcon,
BookOpenIcon,
ChartBarIcon,
- UsersIcon,
MapIcon,
ClockIcon,
CogIcon,
@@ -30,7 +29,6 @@ const menuSections = [
heading: "Management",
items: [
{ label: "History", href: "/history", icon: ClockIcon },
- // { label: "Organisations", href: "/organisations", icon: UsersIcon },
{ label: "Settings", href: "/settings", icon: CogIcon },
],
},
diff --git a/components/dashboard/AdminDashboard.tsx b/components/dashboard/AdminDashboard.tsx
new file mode 100644
index 0000000..23b9c2a
--- /dev/null
+++ b/components/dashboard/AdminDashboard.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from "recharts";
+
+interface Employee {
+ id: number;
+ firstname: string;
+ lastname: string;
+ totalCourses: number;
+ completedCourses: number;
+}
+
+interface Enrollment {
+ courseName: string;
+ enrolledCount: number;
+}
+
+interface AdminDashboardData {
+ welcome: string;
+ employees: Employee[];
+ enrollments: Enrollment[];
+}
+
+export default function AdminDashboard() {
+ const [data, setData] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetch("/api/dashboard/admin-dashboard", { credentials: "include" })
+ .then(async (res) => {
+ if (!res.ok) throw new Error("Failed to load dashboard");
+ const json = await res.json();
+ const enrollments = (json.enrollments || []).map((e: any) => ({
+ courseName: e.coursename,
+ enrolledCount: Number(e.enrolledcount),
+ }));
+ const employees = (json.employees || []).map((emp: any) => ({
+ id: emp.id,
+ firstname: emp.firstname,
+ lastname: emp.lastname,
+ totalCourses: Number(emp.totalCourses),
+ completedCourses: Number(emp.completedCourses),
+ }));
+ setData({ ...json, employees, enrollments });
+ })
+ .catch((err) => setError(err.message || "Unknown error"))
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading) {
+ return (
+
+ );
+ }
+ if (error || !data) {
+ return {error}
;
+ }
+
+ // Calculate quick stats
+ const totalEmployees = data.employees.length;
+ const totalCourses = data.enrollments.length;
+ const totalEnrollments = data.enrollments.reduce(
+ (a, e) => a + e.enrolledCount,
+ 0
+ );
+ const totalCompleted = data.employees.reduce(
+ (a, e) => a + e.completedCourses,
+ 0
+ );
+
+ return (
+
+
{data.welcome}
+
+
+
+
+
+
+
+
+
+
Course Enrollments
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Employee Course Progress
+
+
+
+
+ | Name |
+ Enrolled |
+ Completed |
+
+
+
+ {data.employees.map((emp) => (
+
+ |
+ {emp.firstname} {emp.lastname}
+ |
+ {emp.totalCourses} |
+ {emp.completedCourses} |
+
+ ))}
+
+
+
+
+
+ );
+}
+
+function DashboardStat({ label, value }: { label: string; value: number }) {
+ return (
+
+ );
+}
diff --git a/components/dashboard/UserDashboard.tsx b/components/dashboard/UserDashboard.tsx
new file mode 100644
index 0000000..dbdb95c
--- /dev/null
+++ b/components/dashboard/UserDashboard.tsx
@@ -0,0 +1,233 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Link from "next/link";
+
+interface Course {
+ id: number;
+ name: string;
+}
+interface Module {
+ id: number;
+ title: string;
+}
+interface DashboardData {
+ welcome: string;
+ currentCourse: Course | null;
+ currentModule: Module | null;
+ nextToLearn: Module[];
+ toRevise: Module[];
+ summaryStats: {
+ completedModules: number;
+ totalModules: number;
+ };
+ globalStats: {
+ completedModules: number;
+ totalModules: number;
+ };
+}
+
+export default function UserDashboard() {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetch("/api/dashboard/user-dashboard", { credentials: "include" })
+ .then(async (res) => {
+ if (!res.ok) throw new Error("Failed to load dashboard");
+ const json = await res.json();
+ setData(json);
+ })
+ .catch((err) => setError(err.message || "Unknown error"))
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading) {
+ return (
+
+ );
+ }
+ if (error || !data) {
+ return (
+
+ {error || "No data."}
+
+ );
+ }
+
+ const {
+ welcome,
+ currentCourse,
+ currentModule,
+ nextToLearn,
+ toRevise,
+ summaryStats,
+ globalStats,
+ } = data;
+
+ return (
+
+
+
+
+
+
+
+
+ Continue where you left off
+
+
+ {currentCourse && currentModule ? (
+ <>
+
+
+ {currentCourse.name}
+
+
+ Module: {currentModule.title}
+
+
+ Resume
+
+
+ >
+ ) : (
+
+ No module in progress.
+
+ )}
+
+
+
+
+
+ Next Steps For You
+
+
+
+
Learn
+ {nextToLearn && nextToLearn.length > 0 ? (
+ nextToLearn.map((mod) => (
+
+ {mod.title}
+
+ ))
+ ) : (
+
All caught up!
+ )}
+
+
+
Revise
+ {toRevise && toRevise.length > 0 ? (
+ toRevise.map((mod) => (
+
+ {mod.title}
+
+ ))
+ ) : (
+
Nothing to revise.
+ )}
+
+
+
+
+
+
+
+
+ At a glance
+
+
+
+
+ Modules Completed{" "}
+
+ (Current Course)
+
+
+
+
+ {summaryStats.completedModules}
+
+ /
+
+ {summaryStats.totalModules}
+
+
+
+ {currentCourse?.name}
+
+ {/* Progress bar */}
+
+
0
+ ? (summaryStats.completedModules /
+ summaryStats.totalModules) *
+ 100
+ : 0
+ }%`,
+ }}
+ >
+
+
+
+
+
+ Modules Completed{" "}
+ (All Courses)
+
+
+
+ {globalStats.completedModules}
+
+ /
+
+ {globalStats.totalModules}
+
+
+
+ across all courses
+
+
+
0
+ ? (globalStats.completedModules /
+ globalStats.totalModules) *
+ 100
+ : 0
+ }%`,
+ }}
+ >
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/history/HistoryComponent.tsx b/components/history/HistoryComponent.tsx
new file mode 100644
index 0000000..fae2ccd
--- /dev/null
+++ b/components/history/HistoryComponent.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+interface ActivityLog {
+ id: number;
+ action: string;
+ metadata: Record;
+ created_at: string;
+}
+
+export default function HistoryComponent() {
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetch("/api/activity", { credentials: "include" })
+ .then((res) => {
+ if (!res.ok) throw new Error(`Status ${res.status}`);
+ return res.json();
+ })
+ .then(({ logs }: { logs: ActivityLog[] }) => setLogs(logs))
+ .catch((err) => {
+ console.error(err);
+ setError("Failed to load history.");
+ })
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading) return Loading history…
;
+ if (error) return {error}
;
+ if (!logs.length) return No history yet.
;
+
+ // sort newest first
+ const sorted = [...logs].sort(
+ (a, b) =>
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+ );
+
+ const formatKey = (key: string) =>
+ key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+
+ return (
+
+ {sorted.map((log, idx) => {
+ const title = log.action
+ .replace(/_/g, " ")
+ .replace(/\b\w/g, (c) => c.toUpperCase());
+ const time = new Date(log.created_at).toLocaleString();
+
+ return (
+ -
+
+
+
+ {idx + 1}.
+
{title}
+
+
+
+ {Object.keys(log.metadata).length > 0 && (
+
+
+ View details
+
+
+ {Object.entries(log.metadata).map(([key, val]) => (
+ -
+ {formatKey(key)}:{" "}
+ {String(val)}
+
+ ))}
+
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/components/members/settings/MemberPasswordSettings.tsx b/components/members/settings/MemberPasswordSettings.tsx
new file mode 100644
index 0000000..7437379
--- /dev/null
+++ b/components/members/settings/MemberPasswordSettings.tsx
@@ -0,0 +1,148 @@
+"use client";
+import { useState } from "react";
+
+export default function MemberPasswordSettings() {
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [message, setMessage] = useState(null);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setMessage(null);
+ setError(null);
+
+ if (newPassword !== confirmPassword) {
+ setError("New passwords do not match");
+ return;
+ }
+
+ if (newPassword.length < 8) {
+ setError("New password must be at least 8 characters long");
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ const res = await fetch("/api/users/password", {
+ method: "PUT",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ currentPassword,
+ newPassword,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.message || "Failed to update password");
+ }
+
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ setError(null);
+ setMessage("Password updated successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to update password");
+ setMessage(null);
+ console.error("Error updating password:", err);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleReset = () => {
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ setError(null);
+ setMessage(null);
+ };
+
+ return (
+
+
+ Change Password
+
+
+
+ );
+}
diff --git a/components/members/settings/MemberProfileSettings.tsx b/components/members/settings/MemberProfileSettings.tsx
new file mode 100644
index 0000000..c33ec13
--- /dev/null
+++ b/components/members/settings/MemberProfileSettings.tsx
@@ -0,0 +1,200 @@
+"use client";
+import { useState, useEffect } from "react";
+import { useAuth } from "@/context/AuthContext";
+
+interface UserProfileData {
+ id: string | number;
+ firstname: string;
+ lastname: string;
+ email: string;
+}
+
+export default function MemberProfileSettings() {
+ const { user, setUser } = useAuth();
+ const [initialData, setInitialData] = useState(null);
+ const [firstname, setFirstname] = useState("");
+ const [lastname, setLastname] = useState("");
+ const [email, setEmail] = useState("");
+ const [isSaving, setIsSaving] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [message, setMessage] = useState(null);
+
+ useEffect(() => {
+ if (user) {
+ const userData = {
+ id: user.userId || "",
+ firstname: user.firstname || "",
+ lastname: user.lastname || "",
+ email: user.email || "",
+ };
+ setInitialData(userData);
+ if (user.firstname) {
+ setFirstname(user.firstname);
+ }
+ if (user.lastname) {
+ setLastname(user.lastname);
+ }
+ if (user.email) {
+ setEmail(user.email);
+ }
+ setLoading(false);
+ }
+ }, [user]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setMessage(null);
+ setError(null);
+
+ if (
+ initialData &&
+ initialData.firstname === firstname &&
+ initialData.lastname === lastname &&
+ initialData.email === email
+ ) {
+ setMessage("No changes made");
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ const res = await fetch("/api/users/profile", {
+ method: "PUT",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ firstname,
+ lastname,
+ email,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.message || "Failed to update profile");
+ }
+
+ const updatedUser = await res.json();
+
+ // Update the auth context with new user data
+ if (updatedUser.user) {
+ setUser(updatedUser.user);
+ }
+
+ setInitialData({ id: user?.userId || "", firstname, lastname, email });
+ setError(null);
+ setMessage("Profile updated successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to update profile");
+ setMessage(null);
+ console.error("Error updating profile:", err);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleReset = () => {
+ if (initialData) {
+ setFirstname(initialData.firstname);
+ setLastname(initialData.lastname);
+ setEmail(initialData.email);
+ setError(null);
+ setMessage(null);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ Profile Settings
+
+
+
+ );
+}
diff --git a/components/members/settings/MemberSkillsSettings.tsx b/components/members/settings/MemberSkillsSettings.tsx
new file mode 100644
index 0000000..f65bf25
--- /dev/null
+++ b/components/members/settings/MemberSkillsSettings.tsx
@@ -0,0 +1,660 @@
+"use client";
+import { useState, useEffect } from "react";
+
+interface Skill {
+ id: number;
+ name: string;
+}
+
+interface UserSkill {
+ id: number;
+ skill_id: number;
+ skill_name: string;
+ level: "beginner" | "intermediate" | "advanced" | "expert";
+}
+
+interface Channel {
+ id: number;
+ name: string;
+ description?: string;
+}
+
+interface Level {
+ id: number;
+ name: string;
+ description?: string;
+ sort_order?: number;
+}
+
+interface UserChannel {
+ id: number;
+ channel_id: number;
+ channel_name: string;
+ channel_description?: string;
+ preference_rank: number;
+}
+
+interface UserLevel {
+ id: number;
+ level_id: number;
+ level_name: string;
+ level_description?: string;
+ sort_order?: number;
+ preference_rank: number;
+}
+
+const SKILL_LEVELS = [
+ { value: "beginner", label: "Beginner" },
+ { value: "intermediate", label: "Intermediate" },
+ { value: "advanced", label: "Advanced" },
+ { value: "expert", label: "Expert" },
+];
+
+export default function MemberSkillsSettings() {
+ const [userSkills, setUserSkills] = useState([]);
+ const [availableSkills, setAvailableSkills] = useState([]);
+ const [selectedSkill, setSelectedSkill] = useState("");
+ const [selectedLevel, setSelectedLevel] = useState("beginner");
+
+ // Channel and Level preferences
+ const [userChannels, setUserChannels] = useState([]);
+ const [userLevels, setUserLevels] = useState([]);
+ const [availableChannels, setAvailableChannels] = useState([]);
+ const [availableLevels, setAvailableLevels] = useState([]);
+ const [selectedChannel, setSelectedChannel] = useState("");
+ const [selectedPreferenceLevel, setSelectedPreferenceLevel] =
+ useState("");
+
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [message, setMessage] = useState(null);
+
+ useEffect(() => {
+ loadUserSkills();
+ loadUserPreferences();
+ }, []);
+
+ const loadUserSkills = async () => {
+ try {
+ const res = await fetch("/api/users/skills", {
+ credentials: "include",
+ });
+ if (!res.ok) throw new Error("Failed to load skills");
+ const data = await res.json();
+ setUserSkills(data.userSkills || []);
+ setAvailableSkills(data.availableSkills || []);
+ setError(null);
+ } catch (err: any) {
+ setError(err.message || "Failed to load skills");
+ console.error("Error loading skills:", err);
+ }
+ };
+
+ const loadUserPreferences = async () => {
+ try {
+ const res = await fetch("/api/users/preferences", {
+ credentials: "include",
+ });
+ if (!res.ok) throw new Error("Failed to load preferences");
+ const data = await res.json();
+ setUserChannels(data.userChannels || []);
+ setUserLevels(data.userLevels || []);
+ setAvailableChannels(data.availableChannels || []);
+ setAvailableLevels(data.availableLevels || []);
+ setError(null);
+ } catch (err: any) {
+ setError(err.message || "Failed to load preferences");
+ console.error("Error loading preferences:", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAddSkill = async () => {
+ if (!selectedSkill) return;
+
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/skills", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ skill_id: parseInt(selectedSkill),
+ level: selectedLevel,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.message || "Failed to add skill");
+ }
+
+ await loadUserSkills();
+ setSelectedSkill("");
+ setSelectedLevel("beginner");
+ setMessage("Skill added successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to add skill");
+ console.error("Error adding skill:", err);
+ }
+ };
+
+ const handleUpdateSkill = async (skillId: number, newLevel: string) => {
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/skills", {
+ method: "PUT",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ skill_id: skillId,
+ level: newLevel,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.message || "Failed to update skill");
+ }
+
+ await loadUserSkills();
+ setMessage("Skill updated successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to update skill");
+ console.error("Error updating skill:", err);
+ }
+ };
+
+ const handleRemoveSkill = async (skillId: number) => {
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/skills", {
+ method: "DELETE",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ skill_id: skillId,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.message || "Failed to remove skill");
+ }
+
+ await loadUserSkills();
+ setMessage("Skill removed successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to remove skill");
+ console.error("Error removing skill:", err);
+ }
+ };
+
+ const handleAddChannel = async () => {
+ if (!selectedChannel) return;
+
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/preferences/channels", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ channel_id: parseInt(selectedChannel),
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(
+ errorData.message || "Failed to add channel preference"
+ );
+ }
+
+ await loadUserPreferences();
+ setSelectedChannel("");
+ setMessage("Channel preference added successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to add channel preference");
+ console.error("Error adding channel preference:", err);
+ }
+ };
+
+ const handleAddLevel = async () => {
+ if (!selectedPreferenceLevel) return;
+
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/preferences/levels", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ level_id: parseInt(selectedPreferenceLevel),
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(errorData.message || "Failed to add level preference");
+ }
+
+ await loadUserPreferences();
+ setSelectedPreferenceLevel("");
+ setMessage("Level preference added successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to add level preference");
+ console.error("Error adding level preference:", err);
+ }
+ };
+
+ const handleRemoveChannel = async (channelId: number) => {
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/preferences/channels", {
+ method: "DELETE",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ channel_id: channelId,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(
+ errorData.message || "Failed to remove channel preference"
+ );
+ }
+
+ await loadUserPreferences();
+ setMessage("Channel preference removed successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to remove channel preference");
+ console.error("Error removing channel preference:", err);
+ }
+ };
+
+ const handleRemoveLevel = async (levelId: number) => {
+ setError(null);
+ setMessage(null);
+
+ try {
+ const res = await fetch("/api/users/preferences/levels", {
+ method: "DELETE",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ level_id: levelId,
+ }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json();
+ throw new Error(
+ errorData.message || "Failed to remove level preference"
+ );
+ }
+
+ await loadUserPreferences();
+ setMessage("Level preference removed successfully");
+ } catch (err: any) {
+ setError(err.message || "Failed to remove level preference");
+ console.error("Error removing level preference:", err);
+ }
+ };
+
+ const getAvailableSkillsForAdding = () => {
+ const userSkillIds = userSkills.map((us) => us.skill_id);
+ return availableSkills.filter((skill) => !userSkillIds.includes(skill.id));
+ };
+
+ const getAvailableChannelsForAdding = () => {
+ const userChannelIds = userChannels.map((uc) => uc.channel_id);
+ return availableChannels.filter(
+ (channel) => !userChannelIds.includes(channel.id)
+ );
+ };
+
+ const getAvailableLevelsForAdding = () => {
+ const userLevelIds = userLevels.map((ul) => ul.level_id);
+ return availableLevels.filter((level) => !userLevelIds.includes(level.id));
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ Learning Preferences
+
+
+
+
+
+ Skills
+
+
+
+
+ Add New Skill
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Skills
+
+ {userSkills.length === 0 ? (
+
+ No skills added yet.
+
+ ) : (
+
+ {userSkills.map((userSkill) => (
+
+
+
+ {userSkill.skill_name}
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ Preferred Channels
+
+
+
+
+ Add Channel Preference
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Preferences
+
+ {userChannels.length === 0 ? (
+
+ No channel preferences set.
+
+ ) : (
+
+ {userChannels.map((userChannel) => (
+
+
+
+ {userChannel.channel_name}
+
+ {userChannel.channel_description && (
+
+ {userChannel.channel_description}
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ Preferred Levels
+
+
+
+
+ Add Level Preference
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Preferences
+
+ {userLevels.length === 0 ? (
+
+ No level preferences set.
+
+ ) : (
+
+ {userLevels.map((userLevel) => (
+
+
+
+ {userLevel.level_name}
+
+ {userLevel.level_description && (
+
+ {userLevel.level_description}
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+ {message && (
+
+ {message}
+
+ )}
+
+ );
+}
diff --git a/components/onboarding/OnboardingQuestionnaire.tsx b/components/onboarding/OnboardingQuestionnaire.tsx
new file mode 100644
index 0000000..4911667
--- /dev/null
+++ b/components/onboarding/OnboardingQuestionnaire.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import { useState, useEffect } from "react";
+
+interface Tag {
+ id: number;
+ name: string;
+}
+
+interface Option {
+ id: number;
+ option_text: string;
+ tag_id: number | null;
+ tag_name: string | null;
+}
+
+interface Question {
+ id: number;
+ question_text: string;
+ position: number;
+ options: Option[];
+}
+
+interface OnboardingQuestionnaireProps {
+ onComplete: (responses: number[]) => void;
+ loading: boolean;
+}
+
+export default function OnboardingQuestionnaire({
+ onComplete,
+ loading,
+}: OnboardingQuestionnaireProps) {
+ const [questions, setQuestions] = useState([]);
+ const [selectedOptions, setSelectedOptions] = useState>(
+ new Set()
+ );
+ const [error, setError] = useState(null);
+ const [fetchLoading, setFetchLoading] = useState(true);
+ const [currentQuestion, setCurrentQuestion] = useState(0);
+
+ useEffect(() => {
+ fetchQuestions();
+ }, []);
+
+ const fetchQuestions = async () => {
+ try {
+ const res = await fetch("/api/onboarding/questions", {
+ credentials: "include",
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setQuestions(data.questions || []);
+ } else {
+ setError("Failed to load questions");
+ }
+ } catch (err) {
+ setError("Error loading questions");
+ console.error(err);
+ } finally {
+ setFetchLoading(false);
+ }
+ };
+
+ const handleOptionSelect = (optionId: number) => {
+ const newSelected = new Set(selectedOptions);
+ if (newSelected.has(optionId)) {
+ newSelected.delete(optionId);
+ } else {
+ newSelected.add(optionId);
+ }
+ setSelectedOptions(newSelected);
+ };
+
+ const handleSubmit = () => {
+ if (selectedOptions.size === 0) {
+ setError("Please select at least one option");
+ return;
+ }
+ onComplete(Array.from(selectedOptions));
+ };
+
+ const goToNext = () => {
+ if (currentQuestion < questions.length - 1) {
+ setCurrentQuestion(currentQuestion + 1);
+ }
+ };
+
+ const goToPrevious = () => {
+ if (currentQuestion > 0) {
+ setCurrentQuestion(currentQuestion - 1);
+ }
+ };
+
+ if (fetchLoading) {
+ return (
+
+
+
+
Loading questions...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (questions.length === 0) {
+ return (
+
+
+
+ No Questions Available
+
+
+ Your organization hasn't set up any onboarding questions yet.
+
+
+
+
+ );
+ }
+
+ const currentQ = questions[currentQuestion];
+ const isLastQuestion = currentQuestion === questions.length - 1;
+ const progress = ((currentQuestion + 1) / questions.length) * 100;
+
+ return (
+
+
+
Skills Assessment
+
+ Help us understand your background and experience level.
+
+
+
+
+
+ Question {currentQuestion + 1} of {questions.length}
+
+
+
+
+
{currentQ.question_text}
+
+
+ {currentQ.options.map((option) => (
+
+ ))}
+
+
+
+
+
+
+
+ {questions.map((_, index) => (
+
+
+ {isLastQuestion ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/components/organisation/OrgNav.tsx b/components/organisation/OrgNav.tsx
index bc14a2a..7ae3ae6 100644
--- a/components/organisation/OrgNav.tsx
+++ b/components/organisation/OrgNav.tsx
@@ -8,6 +8,7 @@ import {
HomeIcon,
BookOpenIcon,
ChartBarIcon,
+ ClockIcon,
UsersIcon,
CogIcon,
} from "@heroicons/react/24/outline";
@@ -28,6 +29,7 @@ const menuSections = [
items: [
{ label: "My Organisation", href: "/organisation", icon: UsersIcon },
{ label: "Users", href: "/users", icon: UsersIcon },
+ { label: "History", href: "/history", icon: ClockIcon },
{ label: "Settings", href: "/settings", icon: CogIcon },
],
},
diff --git a/components/organisation/courses/CourseCard.tsx b/components/organisation/courses/CourseCard.tsx
index ba75070..2b3892c 100644
--- a/components/organisation/courses/CourseCard.tsx
+++ b/components/organisation/courses/CourseCard.tsx
@@ -2,9 +2,17 @@
import Link from "next/link";
-export interface Tag {
+export interface Channel {
id: number;
name: string;
+ description?: string;
+}
+
+export interface Level {
+ id: number;
+ name: string;
+ description?: string;
+ sort_order?: number;
}
export interface Course {
@@ -13,7 +21,8 @@ export interface Course {
description?: string;
total_modules?: number;
completed_modules?: number;
- tags?: Tag[];
+ channel?: Channel;
+ level?: Level;
}
interface Props {
@@ -118,16 +127,18 @@ export default function CourseCard({
{course.name}
{course.description?.slice(0, 100)}
- {course.tags && course.tags.length > 0 && (
+ {(course.channel || course.level) && (
- {course.tags.map((t) => (
-
- {t.name}
+ {course.channel && (
+
+ {course.channel.name}
+
+ )}
+ {course.level && (
+
+ {course.level.name}
- ))}
+ )}
)}
diff --git a/components/organisation/courses/CourseForm.tsx b/components/organisation/courses/CourseForm.tsx
index 19a9525..42ebfdb 100644
--- a/components/organisation/courses/CourseForm.tsx
+++ b/components/organisation/courses/CourseForm.tsx
@@ -1,4 +1,3 @@
-// components/organisation/courses/CourseForm.tsx
"use client";
import { useState, useEffect } from "react";
@@ -9,11 +8,24 @@ export interface Course {
id: number;
name: string;
description?: string;
- tags?: { id: number; name: string }[];
+ channel?: { id: number; name: string; description?: string };
+ level?: {
+ id: number;
+ name: string;
+ description?: string;
+ sort_order?: number;
+ };
+}
+interface Channel {
+ id: number;
+ name: string;
+ description?: string;
}
-interface Tag {
+interface Level {
id: number;
name: string;
+ description?: string;
+ sort_order?: number;
}
interface Option {
value: number;
@@ -28,17 +40,32 @@ interface Props {
export default function CourseForm({ mode, courseId }: Props) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
- const [allTags, setAllTags] = useState([]);
- const [options, setOptions] = useState