diff --git a/package-lock.json b/package-lock.json index 6fc436d..c725334 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,7 +88,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2754,7 +2753,6 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2765,7 +2763,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2776,7 +2773,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2832,7 +2828,6 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -3084,7 +3079,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3323,7 +3317,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3900,7 +3893,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5015,7 +5007,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5063,7 +5054,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5252,7 +5242,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5262,7 +5251,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5282,7 +5270,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5488,8 +5475,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -5893,7 +5879,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6061,7 +6046,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6199,7 +6183,6 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 7bf5ce7..c831e5a 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -37,11 +37,15 @@ import type { CreateQuestionRequest, CreateQuestionResponse, CreateVimeoVideoRequest, + CreateCourseCategoryRequest, } from "../types/course.types" export const getCourseCategories = () => http.get("/course-management/categories") +export const createCourseCategory = (data: CreateCourseCategoryRequest) => + http.post("/course-management/categories", data) + export const getCoursesByCategory = (categoryId: number) => http.get(`/course-management/categories/${categoryId}/courses`) diff --git a/src/api/team.api.ts b/src/api/team.api.ts index c9b3270..c0b3d8f 100644 --- a/src/api/team.api.ts +++ b/src/api/team.api.ts @@ -1,5 +1,5 @@ import http from "./http" -import type { GetTeamMembersResponse, GetTeamMemberResponse } from "../types/team.types" +import type { GetTeamMembersResponse, GetTeamMemberResponse, CreateTeamMemberRequest } from "../types/team.types" export const getTeamMembers = (page?: number, pageSize?: number) => http.get("/team/members", { @@ -11,3 +11,6 @@ export const getTeamMembers = (page?: number, pageSize?: number) => export const getTeamMemberById = (id: number) => http.get(`/team/members/${id}`) + +export const createTeamMember = (data: CreateTeamMemberRequest) => + http.post("/team/register", data) diff --git a/src/api/users.api.ts b/src/api/users.api.ts index 89e3ca4..30936b6 100644 --- a/src/api/users.api.ts +++ b/src/api/users.api.ts @@ -14,3 +14,17 @@ export const getUserById = (id: number) => export const getMyProfile = () => http.get("/team/me"); + +// Best-guess API for creating a new user (admin-side). +// Adjust payload shape or endpoint if backend differs. +export interface CreateUserRequest { + first_name: string; + last_name: string; + email: string; + phone_number: string; + role: string; + notes?: string; +} + +export const createUser = (payload: CreateUserRequest) => + http.post("/users", payload); diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 730eed3..09cf9a8 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -4,6 +4,8 @@ import { DashboardPage } from "../pages/DashboardPage" import { AnalyticsPage } from "../pages/analytics/AnalyticsPage" import { ContentManagementLayout } from "../pages/content-management/ContentManagementLayout" import { CourseCategoryPage } from "../pages/content-management/CourseCategoryPage" +import { AllCoursesPage } from "../pages/content-management/AllCoursesPage" +import { CourseFlowBuilderPage } from "../pages/content-management/CourseFlowBuilderPage" import { ContentOverviewPage } from "../pages/content-management/ContentOverviewPage" import { CoursesPage } from "../pages/content-management/CoursesPage" import { PracticeQuestionsPage } from "../pages/content-management/PracticeQuestionsPage" @@ -33,6 +35,7 @@ import { IssuesPage } from "../pages/issues/IssuesPage" import { ProfilePage } from "../pages/ProfilePage" import { SettingsPage } from "../pages/SettingsPage" import { TeamManagementPage } from "../pages/team/TeamManagementPage" +import { AddTeamMemberPage } from "../pages/team/AddTeamMemberPage" import { TeamMemberDetailPage } from "../pages/team/TeamMemberDetailPage" import { LoginPage } from "../pages/auth/LoginPage" import { ForgotPasswordPage } from "../pages/auth/ForgotPasswordPage" @@ -62,6 +65,8 @@ export function AppRoutes() { }> } /> + } /> + } /> } /> } /> {/* Course → Sub-course → Video/Practice */} @@ -85,6 +90,7 @@ export function AppRoutes() { } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 44d283b..c57038b 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -29,7 +29,7 @@ import { import { StatCard } from "../components/dashboard/StatCard" import alertSrc from "../assets/Alert.svg" import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card" -// import { cn } from "../lib/utils" +import { cn } from "../lib/utils" import { getTeamMemberById } from "../api/team.api" import { getDashboard } from "../api/analytics.api" import { useEffect, useState } from "react" @@ -46,6 +46,7 @@ export function DashboardPage() { const [userFirstName, setUserFirstName] = useState("") const [dashboard, setDashboard] = useState(null) const [loading, setLoading] = useState(true) + const [activeStatTab, setActiveStatTab] = useState<"primary" | "secondary">("primary") useEffect(() => { const fetchUser = async () => { @@ -123,69 +124,109 @@ export function DashboardPage() { ) : ( <> - {/* Stat Cards */} -
- 0} - /> - 0} - /> - 0} - /> - 0.5} - /> + {/* Stat tabs */} +
+
+ + +
+ {/* Stat Cards */} + {activeStatTab === "primary" && ( +
+ 0} + /> + 0} + /> + 0} + /> + 0.5} + /> +
+ )} + {/* Secondary Stats */} -
- - - - -
+ {activeStatTab === "secondary" && ( +
+ + + + +
+ )} {/* User Registrations Chart */}
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index c632bce..933fa24 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -44,7 +44,7 @@ function formatDateTime(dateStr: string | null | undefined): string { function LoadingSkeleton() { return ( -
+
{/* Hero skeleton */}
@@ -93,15 +93,15 @@ function InfoRow({ extra?: React.ReactNode; }) { return ( -
+
{label}
-
- {value || "—"} +
+ {value || "—"} {extra}
@@ -121,13 +121,13 @@ function VerifiedIcon({ verified }: { verified: boolean }) { } function ProgressRing({ percent }: { percent: number }) { - const radius = 18; + const radius = 14; const circumference = 2 * Math.PI * radius; const offset = circumference - (percent / 100) * circumference; return (
- + - {percent}% + {percent}%
); } @@ -179,7 +179,7 @@ export function ProfilePage() { if (error || !profile) { return ( -
+
@@ -203,227 +203,246 @@ export function ProfilePage() { const initials = `${profile.first_name?.[0] ?? ""}${profile.last_name?.[0] ?? ""}`.toUpperCase(); const completionPct = profile.profile_completion_percentage ?? 0; - const sectionCardIcons: Record = { - personal: { icon: User, color: "from-brand-500 to-brand-600" }, - contact: { icon: Mail, color: "from-brand-400 to-brand-500" }, - account: { icon: Shield, color: "from-brand-600 to-brand-500" }, - }; - return ( -
- {/* Hero Card */} - - {/* Banner gradient */} -
- {/* Decorative pattern overlay */} -
-
+
+ {/* Page header (no tabs) */} +
+

My Info

+

Profile

+
+ + {/* Main profile layout card */} +
+ {/* Header strip */} +
+
+
+

Overview

+

+ Personal, job and account details for this team member. +

+
- {/* Bottom fade */} -
- -
- {/* Avatar */} - - - - {initials} - - - - {/* Name */} -

- {fullName} -

- - {/* Role badge */} - - - {profile.role} - - - {/* Status pills */} -
- {/* Active status */} -
- - {profile.status} +
+
+ {/* Left column: About & details */} +
+ {/* Identity */} +
+ + + + {initials} + + +
+
+

{fullName}

+ + #{profile.id} + +
+
+ + + {profile.role} + + + + Joined {formatDate(profile.created_at)} + +
+
+ + + {profile.status} + +
+ + Profile complete +
+
+
- {/* Email verification */} -
- {profile.email_verified ? ( - - ) : ( - - )} - Email {profile.email_verified ? "Verified" : "Unverified"} + {/* About / Contact */} +
+

+ About +

+
+ } /> + } /> + +
- {/* Phone verification */} -
- {profile.phone_verified ? ( - - ) : ( - - )} - Phone {profile.phone_verified ? "Verified" : "Unverified"} + {/* Employee details */} +
+

+ Employee details +

+
+
+
Date of birth
+
+ {formatDate(profile.birth_day)} +
+
+
+
Age
+
+ {profile.age ? `${profile.age} years` : "—"} +
+
+
+
Gender
+
+ {profile.gender || "Not specified"} +
+
+
+
Age group
+
+ {profile.age_group || "—"} +
+
+
+
Occupation
+
+ {profile.occupation || "—"} +
+
+
+
Preferred language
+
+ {profile.preferred_language || "—"} +
+
+
+
+
+ + {/* Right column: Activity & account summary */} +
+ {/* Activity */} +
+

+ Activity +

+ + +
+
+
+ +
+
+

+ Last login +

+

+ {formatDateTime(profile.last_login)} +

+
+
+
+
+
+ +
+
+

+ Account created +

+

+ {formatDateTime(profile.created_at)} +

+
+
+
+
- {/* Profile completion ring */} -
- - Profile Complete + {/* Account summary */} +
+

+ Account +

+ + +
+ Role + {profile.role} +
+
+ Status + + + {profile.status} + +
+
+ Email + + + {profile.email} + + + +
+
+ Phone + + + {profile.phone_number || "—"} + + + +
+
+
- - - - {/* Info Cards */} -
- {/* Personal Information */} - -
- -
-
- -
- - Personal Information - -
-
- - - - - - - - - - {/* Contact & Location */} - -
- -
-
- -
- - Contact & Location - -
-
- - } - /> - } - /> - - - - - - {/* Account Details */} - -
- -
-
- -
- - Account Details - -
-
- - - - - - - } - /> - - +
); diff --git a/src/pages/analytics/AnalyticsPage.tsx b/src/pages/analytics/AnalyticsPage.tsx index 61957f4..2c91abd 100644 --- a/src/pages/analytics/AnalyticsPage.tsx +++ b/src/pages/analytics/AnalyticsPage.tsx @@ -284,6 +284,7 @@ export function AnalyticsPage() { const [dashboard, setDashboard] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) + const [activeSummaryTab, setActiveSummaryTab] = useState<"key" | "content" | "operations">("key") const fetchData = async () => { setLoading(true) @@ -390,107 +391,166 @@ export function AnalyticsPage() {
+ {/* Summary Tabs */} +
+
+ + + +
+
+
- {/* ─── Key Metrics ─── */} -
-
- 0 ? "up" : "neutral"} - /> - 0 ? "up" : "neutral"} - /> - 0 ? "up" : "neutral"} - /> - = 0.5 ? "up" : "down"} - /> -
-
+ {activeSummaryTab === "key" && ( + <> + {/* ─── Key Metrics ─── */} +
+
+ 0 ? "up" : "neutral"} + /> + 0 ? "up" : "neutral"} + /> + 0 ? "up" : "neutral"} + /> + = 0.5 ? "up" : "down"} + /> +
+
+ + )} - {/* ─── Content & Platform ─── */} -
-
- - + {/* ─── Content & Platform ─── */} +
- - -
-
+ count={courses.total_courses + content.total_questions} + defaultOpen + > +
+ + + + +
+ + + )} - {/* ─── Operations ─── */} -
-
- - - 0 ? "up" : "neutral"} - /> - `${q.count} ${q.label.toLowerCase()}`).join(" · ")} - trend="neutral" - /> -
-
+ {activeSummaryTab === "operations" && ( + <> + {/* ─── Operations ─── */} +
+
+ + + 0 ? "up" : "neutral"} + /> + `${q.count} ${q.label.toLowerCase()}`).join(" · ")} + trend="neutral" + /> +
+
+ + )} {/* ─── User Analytics ─── */}
diff --git a/src/pages/content-management/AddQuestionPage.tsx b/src/pages/content-management/AddQuestionPage.tsx index 13ec07c..85f7251 100644 --- a/src/pages/content-management/AddQuestionPage.tsx +++ b/src/pages/content-management/AddQuestionPage.tsx @@ -1,6 +1,7 @@ import { useState } from "react" import { useNavigate, useParams } from "react-router-dom" import { ArrowLeft, Plus, X } from "lucide-react" +import { toast } from "sonner" import { Button } from "../../components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" import { Input } from "../../components/ui/input" @@ -105,32 +106,44 @@ export function AddQuestionPage() { // Validation if (!formData.question.trim()) { - alert("Please enter a question") + toast.error("Missing question", { + description: "Please enter a question before saving.", + }) return } if (formData.type === "multiple-choice" || formData.type === "true-false") { if (!formData.correctAnswer) { - alert("Please select a correct answer") + toast.error("Missing correct answer", { + description: "Select the correct answer for this question.", + }) return } if (formData.type === "multiple-choice") { const hasEmptyOptions = formData.options.some((opt) => !opt.trim()) if (hasEmptyOptions) { - alert("Please fill in all options") + toast.error("Incomplete options", { + description: "Fill in all answer options for this multiple choice question.", + }) return } } } else if (formData.type === "short-answer") { if (!formData.correctAnswer.trim()) { - alert("Please enter a correct answer") + toast.error("Missing correct answer", { + description: "Enter the expected correct answer.", + }) return } } // In a real app, save the question here console.log("Saving question:", formData) - alert(isEditing ? "Question updated successfully!" : "Question created successfully!") + toast.success(isEditing ? "Question updated" : "Question created", { + description: isEditing + ? "The question has been updated successfully." + : "Your new question has been created.", + }) navigate("/content/questions") } diff --git a/src/pages/content-management/AllCoursesPage.tsx b/src/pages/content-management/AllCoursesPage.tsx new file mode 100644 index 0000000..3935c08 --- /dev/null +++ b/src/pages/content-management/AllCoursesPage.tsx @@ -0,0 +1,584 @@ +import { useEffect, useState } from "react" +import { useNavigate } from "react-router-dom" +import { Search, Plus, RefreshCw, Edit2, ToggleLeft, ToggleRight } from "lucide-react" +import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" +import { Button } from "../../components/ui/button" +import { Input } from "../../components/ui/input" +import { Select } from "../../components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../../components/ui/table" +import { Badge } from "../../components/ui/badge" +import { FileUpload } from "../../components/ui/file-upload" +import { getCourseCategories, getCoursesByCategory, createCourse, updateCourseStatus, updateCourse } from "../../api/courses.api" +import type { Course, CourseCategory } from "../../types/course.types" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "../../components/ui/dialog" +import { Textarea } from "../../components/ui/textarea" +import { toast } from "sonner" + +type CourseWithCategory = Course & { category_name: string } + +export function AllCoursesPage() { + const navigate = useNavigate() + const [courses, setCourses] = useState([]) + const [categories, setCategories] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const [search, setSearch] = useState("") + const [categoryFilter, setCategoryFilter] = useState<"all" | string>("all") + + const [createOpen, setCreateOpen] = useState(false) + const [createCategoryId, setCreateCategoryId] = useState("") + const [createSubCategoryId, setCreateSubCategoryId] = useState("") + const [createTitle, setCreateTitle] = useState("") + const [createDescription, setCreateDescription] = useState("") + const [createThumbnail, setCreateThumbnail] = useState(null) + const [createVideo, setCreateVideo] = useState(null) + const [creating, setCreating] = useState(false) + const [togglingId, setTogglingId] = useState(null) + const [editOpen, setEditOpen] = useState(false) + const [courseToEdit, setCourseToEdit] = useState(null) + const [editTitle, setEditTitle] = useState("") + const [editDescription, setEditDescription] = useState("") + const [updating, setUpdating] = useState(false) + + const fetchAllCourses = async () => { + setLoading(true) + setError(null) + try { + const categoriesRes = await getCourseCategories() + const cats = categoriesRes.data.data.categories ?? [] + setCategories(cats) + + const allCourses: CourseWithCategory[] = [] + for (const cat of cats) { + const res = await getCoursesByCategory(cat.id) + const catCourses = res.data.data.courses ?? [] + allCourses.push( + ...catCourses.map((c) => ({ + ...c, + category_name: cat.name, + })), + ) + } + setCourses(allCourses) + } catch (err) { + console.error("Failed to load courses:", err) + setError("Failed to load courses") + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchAllCourses() + }, []) + + const filteredCourses = courses.filter((course) => { + if (categoryFilter !== "all" && String(course.category_id) !== categoryFilter) { + return false + } + if (search.trim()) { + const q = search.toLowerCase() + const haystack = `${course.title} ${course.description} ${course.category_name}`.toLowerCase() + if (!haystack.includes(q)) return false + } + return true + }) + + const handleCreateCourse = async () => { + const effectiveCategoryId = createSubCategoryId || createCategoryId + + if (!effectiveCategoryId || !createTitle.trim() || !createDescription.trim()) { + toast.error("Missing fields", { + description: "Category (or subcategory), title, and description are required.", + }) + return + } + + setCreating(true) + try { + await createCourse({ + category_id: Number(effectiveCategoryId), + title: createTitle.trim(), + description: createDescription.trim(), + }) + + toast.success("Course created", { + description: `"${createTitle.trim()}" has been created.`, + }) + + setCreateOpen(false) + setCreateCategoryId("") + setCreateSubCategoryId("") + setCreateTitle("") + setCreateDescription("") + setCreateThumbnail(null) + setCreateVideo(null) + await fetchAllCourses() + } catch (err: any) { + console.error("Failed to create course:", err) + toast.error("Failed to create course", { + description: err?.response?.data?.message || "Please try again.", + }) + } finally { + setCreating(false) + } + } + + const handleToggleStatus = async (course: CourseWithCategory) => { + setTogglingId(course.id) + try { + await updateCourseStatus(course.id, !course.is_active) + await fetchAllCourses() + } catch (err) { + console.error("Failed to update course status:", err) + toast.error("Failed to update course status") + } finally { + setTogglingId(null) + } + } + + const openEditDialog = (course: CourseWithCategory) => { + setCourseToEdit(course) + setEditTitle(course.title) + setEditDescription(course.description || "") + setEditOpen(true) + } + + const handleUpdateCourse = async () => { + if (!courseToEdit) return + if (!editTitle.trim() || !editDescription.trim()) { + toast.error("Missing fields", { + description: "Title and description are required.", + }) + return + } + + setUpdating(true) + try { + await updateCourse(courseToEdit.id, { + title: editTitle.trim(), + description: editDescription.trim(), + }) + toast.success("Course updated") + setEditOpen(false) + setCourseToEdit(null) + await fetchAllCourses() + } catch (err: any) { + console.error("Failed to update course:", err) + toast.error("Failed to update course", { + description: err?.response?.data?.message || "Please try again.", + }) + } finally { + setUpdating(false) + } + } + + if (loading) { + return ( +
+
+ +
+

Loading all courses…

+
+ ) + } + + if (error) { + return ( +
+
+
+ +
+

{error}

+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

All Courses

+

+ View and manage courses across all categories. +

+
+ +
+ + + + + Course Management + + + + {/* Search / Filters */} +
+
+ + setSearch(e.target.value)} + className="pl-10 transition-colors focus:border-brand-300 focus:ring-brand-200" + /> +
+
+ +
+
+ +
+ Showing {filteredCourses.length} of {courses.length} courses +
+ + {/* Courses Table */} + {filteredCourses.length > 0 ? ( +
+ + + + + Course + + + Category + + + Status + + + Actions + + + + + {filteredCourses.map((course, index) => ( + + navigate( + `/content/category/${course.category_id}/courses/${course.id}/sub-courses`, + ) + } + > + +
+ {course.title} +
+ {course.description && ( +
+ {course.description} +
+ )} +
+ + {course.category_name} + + + + {course.is_active ? "Active" : "Inactive"} + + + +
+ + + +
+
+
+ ))} +
+
+
+ ) : ( +
+
+ +
+

No courses found

+

+ Try adjusting your search or category filter, or create a new course. +

+
+ )} +
+
+ + {/* Create course dialog */} + + + + Create course + + Choose a category, add basic details, and optionally attach a thumbnail and intro + video. + + + +
+
+
+ + +
+
+ + +
+
+ + setCreateTitle(e.target.value)} + /> +
+
+ +
+ +