diff --git a/src/app/api/secure/exam-seating/student-batches/route.ts b/src/app/api/secure/exam-seating/student-batches/route.ts index 071f09b..a0b4429 100644 --- a/src/app/api/secure/exam-seating/student-batches/route.ts +++ b/src/app/api/secure/exam-seating/student-batches/route.ts @@ -114,3 +114,21 @@ export async function DELETE(request: NextRequest) { return NextResponse.json(results); } + +export async function GET(request: NextRequest) { + const userId = await getUserId(request); + if (!userId) + return NextResponse.json({ message: "Unauthenticated" }, { status: 401 }); + + const id = request.nextUrl.searchParams.get("id"); + if (!id) + return NextResponse.json({ message: "Invalid request" }, { status: 400 }); + + const result = await prisma.studentBatchForExam.findUnique({ + where: { + id: id, + }, + }); + + return NextResponse.json(result); +} diff --git a/src/app/dashboard/tools/exam-seating/student-batches/[batchId]/_components.tsx b/src/app/dashboard/tools/exam-seating/student-batches/[batchId]/_components.tsx new file mode 100644 index 0000000..be615a1 --- /dev/null +++ b/src/app/dashboard/tools/exam-seating/student-batches/[batchId]/_components.tsx @@ -0,0 +1,656 @@ +"use client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useEffect, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Ban, FileSpreadsheet, Hash, UploadCloud, User } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import Link from "next/link"; +import Papa from "papaparse"; +import { Label } from "@/components/ui/label"; +import Dropzone from "react-dropzone"; +import axios from "axios"; +import { StudentBatchForExam } from "@prisma/client"; +import { useToast } from "@/components/ui/use-toast"; +import { useRouter } from "next/navigation"; +import { usePermenantGet } from "@/lib/swr"; +import { mutate } from "swr"; + +const formSchema: z.ZodType<{ + name: string; + students: { + name?: string | undefined; + rollNumber?: string | undefined; + regNumber?: string | undefined; + admnNumber?: string | undefined; + primaryNumber?: string | undefined; + }[]; +}> = z.object({ + name: z.string().min(3, "Minimum 3 characters required"), + students: z + .array( + z.object({ + name: z.string().optional(), + primaryNumber: z.string(), + rollNumber: z.string().optional(), + regNumber: z.string().optional(), + admnNumber: z.string().optional(), + }) + ) + .min(1), +}); + +function EditStudentBatchComponent({ id }: { id?: string }) { + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onBlur", + }); + const { toast } = useToast(); + const router = useRouter(); + + const { data: existingData, mutate } = usePermenantGet( + `/api/secure/exam-seating/student-batches?id=${id}` + ); + + useEffect(() => { + form.reset({ + name: existingData?.name, + students: existingData?.students as any, + }); + }, [form, existingData]); + + const onSubmit = (data: z.infer) => { + axios + .put("/api/secure/exam-seating/student-batches", { + ...data, + id, + }) + .then((res) => { + mutate(); + toast({ + title: "Well done!", + description: `Class ${res.data.name} updated successfully`, + }); + router.push("/dashboard/tools/exam-seating/student-batches"); + }) + .catch((e) => { + toast({ + title: "Error!", + description: `Something went wrong`, + }); + }); + }; + + const [isOpen, setIsOpen] = useState(false); + const modalType = useRef<"add-student" | "add-roll" | "add-csv">( + "add-student" + ); + const students = form.watch("students") || []; + return ( + +
+ +
+ ( + + Batch name + + + + Enter batch name. + + + )} + /> +
+
+ + + + + + + + (modalType.current = "add-student")} + > + + Add single + + (modalType.current = "add-roll")} + > + + Add from Roll No / Reg No + + (modalType.current = "add-csv")} + > + + Add from CSV + + + + + + {students.length < 1 && ( +
+
+ + No students added +
+
+ )} + + {students.length > 0 && ( + + A list of students you added. + + + Name + Reg No + Roll No + Admn No + Display No + + + + + {students.map((student) => ( + + + {student.name} + + {student.regNumber} + {student.rollNumber} + {student.admnNumber} + + {student.primaryNumber} + + + + + ))} + +
+ )} +
+ +
+ + + {modalType.current === "add-student" && ( + { + form.setValue("students", [...students, e]); + setIsOpen(false); + }} + /> + )} + {modalType.current === "add-csv" && ( + { + form.setValue("students", [...students, ...e]); + setIsOpen(false); + }} + /> + )} + {modalType.current === "add-roll" && ( + { + form.setValue("students", [...students, ...e]); + setIsOpen(false); + }} + /> + )} + +
+ ); +} + +export default EditStudentBatchComponent; + +const studentFormSchema = z + .object({ + name: z.string(), + rollNumber: z.string().optional(), + regNumber: z.string().optional(), + admnNumber: z.string().optional(), + }) + .refine((data) => data.rollNumber || data.regNumber || data.admnNumber, { + message: "Either any of number should be filled in.", + path: ["rollNumber", "regNumber", "admnNumber"], + }); + +type StudentType = z.infer["students"][0]; + +function SingleModal({ onAdd }: { onAdd?: (data: StudentType) => void }) { + const form = useForm>({ + resolver: zodResolver(studentFormSchema), + }); + + const onSubmit = (data: z.infer) => { + onAdd?.({ + ...data, + primaryNumber: data.regNumber || data.rollNumber || data.admnNumber, + }); + }; + + return ( +
+ + + Add student + Enter student details here + +
+ ( + + Student Name + + + + Enter student full name. + + + )} + /> + ( + + Roll No + + + + + Enter student roll number. (optional field) + + + + )} + /> + ( + + Register number + + + + + Enter student register number. (optional field) + + + + )} + /> + ( + + Admission number + + + + + Enter student admission number. (optional field) + + + + )} + /> +
+ +
+ + ); +} + +type studentSchemaType = z.infer; + +const parseCsv = ( + inputFile: File, + onSuccess: (e: studentSchemaType[]) => void = (e) => console.log(e), + onError: (e: string) => void = (e) => console.log(e) +) => { + const reader = new FileReader(); + + reader.onload = (event) => { + if (!event.target) return; + const file = event.target.result; + if (typeof file !== "string") return; + let allLines = file.split(/\r\n|\n/); + // Reading line by line + if (allLines.length < 2) { + onError("File is empty"); + return; + } + Papa.parse(file, { + header: true, + skipEmptyLines: true, + complete: function (results) { + onSuccess(results.data); + }, + }); + }; +}; + +function CsvModal({ + onAdd, +}: { + onAdd?: (data: z.infer["students"]) => void; +}) { + const [error, seterror] = useState(""); + const verifyAndFormatFile = async (e: File) => { + parseCsv( + e, + (k) => { + onAdd?.( + k.map((l) => ({ + ...l, + primaryNumber: l.regNumber || l.rollNumber || l.admnNumber, + })) + ); + }, + seterror + ); + }; + return ( + <> + + Add students from CSV + + Upload your csv file contianing student data here + + +
+

+ You can download a sample .csv file{" "} + + from here + + . Modifying and re-uploading it is recommeneded. You can also try to + create your own csv file. We only look for headers{" "} + + name, rollNumber, regNumber, admnNumber + + . +

+ { + verifyAndFormatFile(acceptedFiles[0]); + }} + > + {({ getRootProps, getInputProps, acceptedFiles, isDragReject }) => ( +
+ {acceptedFiles.length > 0 ? ( +
+ +

+ {acceptedFiles[0].name} +

+

+ Click to upload another file or + drag and drop +

+
+ ) : ( +
+ {isDragReject ? ( + <> + +

+ + File format not supported + +

+ + ) : ( + <> + +

+ Click to upload{" "} + or drag and drop +

+ + )} +

+ CSV files only. +
Results won't be accurate if you upload modified + files. +

+
+ )} + +
+ )} +
+
+ {error !== "" &&

{error}

} + + + ); +} + +const rollFormSchema = z.object({ + start: z.string().optional(), + end: z.string().optional(), + exclude: z + .string() + .optional() + .refine( + (e) => { + if (!e) return true; + return e.split(",").every((e) => !isNaN(parseInt(e))); + }, + { + message: "Please enter a valid numbers seperated by comma", + } + ), + prefix: z.string().optional(), +}); + +function RollModal({ + onAdd, +}: { + onAdd?: (data: z.infer["students"]) => void; +}) { + const form = useForm>({ + resolver: zodResolver(rollFormSchema), + defaultValues: { + start: "", + end: "", + prefix: "", + exclude: "", + }, + }); + + const onSubmit = (data: z.infer) => { + const students: StudentType[] = []; + if (data.start && data.end) { + const start = parseInt(data.start); + const end = parseInt(data.end); + const exclude = data.exclude?.split(",").map((e) => parseInt(e)); + for (let i = start; i <= end; i++) { + if (exclude?.includes(i)) continue; + students.push({ + name: `${data.prefix || ""}${i}`, + rollNumber: i.toString(), + primaryNumber: i.toString(), + }); + } + } + onAdd?.(students); + }; + + return ( +
+ + + Add student + Enter student details here + +
+ ( + + Starting roll no + + + + Enter starting roll number. + + + )} + /> + ( + + Ending roll no + + + + Enter ending roll number. + + + )} + /> + ( + + Roll no.s to exclude + + + + + Enter numbers seperated by comma + + + + )} + /> + ( + + Prefix + + + + + Enter prefix to add with roll no as name + + + + )} + /> +
+ +
+ + ); +} diff --git a/src/app/dashboard/tools/exam-seating/student-batches/[batchId]/page.tsx b/src/app/dashboard/tools/exam-seating/student-batches/[batchId]/page.tsx new file mode 100644 index 0000000..b7026a3 --- /dev/null +++ b/src/app/dashboard/tools/exam-seating/student-batches/[batchId]/page.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import EditStudentBatchComponent from "./_components"; +import { prisma } from "@/server/db/prisma"; +import { useProfile } from "@/lib/swr"; +import { getUserId } from "@/components/auth/server"; + +async function page({ params }: { params: { batchId: string } }) { + return ( +
+
+

+ Edit student batch +

+
+
+ +
+
+ ); +} + +export default page; diff --git a/src/app/dashboard/tools/exam-seating/student-batches/_components/StudentBatchTable.tsx b/src/app/dashboard/tools/exam-seating/student-batches/_components/StudentBatchTable.tsx index 56b892b..bd1d970 100644 --- a/src/app/dashboard/tools/exam-seating/student-batches/_components/StudentBatchTable.tsx +++ b/src/app/dashboard/tools/exam-seating/student-batches/_components/StudentBatchTable.tsx @@ -35,6 +35,7 @@ import axios from "axios"; import { mutate } from "swr"; import { Skeleton } from "@/components/ui/skeleton"; import { StudentBatchForExam } from "@prisma/client"; +import Link from "next/link"; export const columns: ColumnDef[] = [ { @@ -78,7 +79,11 @@ export const columns: ColumnDef[] = [ Actions - Edit + + Edit + diff --git a/src/app/dashboard/tools/exam-seating/student-batches/new/_components.tsx b/src/app/dashboard/tools/exam-seating/student-batches/new/_components.tsx index e12acb5..67027ca 100644 --- a/src/app/dashboard/tools/exam-seating/student-batches/new/_components.tsx +++ b/src/app/dashboard/tools/exam-seating/student-batches/new/_components.tsx @@ -196,6 +196,14 @@ function NewStudentBatchComponent() {