Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
00556ac
build analytics page outline
n1sh1thaS May 12, 2025
20a1c20
create table design
n1sh1thaS May 12, 2025
ac21bbe
build attendance pie chart
n1sh1thaS May 12, 2025
3cd7ac5
fetch past question data
n1sh1thaS May 19, 2025
eec995b
fetch class average data
n1sh1thaS May 19, 2025
30383d8
fix lint errors and past question card issue
n1sh1thaS May 20, 2025
1356154
fetch student poll and attendance scores
n1sh1thaS May 20, 2025
c8d3a91
build analytics page buttons
n1sh1thaS May 20, 2025
a8d10bf
create attendance rate chart
n1sh1thaS May 27, 2025
f9b2b9c
edit student table, add search functionality
n1sh1thaS May 27, 2025
8779425
change to protected route
n1sh1thaS May 28, 2025
df024a9
Merge branch 'main' of https://github.com/CSES-Dev/webclicker-plusplu…
n1sh1thaS May 28, 2025
a39abb1
create page header
n1sh1thaS May 28, 2025
77e1e5e
create layout for questionnaire and analytics
n1sh1thaS May 28, 2025
3a1b246
ui improvements with charts, table, and header
n1sh1thaS May 28, 2025
e636f8a
use debounce for attendance chart
n1sh1thaS May 28, 2025
745574b
fix lint errors
n1sh1thaS May 28, 2025
8766ffd
extract calculations as utils
n1sh1thaS May 29, 2025
84f5ab4
fix table height
n1sh1thaS May 29, 2025
3ff7f29
fix lint errors
n1sh1thaS May 29, 2025
6844ce0
fix student data error
n1sh1thaS May 29, 2025
db79296
create separate components, increase debounce timer
n1sh1thaS Jun 2, 2025
3523ab4
fix loading issue
n1sh1thaS Jun 2, 2025
2379dd5
minor fixes and refactoring
j3rrythomas Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 145 additions & 151 deletions app/api/updateCourse/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,173 +5,167 @@ import { authOptions } from "@/lib/auth";
import prisma from "@/lib/prisma";

const updateSchema = z.object({
title: z.string().min(2),
color: z.string().length(7),
days: z.array(z.string()).min(1),
startTime: z.string(),
endTime: z.string(),
title: z.string().min(2),
color: z.string().length(7),
days: z.array(z.string()).min(1),
startTime: z.string(),
endTime: z.string(),
});

function getCourseId(request: Request): number {
const url = new URL(request.url);
const id = url.pathname.split('/').pop();
const courseId = parseInt(id ?? '');
if (isNaN(courseId)) {
throw new Error('Invalid course ID');
}
return courseId;
const url = new URL(request.url);
const id = url.pathname.split("/").pop();
const courseId = parseInt(id ?? "");

if (isNaN(courseId)) {
throw new Error("Invalid course ID");
}
return courseId;
}

// PUT - Update Course
export async function PUT(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

try {
const courseId = getCourseId(request);

// Verify user has permission
const userCourse = await prisma.userCourse.findUnique({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},
},
});

if (!userCourse || userCourse.role !== "LECTURER") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

// Validate request body
const body: unknown = await request.json();
const parsed = updateSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid input", details: parsed.error.flatten() },
{ status: 400 }
);
}
try {
const courseId = getCourseId(request);

const { title, color, days, startTime, endTime } = parsed.data;

// Update course and schedule in transaction
const [updatedCourse] = await prisma.$transaction([
prisma.course.update({
where: { id: courseId },
data: { title, color },
}),
prisma.schedule.updateMany({
where: { courseId },
data: { dayOfWeek: days, startTime, endTime },
}),
]);

return NextResponse.json(updatedCourse);
} catch (error) {
if (error instanceof Error && error.message === 'Invalid course ID') {
return NextResponse.json({ error: "Invalid course ID" }, { status: 400 });
// Verify user has permission
const userCourse = await prisma.userCourse.findUnique({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},
},
});

if (!userCourse || userCourse.role !== "LECTURER") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

// Validate request body
const body: unknown = await request.json();
const parsed = updateSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid input", details: parsed.error.flatten() },
{ status: 400 },
);
}

const { title, color, days, startTime, endTime } = parsed.data;

// Update course and schedule in transaction
const [updatedCourse] = await prisma.$transaction([
prisma.course.update({
where: { id: courseId },
data: { title, color },
}),
prisma.schedule.updateMany({
where: { courseId },
data: { dayOfWeek: days, startTime, endTime },
}),
]);

return NextResponse.json(updatedCourse);
} catch (error) {
if (error instanceof Error && error.message === "Invalid course ID") {
return NextResponse.json({ error: "Invalid course ID" }, { status: 400 });
}

console.error("Error updating course:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}

console.error("Error updating course:", error);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}

// DELETE - Delete Course
export async function DELETE(request: Request) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

try {
const courseId = getCourseId(request);

// Verify user has permission
const userCourse = await prisma.userCourse.findUnique({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},
},
});

if (!userCourse) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (userCourse.role === "LECTURER") {
// Lecturer - delete all related records in proper order
await prisma.$transaction([
prisma.response.deleteMany({
where: {
question: {
session: {
courseId,
},
},
},
}),
prisma.option.deleteMany({
where: {
question: {
session: {
courseId,
},
},
},
}),
prisma.question.deleteMany({
where: {
session: {
courseId,
},
},
}),
prisma.courseSession.deleteMany({
where: { courseId },
}),
prisma.schedule.deleteMany({
where: { courseId },
}),
prisma.userCourse.deleteMany({
where: { courseId },
}),
prisma.course.delete({
where: { id: courseId },
}),
]);
} else {
// Student - only remove their association
await prisma.userCourse.delete({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},
},
});
}
try {
const courseId = getCourseId(request);

return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof Error && error.message === 'Invalid course ID') {
return NextResponse.json({ error: "Invalid course ID" }, { status: 400 });
// Verify user has permission
const userCourse = await prisma.userCourse.findUnique({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},
},
});

if (!userCourse) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

if (userCourse.role === "LECTURER") {
// Lecturer - delete all related records in proper order
await prisma.$transaction([
prisma.response.deleteMany({
where: {
question: {
session: {
courseId,
},
},
},
}),
prisma.option.deleteMany({
where: {
question: {
session: {
courseId,
},
},
},
}),
prisma.question.deleteMany({
where: {
session: {
courseId,
},
},
}),
prisma.courseSession.deleteMany({
where: { courseId },
}),
prisma.schedule.deleteMany({
where: { courseId },
}),
prisma.userCourse.deleteMany({
where: { courseId },
}),
prisma.course.delete({
where: { id: courseId },
}),
]);
} else {
// Student - only remove their association
await prisma.userCourse.delete({
where: {
userId_courseId: {
userId: session.user.id,
courseId,
},
},
});
}

return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof Error && error.message === "Invalid course ID") {
return NextResponse.json({ error: "Invalid course ID" }, { status: 400 });
}

console.error("Error deleting course:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}

console.error("Error deleting course:", error);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}
}
52 changes: 52 additions & 0 deletions app/dashboard/course/[courseId]/(courseInfo)/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { useParams } from "next/navigation";
import React, { useState } from "react";
import AttendanceLineChart from "@/components/ui/AttendanceLineChart";
import PerformanceData from "@/components/ui/PerformanceData";
import StudentTable from "@/components/ui/StudentTable";
import { analyticsPages } from "@/lib/constants";

export default function Page() {
const params = useParams();
const courseId = parseInt((params.courseId as string) ?? "0");
const [page, setPage] = useState<string>("Performance");

return (
<div className="w-full flex flex-col">
<div className="flex flex-row gap-2 bg-slate-200 h-fit w-fit p-1 rounded-md mb-4">
{analyticsPages.map((pageTitle: string) => (
<button
key={pageTitle}
className={`p-2 h-fit px-4 rounded-md transition-colors duration-300 ease-in-out ${
page === pageTitle
? "bg-white text-[#1441DB]"
: "bg-slate-200 text-slate-500"
}`}
onClick={() => {
setPage(pageTitle);
}}
>
{pageTitle}
</button>
))}
</div>
{/* Performance and Attendance Data */}
<div className="flex flex-col md:flex-row justify-center md:justify-between items-center md:items-stretch bg-white overflow-auto h-96 max-h-96 w-full rounded-[20px] border border-[#A5A5A5]">
{page === "Performance" ? (
<PerformanceData courseId={courseId} />
) : (
<AttendanceLineChart courseId={courseId} />
)}
</div>
<div className="flex flex-row justify-between mt-8 pl-1">
<h1 className="text-2xl font-normal">Student Data</h1>
<button className="h-10 w-40 px-3 bg-[hsl(var(--primary))] text-white rounded-lg focus:outline-none">
Export CSV
</button>
</div>
{/* Student Data Table */}
<StudentTable courseId={courseId} />
</div>
);
}
Loading