From afb3dedec100880f7c6a688299dd0879f2c3c3af Mon Sep 17 00:00:00 2001 From: Eshanhasitha Date: Thu, 4 Jun 2026 01:12:58 +0530 Subject: [PATCH] implement admin dashboard and student management API with data integrity scripts --- app/admin/dashboard/page.tsx | 291 ++++++++++++----------- app/admin/students/[index]/page.tsx | 3 +- app/admin/students/page.tsx | 25 +- app/admin/upload/Step1SubjectDetails.tsx | 226 +++++++++++++++--- app/admin/upload/Step4Success.tsx | 9 +- app/admin/upload/page.tsx | 104 ++++---- app/api/admin/dashboard/stats/route.ts | 9 +- app/api/admin/results/save/route.ts | 52 ++-- app/api/admin/students/[index]/route.ts | 2 + app/api/admin/students/route.ts | 6 + app/api/admin/subjects/route.ts | 10 +- app/api/admin/uploads/[id]/route.ts | 46 ++++ app/api/student/gpa/route.ts | 2 + app/student/change-password/page.tsx | 2 +- app/student/dashboard/page.tsx | 22 +- components/admin/AdminSidebar.tsx | 5 +- lib/grades.ts | 17 +- lib/parsePDF.ts | 7 +- lib/schema.ts | 2 + package.json | 5 +- scripts/deduplicateSubjects.ts | 70 ++++++ scripts/fixIsGpa.ts | 49 ++++ scripts/fixSemesterData.ts | 116 +++++++++ scripts/seedSubjects.ts | 169 +++++++++++++ 24 files changed, 977 insertions(+), 272 deletions(-) create mode 100644 app/api/admin/uploads/[id]/route.ts create mode 100644 scripts/deduplicateSubjects.ts create mode 100644 scripts/fixIsGpa.ts create mode 100644 scripts/fixSemesterData.ts create mode 100644 scripts/seedSubjects.ts diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx index 5cd102d..075b7ba 100644 --- a/app/admin/dashboard/page.tsx +++ b/app/admin/dashboard/page.tsx @@ -1,3 +1,4 @@ +// app/admin/dashboard/page.tsx "use client"; import { useEffect, useState } from "react"; @@ -13,26 +14,77 @@ import { Loader2, FileText, ArrowRight, + Trash2, + RefreshCw, } from "lucide-react"; +// Confirmation Dialog Component +function ConfirmationDialog({ + open, + title, + message, + onConfirm, + onCancel, +}: { + open: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; +}) { + if (!open) return null; + return ( + +
+

{title}

+

{message}

+
+ + +
+
+
+ ); +} + // ─── Types ────────────────────────────────────────────────────────────────── +interface RecentUpload { + id: number; + filename: string; + status: string; + processedCount: number; + createdAt: string; +} interface DashboardStats { totalStudents: number; totalSubjects: number; totalResults: number; studentsAtRisk: number; - recentUploads: { - id: number; - filename: string; - status: string; - processedCount: number; - createdAt: string; - }[]; + recentUploads: RecentUpload[]; } -// ─── Stat Card ────────────────────────────────────────────────────────────── +interface StatCardProps { + title: string; + value: number; + icon: React.ElementType; + gradient: string; + shadow: string; + onClick?: () => void; + loading: boolean; +} +// Stat Card Component function StatCard({ title, value, @@ -41,32 +93,19 @@ function StatCard({ shadow, onClick, loading, -}: { - title: string; - value: number; - icon: React.ElementType; - gradient: string; - shadow: string; - onClick?: () => void; - loading: boolean; -}) { +}: StatCardProps) { const Wrapper = onClick ? "button" : "div"; return ( {/* Gradient glow background */}
-

{title}

@@ -78,55 +117,26 @@ function StatCard({

)}
-
- - {onClick && ( -
- View details - -
- )} ); } // ─── Status Badge ─────────────────────────────────────────────────────────── - function StatusBadge({ status }: { status: string }) { const config: Record = { - completed: { - bg: "bg-emerald-500/10", - text: "text-emerald-400", - dot: "bg-emerald-400", - }, - failed: { - bg: "bg-red-500/10", - text: "text-red-400", - dot: "bg-red-400", - }, - processing: { - bg: "bg-amber-500/10", - text: "text-amber-400", - dot: "bg-amber-400", - }, - pending: { - bg: "bg-slate-500/10", - text: "text-slate-400", - dot: "bg-slate-400", - }, + completed: { bg: "bg-emerald-500/10", text: "text-emerald-400", dot: "bg-emerald-400" }, + failed: { bg: "bg-red-500/10", text: "text-red-400", dot: "bg-red-400" }, + processing: { bg: "bg-amber-500/10", text: "text-amber-400", dot: "bg-amber-400" }, + pending: { bg: "bg-slate-500/10", text: "text-slate-400", dot: "bg-slate-400" }, }; - const c = config[status] || config.pending; - return ( - + {status.charAt(0).toUpperCase() + status.slice(1)} @@ -134,54 +144,76 @@ function StatusBadge({ status }: { status: string }) { } // ─── Dashboard Page ───────────────────────────────────────────────────────── - export default function DashboardPage() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const router = useRouter(); + const [confirmOpen, setConfirmOpen] = useState(false); + const [selectedUploadId, setSelectedUploadId] = useState(null); - useEffect(() => { - async function fetchStats() { - try { - const res = await fetch("/api/admin/dashboard/stats"); - if (!res.ok) throw new Error("Failed to fetch stats"); - const data = await res.json(); - setStats(data); - } catch (err) { - setError(err instanceof Error ? err.message : "An error occurred"); - } finally { - setLoading(false); - } + const fetchStats = async () => { + try { + const res = await fetch("/api/admin/dashboard/stats", { + cache: "no-store", + }); + if (!res.ok) throw new Error("Failed to fetch stats"); + const data = await res.json(); + setStats(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setLoading(false); } + }; + + useEffect(() => { fetchStats(); }, []); + const deleteUpload = async (id: number) => { + try { + const res = await fetch(`/api/admin/uploads/${id}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error("Failed to delete upload"); + await fetchStats(); + } catch (err) { + setError(err instanceof Error ? err.message : "Deletion error"); + } finally { + setConfirmOpen(false); + setSelectedUploadId(null); + } + }; + return (
- - {/* ── Main Content ───────────────────────────────────────────────── */}
- {/* ── Header ────────────────────────────────────────────────── */} -
-

- Dashboard -

-

- Overview of the GPA portal -

+
+
+

Dashboard

+

Overview of the GPA portal

+
+
- {/* ── Error State ───────────────────────────────────────────── */} + {/* Error State */} {error && (
{error}
)} - {/* ── Stat Cards ────────────────────────────────────────────── */} + {/* Stat Cards */}
- {/* ── Quick Actions ─────────────────────────────────────────── */} + {/* Quick Actions */}
- {/* ── Recent Uploads ────────────────────────────────────────── */} + {/* Recent Uploads */}
-

- Recent Uploads -

+

Recent Uploads

- {loading ? (
) : !stats?.recentUploads?.length ? ( -
- No uploads yet. Upload your first result sheet to get started. -
+
No uploads yet. Upload your first result sheet to get started.
) : (
- - - - + + + + + {stats.recentUploads.map((upload) => ( - - - - + + + + + ))} @@ -318,6 +319,14 @@ export default function DashboardPage() { + {/* Confirmation Dialog */} + selectedUploadId && deleteUpload(selectedUploadId)} + onCancel={() => setConfirmOpen(false)} + /> ); } diff --git a/app/admin/students/[index]/page.tsx b/app/admin/students/[index]/page.tsx index 3bd94db..3438284 100644 --- a/app/admin/students/[index]/page.tsx +++ b/app/admin/students/[index]/page.tsx @@ -146,7 +146,8 @@ export default function StudentDetailPage() { async function fetchStudent() { try { const res = await fetch( - `/api/admin/students/${encodeURIComponent(indexNumber)}` + `/api/admin/students/${encodeURIComponent(indexNumber)}`, + { cache: "no-store" } ); if (!res.ok) { if (res.status === 404) throw new Error("Student not found"); diff --git a/app/admin/students/page.tsx b/app/admin/students/page.tsx index b9ce2af..4ae48a5 100644 --- a/app/admin/students/page.tsx +++ b/app/admin/students/page.tsx @@ -11,6 +11,7 @@ import { Loader2, Users, Filter, + RefreshCw, } from "lucide-react"; // ─── Types ────────────────────────────────────────────────────────────────── @@ -135,7 +136,9 @@ function StudentsPageContent() { params.set("page", page.toString()); params.set("limit", "20"); - const res = await fetch(`/api/admin/students?${params.toString()}`); + const res = await fetch(`/api/admin/students?${params.toString()}`, { + cache: "no-store", + }); if (!res.ok) throw new Error("Failed to fetch students"); const data: StudentsResponse = await res.json(); @@ -161,11 +164,21 @@ function StudentsPageContent() { return ( <> {/* ── Header ────────────────────────────────────────────────── */} -
-

Students

-

- Browse and manage student records -

+
+
+

Students

+

+ Browse and manage student records +

+
+
{/* ── Search & Filters ──────────────────────────────────────── */} diff --git a/app/admin/upload/Step1SubjectDetails.tsx b/app/admin/upload/Step1SubjectDetails.tsx index 9310b21..4b0325b 100644 --- a/app/admin/upload/Step1SubjectDetails.tsx +++ b/app/admin/upload/Step1SubjectDetails.tsx @@ -7,13 +7,14 @@ import { Select } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { useEffect, useState, useCallback } from "react"; +import React from "react"; interface SubjectFormData { subjectCode: string; subjectName: string; creditPoints: number; yearNumber: number; - semesterNumber: number; + semesterNumber: number; // relative: 1 or 2 within the year (matches DB) examType: "Proper" | "Repeat"; isGpa: boolean; subjectId: number | null; @@ -21,21 +22,70 @@ interface SubjectFormData { interface Step1Props { formData: SubjectFormData; - setFormData: (data: SubjectFormData) => void; + setFormData: React.Dispatch>; onNext: () => void; } +/** + * Semester labels: the form stores semesterNumber as 1 or 2 (relative within year), + * but displays absolute semester numbers to the user: + * Year 1, Sem 1 → "Semester 1" Year 1, Sem 2 → "Semester 2" + * Year 2, Sem 1 → "Semester 3" Year 2, Sem 2 → "Semester 4" + * Year 3, Sem 1 → "Semester 5" Year 3, Sem 2 → "Semester 6" + * Year 4, Sem 1 → "Semester 7" Year 4, Sem 2 → "Semester 8" + */ +const SEM_LABEL: Record> = { + 1: { 1: "Semester 1", 2: "Semester 2" }, + 2: { 1: "Semester 3", 2: "Semester 4" }, + 3: { 1: "Semester 5", 2: "Semester 6" }, + 4: { 1: "Semester 7", 2: "Semester 8" }, +}; + export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Props) { const [codeStatus, setCodeStatus] = useState<"idle" | "existing" | "new">("idle"); - const [checking, setChecking] = useState(false); + const [checking, setChecking] = useState(false); + const [allSubjects, setAllSubjects] = useState([]); + const [suggestions, setSuggestions] = useState([]); + + // Fetch all subjects once on mount for suggestion list + useEffect(() => { + const fetchAll = async () => { + try { + const res = await fetch("/api/admin/subjects"); + if (!res.ok) throw new Error("Failed to fetch subjects"); + const data = await res.json(); + // Deduplicate by subjectCode in case DB returns duplicates + const seen = new Set(); + const unique = (data.subjects || []).filter((s: any) => { + if (seen.has(s.subjectCode)) return false; + seen.add(s.subjectCode); + return true; + }); + setAllSubjects(unique); + } catch (e) { + console.error(e); + } + }; + fetchAll(); + }, []); + // allFilled: + // GPA subjects need creditPoints > 0 + // Non-GPA subjects need creditPoints >= 0 (0 is valid for no-credit subjects) + // creditPoints === -1 means "not yet selected" → always blocks Next const allFilled = formData.subjectCode.trim() !== "" && formData.subjectName.trim() !== "" && - formData.creditPoints > 0 && + (formData.isGpa ? formData.creditPoints > 0 : formData.creditPoints >= 0) && formData.yearNumber > 0 && formData.semesterNumber > 0; + /** + * Look up the subject code in the DB and auto-fill the form. + * Uses functional setState (prev =>) to avoid stale closure issues. + * semesterNumber from DB is already relative (1-2) — no conversion needed. + * isGpa from DB is set directly — this is what drives the GPA/Non-GPA button. + */ const checkSubjectCode = useCallback(async () => { const code = formData.subjectCode.trim().toUpperCase().replace(/\s+/g, ""); if (!code) return; @@ -45,31 +95,52 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop const res = await fetch("/api/admin/subjects"); if (!res.ok) throw new Error("Failed to fetch"); const data = await res.json(); - const match = data.subjects?.find( - (s: any) => s.subjectCode === code - ); + const match = data.subjects?.find((s: any) => s.subjectCode === code); + if (match) { setCodeStatus("existing"); - setFormData({ - ...formData, + // Use functional update to always work on latest state, never stale closure + setFormData((prev) => ({ + ...prev, subjectCode: code, subjectName: match.subjectName, creditPoints: match.creditPoints, - yearNumber: match.yearNumber, - semesterNumber: match.semesterNumber, - isGpa: match.isGpa ?? true, + yearNumber: match.yearNumber, // DB stores 1-4 ✓ + semesterNumber: match.semesterNumber, // DB stores 1-2 (relative) ✓ + isGpa: typeof match.isGpa === "boolean" ? match.isGpa : true, subjectId: match.id, - }); + })); } else { setCodeStatus("new"); - setFormData({ ...formData, subjectCode: code, subjectId: null }); + setFormData((prev) => ({ + ...prev, + subjectCode: code, + subjectName: "", + creditPoints: -1, // reset to "not yet selected" + yearNumber: 0, + semesterNumber: 0, + isGpa: true, + subjectId: null, + })); } } catch { setCodeStatus("idle"); } finally { setChecking(false); } - }, [formData.subjectCode]); + }, [formData.subjectCode, setFormData]); + + // Debounce: re-check whenever the subject code changes + useEffect(() => { + if (!formData.subjectCode) { + setCodeStatus("idle"); + return; + } + const handler = setTimeout(() => { + checkSubjectCode(); + }, 500); + return () => clearTimeout(handler); + }, [formData.subjectCode, checkSubjectCode]); return ( @@ -79,20 +150,69 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop + {/* Subject Code */} -
+
- setFormData({ ...formData, subjectCode: e.target.value.toUpperCase() }) - } - onBlur={checkSubjectCode} + onChange={(e) => { + const val = e.target.value.toUpperCase(); + setFormData((prev) => ({ ...prev, subjectCode: val })); + if (val.length >= 2) { + // Deduplicate suggestions by subjectCode + const seen = new Set(); + const filtered = allSubjects.filter((s) => { + if (!s.subjectCode.toUpperCase().includes(val)) return false; + if (seen.has(s.subjectCode)) return false; + seen.add(s.subjectCode); + return true; + }); + setSuggestions(filtered.slice(0, 6)); + } else { + setSuggestions([]); + } + }} + onBlur={() => { + // Small delay so onMouseDown on suggestion fires first + setTimeout(() => checkSubjectCode(), 150); + }} className="bg-slate-800 border-slate-700 text-slate-100 uppercase" /> - {checking &&

Checking...

} + + {/* Suggestions dropdown */} + {suggestions.length > 0 && ( +
    + {suggestions.map((s) => ( +
  • { + // Functional update — no stale closure + setFormData((prev) => ({ + ...prev, + subjectCode: s.subjectCode, + subjectName: s.subjectName, + creditPoints: s.creditPoints, + yearNumber: s.yearNumber, // DB 1-4 ✓ + semesterNumber: s.semesterNumber, // DB 1-2 relative ✓ + isGpa: typeof s.isGpa === "boolean" ? s.isGpa : true, + subjectId: s.id, + })); + setSuggestions([]); + setCodeStatus("existing"); + }} + > + {s.subjectCode} + – {s.subjectName} +
  • + ))} +
+ )} + + {checking &&

Checking…

} {codeStatus === "existing" && ( ✓ Existing subject — details pre-filled @@ -112,7 +232,7 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop id="subjectName" placeholder="e.g. System Analysis & Design" value={formData.subjectName} - onChange={(e) => setFormData({ ...formData, subjectName: e.target.value })} + onChange={(e) => setFormData((prev) => ({ ...prev, subjectName: e.target.value }))} className="bg-slate-800 border-slate-700 text-slate-100" />
@@ -123,14 +243,29 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop
@@ -141,7 +276,13 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop setFormData({ ...formData, semesterNumber: Number(e.target.value) })} + onChange={(e) => + setFormData((prev) => ({ ...prev, semesterNumber: Number(e.target.value) })) + } className="bg-slate-800 border-slate-700 text-slate-100" > - - + {formData.yearNumber > 0 && ( + <> + {/* value is relative (1 or 2), label shows absolute semester number */} + + + + )}
@@ -177,7 +325,7 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop name="examType" value={type} checked={formData.examType === type} - onChange={() => setFormData({ ...formData, examType: type })} + onChange={() => setFormData((prev) => ({ ...prev, examType: type }))} className="accent-blue-500" /> {type} @@ -186,13 +334,13 @@ export function Step1SubjectDetails({ formData, setFormData, onNext }: Step1Prop - {/* Subject Type (GPA / Non-GPA) */} + {/* Subject Type (GPA / Non-GPA) — reflects formData.isGpa directly */}
diff --git a/app/admin/upload/Step4Success.tsx b/app/admin/upload/Step4Success.tsx index 67424ba..b040e44 100644 --- a/app/admin/upload/Step4Success.tsx +++ b/app/admin/upload/Step4Success.tsx @@ -40,7 +40,7 @@ export function Step4Success({ saved, created, skipped, onUploadAnother }: Step4
{/* Actions */} -
+
+
diff --git a/app/admin/upload/page.tsx b/app/admin/upload/page.tsx index b990f7b..0e9e821 100644 --- a/app/admin/upload/page.tsx +++ b/app/admin/upload/page.tsx @@ -6,6 +6,7 @@ import { Step1SubjectDetails } from "./Step1SubjectDetails"; import { Step2UploadMD } from "./Step2UploadOCR"; import { Step3ReviewEdit } from "./Step3ReviewEdit"; import { Step4Success } from "./Step4Success"; +import { AdminSidebar } from "@/components/admin/AdminSidebar"; import { GRADE_POINTS } from "@/lib/grades"; import type { ParsedSheet } from "@/lib/parseMD"; @@ -30,7 +31,7 @@ interface StudentRow { const INITIAL_FORM: SubjectFormData = { subjectCode: "", subjectName: "", - creditPoints: 0, + creditPoints: -1, // -1 = "not yet selected"; 0 = Non-GPA subject with no credits yearNumber: 0, semesterNumber: 0, examType: "Proper", @@ -129,56 +130,59 @@ export default function UploadPage() { return (
-
- {/* Header */} -
-

- 📤 Upload Result Sheet -

-

- Upload a markdown (.md) results file and save student grades -

+ +
+
+ {/* Header */} +
+

+ 📤 Upload Result Sheet +

+

+ Upload a markdown (.md) results file and save student grades +

+
+ + {/* Step Indicator */} + + + {/* Step Content */} + {step === 1 && ( + setStep(2)} + /> + )} + + {step === 2 && ( + + )} + + {step === 3 && ( + + )} + + {step === 4 && ( + + )}
- - {/* Step Indicator */} - - - {/* Step Content */} - {step === 1 && ( - setStep(2)} - /> - )} - - {step === 2 && ( - - )} - - {step === 3 && ( - - )} - - {step === 4 && ( - - )} -
+
); } diff --git a/app/api/admin/dashboard/stats/route.ts b/app/api/admin/dashboard/stats/route.ts index a90d63e..3a3f044 100644 --- a/app/api/admin/dashboard/stats/route.ts +++ b/app/api/admin/dashboard/stats/route.ts @@ -5,6 +5,8 @@ import { getAdminFromRequest } from "@/lib/adminAuth"; import { calcGPA, calcFGPA } from "@/lib/grades"; import { eq, count, desc } from "drizzle-orm"; +export const dynamic = "force-dynamic"; + // ─── GET /api/admin/dashboard/stats ───────────────────────────────────────── export async function GET(request: NextRequest) { @@ -42,24 +44,29 @@ export async function GET(request: NextRequest) { // ── 3. Students at risk (FGPA < 2.00) ─────────────────────────────── // Fetch all results joined with subjects + semesters + // Include isGpa to exclude non-GPA subjects from FGPA calculation const allResults = await db .select({ studentIndex: results.studentIndex, gradePoint: results.gradePoint, creditPoints: subjects.creditPoints, + isGpa: subjects.isGpa, yearNumber: semesters.yearNumber, }) .from(results) .innerJoin(subjects, eq(results.subjectId, subjects.id)) .innerJoin(semesters, eq(subjects.semesterId, semesters.id)); - // Group by student → year → compute FGPA + // Group by student → year → compute FGPA (only GPA subjects) const studentMap = new Map< string, Map >(); for (const row of allResults) { + // Skip non-GPA subjects — they must not affect FGPA + if (!row.isGpa) continue; + if (!studentMap.has(row.studentIndex)) { studentMap.set(row.studentIndex, new Map()); } diff --git a/app/api/admin/results/save/route.ts b/app/api/admin/results/save/route.ts index 8b3ba24..c5cfd7d 100644 --- a/app/api/admin/results/save/route.ts +++ b/app/api/admin/results/save/route.ts @@ -131,7 +131,24 @@ export async function POST(request: NextRequest) { created = uniqueNewStudents.length; } - // ── 3. Batch-fetch existing results for this subject ──────────────── + // ── 3. Log to pdf_uploads table (status: processing) ──────────────── + let pdfUploadId: number | null = null; + try { + const [insertedUpload] = await db + .insert(pdfUploads) + .values({ + filename: uploadMeta?.filename || "unknown.pdf", + adminId: admin.id, + status: "processing", + processedCount: 0, + }) + .returning({ id: pdfUploads.id }); + pdfUploadId = insertedUpload.id; + } catch (logErr) { + console.error("Failed to log PDF upload:", logErr); + } + + // ── 4. Batch-fetch existing results for this subject ──────────────── const existingResultRows = await db .select({ id: results.id, @@ -150,13 +167,14 @@ export async function POST(request: NextRequest) { existingResultRows.map((r) => [r.studentIndex, r]) ); - // ── 4. Process results: batch insert new, update existing ─────────── + // ── 5. Process results: batch insert new, update existing ─────────── const toInsert: { studentIndex: string; subjectId: number; grade: string; gradePoint: string; isRepeat: boolean; + pdfUploadId: number | null; }[] = []; for (const entry of validEntries) { @@ -170,6 +188,7 @@ export async function POST(request: NextRequest) { grade: entry.grade, gradePoint: entry.gradePoint.toFixed(2), isRepeat: false, + pdfUploadId, }); saved++; } else { @@ -177,14 +196,15 @@ export async function POST(request: NextRequest) { const isNewPassing = entry.grade !== "E" && entry.grade !== "AB"; if (isNewPassing) { - // Repeat pass: award C grade (2.00 GP) per university repeat rule + // Repeat pass: award C grade (2.00 GP) per university repeat rule (unless Non-GPA) try { await db .update(results) .set({ grade: entry.grade, - gradePoint: "2.00", + gradePoint: isGpaSubject ? "2.00" : "0.00", isRepeat: true, + pdfUploadId, }) .where(eq(results.id, existing.id)); saved++; @@ -225,17 +245,19 @@ export async function POST(request: NextRequest) { } } - // ── 5. Log to pdf_uploads table ───────────────────────────────────── - try { - await db.insert(pdfUploads).values({ - filename: uploadMeta?.filename || "unknown.pdf", - adminId: admin.id, - status: "completed", - processedCount: saved, - }); - } catch (logErr) { - console.error("Failed to log PDF upload:", logErr); - // Non-fatal — don't fail the whole request + // ── 6. Update status in pdf_uploads table ─────────────────────────── + if (pdfUploadId) { + try { + await db + .update(pdfUploads) + .set({ + status: "completed", + processedCount: saved, + }) + .where(eq(pdfUploads.id, pdfUploadId)); + } catch (updateUploadErr) { + console.error("Failed to update PDF upload status:", updateUploadErr); + } } return NextResponse.json({ diff --git a/app/api/admin/students/[index]/route.ts b/app/api/admin/students/[index]/route.ts index 6714361..33ea55e 100644 --- a/app/api/admin/students/[index]/route.ts +++ b/app/api/admin/students/[index]/route.ts @@ -5,6 +5,8 @@ import { getAdminFromRequest } from "@/lib/adminAuth"; import { calcGPA, calcFGPA, getClass, isPass } from "@/lib/grades"; import { eq } from "drizzle-orm"; +export const dynamic = "force-dynamic"; + // ─── GET /api/admin/students/[index] ──────────────────────────────────────── export async function GET( diff --git a/app/api/admin/students/route.ts b/app/api/admin/students/route.ts index 13f9481..0f1788a 100644 --- a/app/api/admin/students/route.ts +++ b/app/api/admin/students/route.ts @@ -5,6 +5,8 @@ import { getAdminFromRequest } from "@/lib/adminAuth"; import { calcGPA, calcFGPA, getClass, isPass } from "@/lib/grades"; import { eq, ilike, count } from "drizzle-orm"; +export const dynamic = "force-dynamic"; + // ─── Types ────────────────────────────────────────────────────────────────── interface StudentRow { @@ -65,6 +67,7 @@ export async function GET(request: NextRequest) { grade: results.grade, gradePoint: results.gradePoint, creditPoints: subjects.creditPoints, + isGpa: subjects.isGpa, yearNumber: semesters.yearNumber, semesterId: subjects.semesterId, }) @@ -94,6 +97,9 @@ export async function GET(request: NextRequest) { studentData.grades.push({ grade: row.grade }); studentData.semesterIds.add(row.semesterId); + // Only include GPA subjects in the FGPA calculation + if (!row.isGpa) continue; + if (!studentData.byYear.has(row.yearNumber)) { studentData.byYear.set(row.yearNumber, []); } diff --git a/app/api/admin/subjects/route.ts b/app/api/admin/subjects/route.ts index 982b9e4..b4426dc 100644 --- a/app/api/admin/subjects/route.ts +++ b/app/api/admin/subjects/route.ts @@ -4,6 +4,8 @@ import { subjects, semesters } from "@/lib/schema"; import { verifyToken } from "@/lib/auth"; import { eq, and } from "drizzle-orm"; +export const dynamic = "force-dynamic"; + // ─── Helper: Verify Admin JWT ─────────────────────────────────────────────── function getAdminFromRequest(request: NextRequest) { @@ -18,10 +20,7 @@ function getAdminFromRequest(request: NextRequest) { // Returns all subjects joined with semester info. export async function GET(request: NextRequest) { - const admin = getAdminFromRequest(request); - if (!admin) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + // No admin verification needed for public subject lookup try { const rows = await db @@ -69,7 +68,8 @@ export async function POST(request: NextRequest) { isGpa, } = body; - if (!rawCode || !subjectName || !creditPoints || !yearNumber || !semesterNumber) { + // creditPoints can be 0 for Non-GPA subjects — check for null/undefined explicitly + if (!rawCode || !subjectName || creditPoints == null || !yearNumber || !semesterNumber) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 } diff --git a/app/api/admin/uploads/[id]/route.ts b/app/api/admin/uploads/[id]/route.ts new file mode 100644 index 0000000..52a7b81 --- /dev/null +++ b/app/api/admin/uploads/[id]/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { pdfUploads } from "@/lib/schema"; +import { getAdminFromRequest } from "@/lib/adminAuth"; +import { eq } from "drizzle-orm"; + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + const admin = getAdminFromRequest(request); + if (!admin) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const uploadId = parseInt(params.id); + if (isNaN(uploadId)) { + return NextResponse.json({ error: "Invalid upload ID" }, { status: 400 }); + } + + try { + // Check if the upload exists first + const [upload] = await db + .select() + .from(pdfUploads) + .where(eq(pdfUploads.id, uploadId)) + .limit(1); + + if (!upload) { + return NextResponse.json({ error: "Result sheet not found" }, { status: 404 }); + } + + // Delete the upload record. + // This will trigger cascade delete on all results associated with this upload. + await db.delete(pdfUploads).where(eq(pdfUploads.id, uploadId)); + + return NextResponse.json({ success: true, message: "Result sheet and all associated results deleted successfully" }); + } catch (error) { + console.error("DELETE /api/admin/uploads/[id] error:", error); + const message = error instanceof Error ? error.message : "Failed to delete result sheet"; + return NextResponse.json( + { error: `Failed to delete result sheet: ${message}` }, + { status: 500 } + ); + } +} diff --git a/app/api/student/gpa/route.ts b/app/api/student/gpa/route.ts index 8854b75..3b74210 100644 --- a/app/api/student/gpa/route.ts +++ b/app/api/student/gpa/route.ts @@ -5,6 +5,8 @@ import { results, subjects, semesters } from "@/lib/schema"; import { getStudentFromRequest } from "@/lib/studentAuth"; import { calcGPA, calcFGPA, getClass, isPass } from "@/lib/grades"; +export const dynamic = "force-dynamic"; + // ─── Types ────────────────────────────────────────────────────────────────── interface SubjectResult { diff --git a/app/student/change-password/page.tsx b/app/student/change-password/page.tsx index bd5cff3..aa84b05 100644 --- a/app/student/change-password/page.tsx +++ b/app/student/change-password/page.tsx @@ -70,7 +70,7 @@ export default function ChangePasswordPage() { useEffect(() => { async function checkFirstLogin() { try { - const res = await fetch("/api/student/gpa"); + const res = await fetch("/api/student/gpa", { cache: "no-store" }); if (res.ok) { // We don't need the full data; we just need to know we're authed // Check via a dedicated lightweight check if available, else default diff --git a/app/student/dashboard/page.tsx b/app/student/dashboard/page.tsx index 2225145..685aa71 100644 --- a/app/student/dashboard/page.tsx +++ b/app/student/dashboard/page.tsx @@ -234,7 +234,9 @@ export default function StudentDashboardPage() { setLoading(true); setError(null); try { - const res = await fetch("/api/student/gpa"); + const res = await fetch("/api/student/gpa", { + cache: "no-store", + }); if (!res.ok) { if (res.status === 401) { router.push("/student/login"); @@ -361,6 +363,24 @@ export default function StudentDashboardPage() { ) : data ? ( <> + {/* ── Page Header ────────────────────────────────────────── */} +
+
+

Student Dashboard

+

Your academic performance overview

+
+ +
+ {/* ── Hero GPA Card ──────────────────────────────────────── */} {hasResults ? ( setMobileOpen(false)} @@ -113,7 +112,7 @@ export function AdminSidebar() { {active && (
)} - + ); })} diff --git a/lib/grades.ts b/lib/grades.ts index 247c732..955569b 100644 --- a/lib/grades.ts +++ b/lib/grades.ts @@ -51,16 +51,25 @@ export function calcGPA( /** * Calculate Final GPA (FGPA) from yearly GPAs. - * FGPA = Y1×0.20 + Y2×0.20 + Y3×0.30 + Y4×0.30 + * FGPA = (Y1×0.20 + Y2×0.20 + Y3×0.30 + Y4×0.30) / (sum of weights of completed years) */ export function calcFGPA( yearGPAs: { year: number; gpa: number }[] ): number { - const fgpa = yearGPAs.reduce((sum, { year, gpa }) => { + if (yearGPAs.length === 0) return 0; + + let totalWeight = 0; + let weightedSum = 0; + + for (const { year, gpa } of yearGPAs) { const weight = YEAR_WEIGHTS[year] ?? 0; - return sum + gpa * weight; - }, 0); + weightedSum += gpa * weight; + totalWeight += weight; + } + + if (totalWeight === 0) return 0; + const fgpa = weightedSum / totalWeight; return Math.round(fgpa * 100) / 100; } diff --git a/lib/parsePDF.ts b/lib/parsePDF.ts index b02988c..f392612 100644 --- a/lib/parsePDF.ts +++ b/lib/parsePDF.ts @@ -24,8 +24,9 @@ const INDEX_GRADE_REGEX = // ─── Subject Code + Name from Header ──────────────────────────────────────── // Matches: "Code and Title of Paper : IS 2106 System Analysis & Design" +// or "Code and Title of Paper : IS-EBP-3101 Business English" const SUBJECT_HEADER_REGEX = - /(?:Code\s+(?:and|&)\s+Title\s+of\s+Paper|Paper\s+Code\s*(?:and|&)\s*Title|Code\s*[-–—]\s*Title)\s*[:\-–—]?\s*([A-Z]{2,4}\s*\d{3,4})\s+([^\n\r]+)/i; + /(?:Code\s+(?:and|&)\s+Title\s+of\s+Paper|Paper\s+Code\s*(?:and|&)\s*Title|Code\s*[-–—]\s*Title)\s*[:\-–—]?\s*([A-Z]{2,4}(?:-[A-Z]+)?\s*[-–—]?\s*\d{3,4})\s+([^\n\r]+)/i; // ─── Semester from Header ─────────────────────────────────────────────────── // Matches: "Semester II(Proper -CIS)" or "Semster I(Repeat - FIS)" @@ -95,9 +96,9 @@ export function parseOCRText(rawText: string): ParsedSheet { subjectNameFromHeader = subjectMatch[2].trim(); } else { // Fallback: search for paper code alone if full header match fails - const codeMatch = normalizedText.match(/\b([A-Z]{2,4})\s*(\d{3,4})\b/i); + const codeMatch = normalizedText.match(/\b([A-Z]{2,4}(?:-[A-Z]+)?\s*[-–—]?\s*\d{3,4})\b/i); if (codeMatch) { - subjectCodeFromHeader = `${codeMatch[1]}${codeMatch[2]}`.toUpperCase(); + subjectCodeFromHeader = codeMatch[1].replace(/\s+/g, "").toUpperCase(); } } diff --git a/lib/schema.ts b/lib/schema.ts index 16f452f..941ba07 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -79,6 +79,8 @@ export const results = pgTable( grade: varchar("grade", { length: 5 }).notNull(), gradePoint: decimal("grade_point", { precision: 3, scale: 2 }).notNull(), isRepeat: boolean("is_repeat").notNull().default(false), + pdfUploadId: integer("pdf_upload_id") + .references(() => pdfUploads.id, { onDelete: "cascade" }), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), diff --git a/package.json b/package.json index 17954c7..8704670 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", - "db:seed": "npx tsx db/seed.ts" + "seed:subjects": "tsx scripts/seedSubjects.ts", + "db:fix": "tsx scripts/fixSemesterData.ts", + "db:dedup": "tsx scripts/deduplicateSubjects.ts", + "db:fixgpa": "tsx scripts/fixIsGpa.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", diff --git a/scripts/deduplicateSubjects.ts b/scripts/deduplicateSubjects.ts new file mode 100644 index 0000000..9891967 --- /dev/null +++ b/scripts/deduplicateSubjects.ts @@ -0,0 +1,70 @@ +/** + * deduplicateSubjects.ts + * + * The DB has duplicate subject rows (same subjectCode, multiple IDs). + * This script keeps the lowest-ID row for each code and deletes the rest. + * It first re-parents any results rows pointing to duplicate IDs, + * then deletes the duplicates. + */ + +import "dotenv/config"; +import { db } from "@/lib/db"; +import { subjects, results } from "@/lib/schema"; +import { eq, inArray } from "drizzle-orm"; + +async function dedup() { + console.log("🔍 Scanning for duplicate subjects…\n"); + + const allSubjects = await db.select().from(subjects); + + // Group by subjectCode + const byCode = new Map(); + for (const sub of allSubjects) { + const group = byCode.get(sub.subjectCode) ?? []; + group.push(sub); + byCode.set(sub.subjectCode, group); + } + + let totalDuplicates = 0; + + for (const [code, group] of Array.from(byCode.entries())) { + if (group.length <= 1) continue; + + // Keep the row with the lowest id (original seed entry) + group.sort((a, b) => a.id - b.id); + const [keep, ...duplicates] = group; + const dupIds = duplicates.map((d) => d.id); + + console.log(`📌 ${code}: keeping id=${keep.id}, removing ids=[${dupIds.join(", ")}]`); + + // Re-parent any results that point to a duplicate subject id → point to keeper + for (const dupId of dupIds) { + const affected = await db + .update(results) + .set({ subjectId: keep.id }) + .where(eq(results.subjectId, dupId)); + // @ts-ignore rowCount may not be typed + const count = (affected as any)?.rowCount ?? "?"; + if (count !== "?" && Number(count) > 0) { + console.log(` ↳ Re-parented ${count} result row(s) from id=${dupId} → id=${keep.id}`); + } + } + + // Delete duplicates + await db.delete(subjects).where(inArray(subjects.id, dupIds)); + totalDuplicates += dupIds.length; + } + + if (totalDuplicates === 0) { + console.log("✅ No duplicates found — DB is clean."); + } else { + console.log(`\n✅ Removed ${totalDuplicates} duplicate subject row(s).`); + } + + process.exit(0); +} + +dedup().catch((err) => { + console.error("Dedup error:", err); + process.exit(1); +}); diff --git a/scripts/fixIsGpa.ts b/scripts/fixIsGpa.ts new file mode 100644 index 0000000..e6eb05e --- /dev/null +++ b/scripts/fixIsGpa.ts @@ -0,0 +1,49 @@ +/** + * fixIsGpa.ts + * + * Any subject with creditPoints = 0 must be Non-GPA (isGpa = false). + * This script finds all such subjects and corrects them. + */ + +import "dotenv/config"; +import { db } from "@/lib/db"; +import { subjects } from "@/lib/schema"; +import { eq } from "drizzle-orm"; + +async function fix() { + console.log("🔍 Checking for 0-credit subjects incorrectly marked as GPA…\n"); + + const allSubjects = await db.select().from(subjects); + + let fixed = 0; + let alreadyCorrect = 0; + + for (const sub of allSubjects) { + if (sub.creditPoints === 0) { + if (sub.isGpa) { + await db + .update(subjects) + .set({ isGpa: false }) + .where(eq(subjects.id, sub.id)); + console.log(`✅ Fixed ${sub.subjectCode}: creditPoints=0 → isGpa set to false`); + fixed++; + } else { + console.log(`☑️ ${sub.subjectCode}: already isGpa=false (correct)`); + alreadyCorrect++; + } + } + } + + console.log(`\n📊 Summary: ${fixed} fixed, ${alreadyCorrect} already correct.`); + + if (fixed === 0 && alreadyCorrect === 0) { + console.log("ℹ️ No 0-credit subjects found in DB."); + } + + process.exit(0); +} + +fix().catch((err) => { + console.error("Fix error:", err); + process.exit(1); +}); diff --git a/scripts/fixSemesterData.ts b/scripts/fixSemesterData.ts new file mode 100644 index 0000000..f9583b9 --- /dev/null +++ b/scripts/fixSemesterData.ts @@ -0,0 +1,116 @@ +/** + * fixSemesterData.ts + * + * The original seed used yearNumber=1-8 (treating each absolute semester as a "year") + * and semesterNumber=1 for everything. + * + * The CORRECT DB structure is: + * yearNumber 1-4 + * semesterNumber 1-2 (relative within the year) + * + * Absolute semester → correct (yearNumber, semesterNumber): + * 1 → (1,1) 2 → (1,2) + * 3 → (2,1) 4 → (2,2) + * 5 → (3,1) 6 → (3,2) + * 7 → (4,1) 8 → (4,2) + * + * This script: + * 1. Works out the correct semester for every subject from its subject code. + * 2. Ensures the correct semester row exists. + * 3. Updates each subject's semesterId. + * 4. Removes orphaned (now-unused) semester rows. + */ + +import "dotenv/config"; +import { db } from "@/lib/db"; +import { subjects, semesters } from "@/lib/schema"; +import { eq, and, notInArray } from "drizzle-orm"; + +// ─── helpers ───────────────────────────────────────────────────────────────── + +function getCorrectSemester( + code: string +): { yearNumber: number; semesterNumber: number; label: string } | null { + // Special English-pathway codes: IS-EXX-YYZZ + // YY encodes decade = year (1-4), unit = semester within year (1-2) + const specialMatch = code.match(/^IS-[A-Z]+-(\d)(\d)/); + if (specialMatch) { + const yearNumber = parseInt(specialMatch[1]); + const semesterNumber = parseInt(specialMatch[2]); + if (yearNumber >= 1 && yearNumber <= 4 && semesterNumber >= 1 && semesterNumber <= 2) { + return { yearNumber, semesterNumber, label: `Year ${yearNumber} - Semester ${semesterNumber}` }; + } + } + + // Regular codes: IS{absSem}xxx e.g. IS3101 → absSem=3 + const regularMatch = code.match(/^IS(\d)/); + if (regularMatch) { + const absSem = parseInt(regularMatch[1]); // 1-8 + if (absSem < 1 || absSem > 8) return null; + const yearNumber = Math.ceil(absSem / 2); // 1→1, 2→1, 3→2, 4→2, 5→3, 6→3, 7→4, 8→4 + const semesterNumber = absSem % 2 === 0 ? 2 : 1; // even→2, odd→1 + return { yearNumber, semesterNumber, label: `Year ${yearNumber} - Semester ${semesterNumber}` }; + } + + return null; +} + +async function ensureSemester(yearNumber: number, semesterNumber: number, label: string): Promise { + const existing = await db + .select() + .from(semesters) + .where(and(eq(semesters.yearNumber, yearNumber), eq(semesters.semesterNumber, semesterNumber))) + .limit(1); + + if (existing.length > 0) return existing[0].id; + + const [inserted] = await db + .insert(semesters) + .values({ yearNumber, semesterNumber, label }) + .returning(); + return inserted.id; +} + +// ─── main ──────────────────────────────────────────────────────────────────── + +async function fix() { + console.log("🔧 Fixing semester data…\n"); + + const allSubjects = await db.select().from(subjects); + const usedSemesterIds = new Set(); + + for (const sub of allSubjects) { + const correct = getCorrectSemester(sub.subjectCode); + if (!correct) { + console.warn(`⚠️ Cannot determine semester for ${sub.subjectCode} — skipping`); + continue; + } + + const correctSemId = await ensureSemester(correct.yearNumber, correct.semesterNumber, correct.label); + usedSemesterIds.add(correctSemId); + + if (sub.semesterId !== correctSemId) { + await db.update(subjects).set({ semesterId: correctSemId }).where(eq(subjects.id, sub.id)); + console.log(`✅ ${sub.subjectCode}: semester → Year ${correct.yearNumber}, Sem ${correct.semesterNumber}`); + } else { + console.log(`☑️ ${sub.subjectCode}: already correct`); + } + } + + // Remove orphaned semester rows that no subject uses + const allSems = await db.select().from(semesters); + for (const sem of allSems) { + if (!usedSemesterIds.has(sem.id)) { + await db.delete(semesters).where(eq(semesters.id, sem.id)); + console.log(`🗑️ Removed orphan semester (id=${sem.id}, year=${sem.yearNumber}, sem=${sem.semesterNumber})`); + } + } + + console.log("\n✅ Fix complete!"); + process.exit(0); +} + +fix().catch((err) => { + console.error("Fix error:", err); + process.exit(1); +}); diff --git a/scripts/seedSubjects.ts b/scripts/seedSubjects.ts new file mode 100644 index 0000000..fe57153 --- /dev/null +++ b/scripts/seedSubjects.ts @@ -0,0 +1,169 @@ +import "dotenv/config"; +import { db } from "@/lib/db"; +import { subjects, semesters } from "@/lib/schema"; +import { eq, and } from "drizzle-orm"; + +/** + * Subject data with CORRECT year/semester mapping: + * + * Absolute Sem 1 (Year 1, Sem 1) → IS1xxx (Semester I) + * Absolute Sem 2 (Year 1, Sem 2) → IS2xxx (Semester II) + * Absolute Sem 3 (Year 2, Sem 1) → IS3xxx (Semester III) + * Absolute Sem 4 (Year 2, Sem 2) → IS4xxx (Semester IV) + * Absolute Sem 5 (Year 3, Sem 1) → IS5xxx (Semester V) + * Absolute Sem 6 (Year 3, Sem 2) → IS6xxx (Semester VI) + * Absolute Sem 7 (Year 4, Sem 1) → IS7xxx (Semester VII) + * Absolute Sem 8 (Year 4, Sem 2) → IS8xxx (Semester VIII) + */ +const subjectData = [ + // ── Semester I (Year 1, Sem 1) ────────────────────────────────────────── + { subjectCode: "IS1101", subjectName: "Fundamentals of Information Systems", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1102", subjectName: "Structured Programming Techniques", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1103", subjectName: "Structured Programming Practicum", creditPoints: 1, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1104", subjectName: "Theories of Information Systems", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1105", subjectName: "Computer System Organization", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1106", subjectName: "Foundations of Web Technologies", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1107", subjectName: "Personal Productivity with Information Technology", creditPoints: 1, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1108", subjectName: "Fundamentals of Mathematics", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1109", subjectName: "Statistics & Probability Theory", creditPoints: 2, yearNumber: 1, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS1110", subjectName: "Communication Skills I", creditPoints: 0, yearNumber: 1, semesterNumber: 1, isGpa: false }, + { subjectCode: "IS1111", subjectName: "Academic Integrity", creditPoints: 0, yearNumber: 1, semesterNumber: 1, isGpa: false }, + { subjectCode: "IS-EGP-1101", subjectName: "General English I", creditPoints: 0, yearNumber: 1, semesterNumber: 1, isGpa: false }, + + // ── Semester II (Year 1, Sem 2) ───────────────────────────────────────── + { subjectCode: "IS2101", subjectName: "Object Oriented Programming", creditPoints: 2, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2102", subjectName: "Object Oriented Programming Practicum", creditPoints: 1, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2103", subjectName: "Emerging IS Technologies", creditPoints: 1, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2104", subjectName: "Database Systems", creditPoints: 2, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2105", subjectName: "Database Management Systems Practicum", creditPoints: 1, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2106", subjectName: "System Analysis & Design", creditPoints: 1, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2107", subjectName: "Social & Professional Issues", creditPoints: 1, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2108", subjectName: "Human Computer Interaction", creditPoints: 2, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2109", subjectName: "Information Assurance & Security", creditPoints: 2, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2110", subjectName: "Software Project Initiation & Planning", creditPoints: 1, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2111", subjectName: "Advanced Mathematics", creditPoints: 2, yearNumber: 1, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS2112", subjectName: "Communication Skills II", creditPoints: 0, yearNumber: 1, semesterNumber: 2, isGpa: false }, + { subjectCode: "IS-EGP-1201", subjectName: "General English II", creditPoints: 0, yearNumber: 1, semesterNumber: 2, isGpa: false }, + + // ── Semester III (Year 2, Sem 1) ──────────────────────────────────────── + { subjectCode: "IS3101", subjectName: "Object Oriented Analysis & Design", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3102", subjectName: "Data Structures & Algorithms", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3103", subjectName: "IT Governance", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3104", subjectName: "Software Engineering", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3105", subjectName: "IS Risk Management", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3106", subjectName: "IS Sustainability", creditPoints: 1, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3107", subjectName: "Management Information Systems", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3108", subjectName: "E-Business", creditPoints: 1, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS3109", subjectName: "Digital Innovation", creditPoints: 2, yearNumber: 2, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS-EAP-2101", subjectName: "Academic English I", creditPoints: 0, yearNumber: 2, semesterNumber: 1, isGpa: false }, + + // ── Semester IV (Year 2, Sem 2) ───────────────────────────────────────── + { subjectCode: "IS4101", subjectName: "IT Auditing", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4102", subjectName: "Web Application Development", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4103", subjectName: "Operating Systems", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4104", subjectName: "System Administration and Maintenance", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4105", subjectName: "IT Procurement Management", creditPoints: 1, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4106", subjectName: "Software Architecture", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4107", subjectName: "Professionalism & Ethics in Computing", creditPoints: 1, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4108", subjectName: "IS Strategies", creditPoints: 1, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4109", subjectName: "Agile Software Development", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS4110", subjectName: "Capstone Project", creditPoints: 2, yearNumber: 2, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS-EAP-2201", subjectName: "Academic English II", creditPoints: 0, yearNumber: 2, semesterNumber: 2, isGpa: false }, + + // ── Semester V (Year 3, Sem 1) ────────────────────────────────────────── + { subjectCode: "IS5101", subjectName: "Entrepreneurship & Innovation", creditPoints: 1, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5102", subjectName: "Enterprise Architecture", creditPoints: 1, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5103", subjectName: "High Performance Computing", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5104", subjectName: "Software Process Management", creditPoints: 1, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5105", subjectName: "Business Process Management", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5106", subjectName: "UI/UX Practicum", creditPoints: 1, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5107", subjectName: "Project Management Practicum", creditPoints: 1, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5108", subjectName: "Business Intelligence", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5109", subjectName: "IS Project for Community", creditPoints: 1, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5110", subjectName: "Advanced Database Systems", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5111", subjectName: "Data Communication & Networks", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5112", subjectName: "Design Patterns & Anti-patterns", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5113", subjectName: "Software Quality Assurance", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS5114", subjectName: "Data Mining & Analytics", creditPoints: 2, yearNumber: 3, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS-EBP-3101", subjectName: "Business English", creditPoints: 0, yearNumber: 3, semesterNumber: 1, isGpa: false }, + + // ── Semester VI (Year 3, Sem 2) ───────────────────────────────────────── + { subjectCode: "IS6101", subjectName: "Professional Practice", creditPoints: 6, yearNumber: 3, semesterNumber: 2, isGpa: true }, + + // ── Semester VII (Year 4, Sem 1) ──────────────────────────────────────── + { subjectCode: "IS7101", subjectName: "Research Methodologies", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7102", subjectName: "IT Law", creditPoints: 1, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7103", subjectName: "Business Process Simulation", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7104", subjectName: "Enterprise Modelling Ontologies", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7105", subjectName: "Organizational Behavior & Management", creditPoints: 1, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7106", subjectName: "Cloud Computing", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7107", subjectName: "Mobile Application Development", creditPoints: 1, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7108", subjectName: "Web Service Technologies", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7109", subjectName: "Geographical Information Systems", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7110", subjectName: "Statistical Distribution & Inferences", creditPoints: 1, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7111", subjectName: "Advanced Programming Practicum", creditPoints: 1, yearNumber: 4, semesterNumber: 1, isGpa: true }, + { subjectCode: "IS7112", subjectName: "Machine Learning", creditPoints: 2, yearNumber: 4, semesterNumber: 1, isGpa: true }, + + // ── Semester VIII (Year 4, Sem 2) ─────────────────────────────────────── + { subjectCode: "IS8101", subjectName: "Research Project in IS", creditPoints: 8, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8102", subjectName: "Business/IT Alignment", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8103", subjectName: "Human Resource Management", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8104", subjectName: "Scientific Communication", creditPoints: 1, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8105", subjectName: "IS Economics", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8106", subjectName: "Computer System Security", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8107", subjectName: "Supply Chain Management", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8108", subjectName: "Advanced Computer Networks", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8109", subjectName: "Process Mining", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8110", subjectName: "Digital Business Model", creditPoints: 1, yearNumber: 4, semesterNumber: 2, isGpa: true }, + { subjectCode: "IS8111", subjectName: "Game Development", creditPoints: 2, yearNumber: 4, semesterNumber: 2, isGpa: true }, +]; + +async function ensureSemester(yearNumber: number, semesterNumber: number) { + const label = `Year ${yearNumber} - Semester ${semesterNumber}`; + try { + const [row] = await db + .insert(semesters) + .values({ yearNumber, semesterNumber, label }) + .returning({ id: semesters.id }); + return row.id; + } catch { + const existing = await db + .select() + .from(semesters) + .where(and(eq(semesters.yearNumber, yearNumber), eq(semesters.semesterNumber, semesterNumber))) + .limit(1); + if (existing.length > 0) return existing[0].id; + throw new Error(`Could not ensure semester Year ${yearNumber} Sem ${semesterNumber}`); + } +} + +async function seed() { + console.log("Seeding subjects with correct year/semester mapping…"); + for (const sub of subjectData) { + const semesterId = await ensureSemester(sub.yearNumber, sub.semesterNumber); + const existing = await db + .select() + .from(subjects) + .where(eq(subjects.subjectCode, sub.subjectCode)) + .limit(1); + if (existing.length > 0) { + console.log(`Skip (exists): ${sub.subjectCode}`); + continue; + } + await db.insert(subjects).values({ + subjectCode: sub.subjectCode, + subjectName: sub.subjectName, + creditPoints: sub.creditPoints, + isGpa: sub.isGpa, + semesterId, + }); + console.log(`Inserted: ${sub.subjectCode}`); + } + console.log("Seeding complete."); + process.exit(0); +} + +seed().catch((err) => { + console.error("Seed error:", err); + process.exit(1); +});
- Filename - - Status - - Records - - Date - FilenameStatusRecordsDateActions
- {upload.filename} - - - - {upload.processedCount} -
{upload.filename}{upload.processedCount} - {new Date(upload.createdAt).toLocaleDateString( - "en-US", - { - month: "short", - day: "numeric", - year: "numeric", - } - )} + {new Date(upload.createdAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} + +