From b843b960a8ec010dce63a774c924183b6481c9c9 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 12:58:47 +0530 Subject: [PATCH 01/80] Rework at-risk page layout --- .../subject-analysis/[subjectId]/overall.tsx | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/pages/subject-analysis/[subjectId]/overall.tsx b/pages/subject-analysis/[subjectId]/overall.tsx index 48ebeb7..88c5fd2 100644 --- a/pages/subject-analysis/[subjectId]/overall.tsx +++ b/pages/subject-analysis/[subjectId]/overall.tsx @@ -136,7 +136,18 @@ export default function SubjectOverallPage() { if (sortDirection === "none") return rows.sort((a, b) => a.originalIndex - b.originalIndex); return rows.sort((a: any, b: any) => ((sortDirection === "asc" ? a[sortColumn] - b[sortColumn] : b[sortColumn] - a[sortColumn]) || a.name.localeCompare(b.name))); }, [baseRows, sortColumn, sortDirection]); - const filteredRiskRows = useMemo(() => { const lo = Math.min(riskLower, riskUpper); const hi = Math.max(riskLower, riskUpper); return baseRows.filter(row => { const value = columnValue(row, riskColumn); return value >= lo && value <= hi; }); }, [baseRows, riskColumn, riskLower, riskUpper]); + + const filteredRiskRows = useMemo(() => { + const lo = Math.min(riskLower, riskUpper); + const hi = Math.max(riskLower, riskUpper); + const rows = baseRows.filter(row => { + const value = columnValue(row, riskColumn); + return value >= lo && value <= hi; + }); + if (sortDirection === "none") return rows.sort((a, b) => a.originalIndex - b.originalIndex); + return rows.sort((a: any, b: any) => ((sortDirection === "asc" ? a[riskColumn] - b[riskColumn] : b[riskColumn] - a[riskColumn]) || a.name.localeCompare(b.name))); + }, [baseRows, riskColumn, riskLower, riskUpper, sortDirection]); + const topFive = useMemo(() => [...filteredRiskRows].sort((a, b) => columnValue(b, riskColumn) - columnValue(a, riskColumn)).slice(0, 5), [filteredRiskRows, riskColumn]); const riskDistribution = useMemo(() => [ { name: "Above 32", value: baseRows.filter(r => r.basic > 32).length }, @@ -171,13 +182,6 @@ export default function SubjectOverallPage() { return next; }); } - function selectRiskRange(index: number) { - setRiskColumn("basic"); - if (index === 0) { setRiskLower(32.1); setRiskUpper(40); } - if (index === 1) { setRiskLower(24); setRiskUpper(32); } - if (index === 2) { setRiskLower(16); setRiskUpper(23.9); } - if (index === 3) { setRiskLower(0); setRiskUpper(15.9); } - } return

Internal Marks Data

Basic marks use the selected components. Moderated marks are calculated from the configured rank criteria.

{HEADERS.map(h => )}{sortedRows.map((row,index) => )}
#NameEnrollment{h.label}
{index+1}{row.name}{row.enrollmentNo}{row.assignment}{row.presentation}{row.attendance}{row.moderatedAttendanceWeighted}{row.midsem1}{row.midsem2}{row.basic}{row.moderated}
: data && <> -

At-Risk Filters

Filter students by any calculated internal-mark value.

{riskDistribution.map((item,index)=>)}

Distribution

{riskDistribution.map((_,i)=>)}
-

Filtered Students

Showing {filteredRiskRows.length} of {baseRows.length} students.

{filteredRiskRows.length} Students
{filteredRiskRows.map(row=>)}
EnrollmentStudentMarksModeratedRankTierStatus
{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{row.rank}{criteria.findIndex(c=>row.rank>=c.from&&row.rank<=c.to)+1||"—"}{row.basic<16?"Critical Risk":row.basic<24?"Needs Attention":"Good"}
-

Top Students in Filter

{topFive.map(row=>
{row.name}
{columnValue(row,riskColumn)}
)}
+
+
+
+

At-Risk Filters

Choose a mark column, range and sort.

+
+ + +
+
+
+ +
+

Filtered Students

Showing {filteredRiskRows.length} of {baseRows.length} students.

{filteredRiskRows.length} Students
+
{filteredRiskRows.map(row=>)}
EnrollmentStudentMarksModeratedRankTierStatus
{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{row.rank}{criteria.findIndex(c=>row.rank>=c.from&&row.rank<=c.to)+1||"—"}{row.basic<16?"Critical Risk":row.basic<24?"Needs Attention":"Good"}
+
+ + +
} ; From b7559f6566a66a7ffcac693ea0e44930abdf1e3b Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 12:59:13 +0530 Subject: [PATCH 02/80] Add at-risk page layout styling --- app/globals.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/globals.css b/app/globals.css index 12cbbb5..569ac28 100644 --- a/app/globals.css +++ b/app/globals.css @@ -34,5 +34,10 @@ .academic-content-grid{align-items:start}.academic-table-wrap{height:620px;max-height:620px}.academic-stats-panel{padding:16px;display:grid;grid-template-columns:1fr 1.4fr;gap:14px}.academic-highest{padding:12px 14px;border:1px solid #edf0f4;border-radius:12px;background:rgba(255,255,255,.58);display:flex;flex-direction:column;gap:5px}.academic-highest span,.academic-tier-card span{font-size:11px;color:#667085}.academic-highest strong{font-size:26px;line-height:1;color:#172033}.academic-highest p{margin:0;color:#475467;font-size:12px;line-height:1.35}.academic-tier-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.academic-tier-card{min-height:70px;padding:10px 12px;border:1px solid;border-radius:12px;background:rgba(255,255,255,.52);display:flex;flex-direction:column;justify-content:space-between}.academic-tier-card strong{font-size:21px;line-height:1}.academic-rank-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.academic-rank-panel{padding:16px;min-width:0}.academic-rank-panel h3{font-size:14px;font-weight:700;margin-bottom:10px}.academic-rank-row{display:grid;grid-template-columns:24px minmax(0,1fr) 32px;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid #edf0f4}.academic-rank-row:last-child{border-bottom:0}.academic-rank-row>span{font-size:11px;color:#98a2b3}.academic-rank-row p{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;color:#475467}.academic-rank-row strong{justify-self:end;font-size:13px;color:#172033}.academic-rank-row strong.is-negative{color:#dc2626}.academic-table-panel .analysis-table td:first-child{min-width:210px}.academic-table-panel .analysis-table th:not(:first-child),.academic-table-panel .analysis-table td:not(:first-child){text-align:center}.academic-table-panel .student-cell{white-space:nowrap}.academic-content-grid .analysis-chart{height:190px} .recharts-tooltip-wrapper .recharts-default-tooltip{border-radius:12px!important;border:1px solid #e2e8f0!important;box-shadow:0 12px 28px rgba(15,23,42,.1)!important;background:rgba(255,255,255,.97)!important}.analysis-stat-card,.analysis-ranked-list,.analysis-grade-badge{border-color:#e2e8f0} -@media(max-width:1100px){.analysis-hero{grid-template-columns:1fr 1fr}.analysis-hero-copy{grid-column:1/-1}.analysis-content-grid{grid-template-columns:1fr}.analysis-settings-grid{grid-template-columns:1fr 1fr}.analysis-sidebar{position:fixed;transform:translateX(-100%)}.analysis-layout{grid-template-columns:1fr}.analysis-page{padding:18px}.academic-table-wrap{height:500px;max-height:500px}.analysis-top-actions>.analysis-raw-button{position:static;width:auto;min-height:0;border:1px solid #dfe5ec;background:rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(15,23,42,.04)}.analysis-top-actions>.analysis-raw-button span:first-child:before{display:none}} -@media(max-width:700px){.analysis-topbar{align-items:flex-start;flex-direction:column}.analysis-title-row h1{font-size:21px}.analysis-top-actions{width:100%}.analysis-settings-grid{grid-template-columns:1fr}.analysis-hero{grid-template-columns:1fr}.analysis-content-grid{grid-template-columns:1fr}.analysis-page{padding:14px}.analysis-insight{align-items:flex-start;flex-direction:column}.analysis-insight-actions{margin-left:0;flex-wrap:wrap}.subject-analysis-nav__item{padding:9px 12px}.analysis-chart{height:200px}.academic-stats-panel,.academic-rank-grid{grid-template-columns:1fr}.academic-tier-grid{grid-template-columns:1fr 1fr}.academic-table-wrap{height:440px;max-height:440px}} + +/* Overall > At Risk */ +.at-risk-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;grid-template-areas:"filter filter" "table side";gap:12px;align-items:start}.at-risk-filter{grid-area:filter}.at-risk-table{grid-area:table;min-width:0}.at-risk-side-stack{grid-area:side;display:grid;gap:12px;min-width:0}.at-risk-side-stack>section:first-child{min-height:250px}.at-risk-filter select,.at-risk-filter input{height:38px}.at-risk-filter label{margin:0}.at-risk-filter .flex-1{min-width:210px} + +@media(max-width:1100px){.analysis-hero{grid-template-columns:1fr 1fr}.analysis-hero-copy{grid-column:1/-1}.analysis-content-grid{grid-template-columns:1fr}.analysis-settings-grid{grid-template-columns:1fr 1fr}.analysis-sidebar{position:fixed;transform:translateX(-100%)}.analysis-layout{grid-template-columns:1fr}.analysis-page{padding:18px}.academic-table-wrap{height:500px;max-height:500px}.analysis-top-actions>.analysis-raw-button{position:static;width:auto;min-height:0;border:1px solid #dfe5ec;background:rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(15,23,42,.04)}.analysis-top-actions>.analysis-raw-button span:first-child:before{display:none}.at-risk-layout{grid-template-columns:minmax(0,1fr) 280px}} +@media(max-width:800px){.at-risk-layout{grid-template-columns:1fr;grid-template-areas:"filter" "table" "side"}.at-risk-filter .flex{align-items:stretch}.at-risk-filter .flex-1,.at-risk-filter label,.at-risk-filter>div>div:last-child{width:100%;min-width:0}.at-risk-side-stack{grid-template-columns:1fr 1fr}.at-risk-side-stack>section:first-child{min-height:0}} +@media(max-width:700px){.analysis-topbar{align-items:flex-start;flex-direction:column}.analysis-title-row h1{font-size:21px}.analysis-top-actions{width:100%}.analysis-settings-grid{grid-template-columns:1fr}.analysis-hero{grid-template-columns:1fr}.analysis-content-grid{grid-template-columns:1fr}.analysis-page{padding:14px}.analysis-insight{align-items:flex-start;flex-direction:column}.analysis-insight-actions{margin-left:0;flex-wrap:wrap}.subject-analysis-nav__item{padding:9px 12px}.analysis-chart{height:200px}.academic-stats-panel,.academic-rank-grid{grid-template-columns:1fr}.academic-tier-grid{grid-template-columns:1fr 1fr}.academic-table-wrap{height:440px;max-height:440px}.at-risk-side-stack{grid-template-columns:1fr}.at-risk-filter .flex{flex-direction:column}} From d68afee93de9e8a33cec70bd344fe33dc4d99276 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 13:32:35 +0530 Subject: [PATCH 03/80] Polish At Risk layout and controls --- .../subject-analysis/[subjectId]/overall.tsx | 86 ++++--------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/pages/subject-analysis/[subjectId]/overall.tsx b/pages/subject-analysis/[subjectId]/overall.tsx index 88c5fd2..f582754 100644 --- a/pages/subject-analysis/[subjectId]/overall.tsx +++ b/pages/subject-analysis/[subjectId]/overall.tsx @@ -103,7 +103,6 @@ export default function SubjectOverallPage() { useEffect(() => { loadAnalysis(); }, [subjectId]); const selectedTotal = useMemo(() => round1(draftBasicColumns.reduce((sum, key) => sum + draftWeights[key], 0)), [draftBasicColumns, draftWeights]); - const baseRows = useMemo(() => { const preliminary = (data?.students || []).map((s: any, originalIndex: number) => ({ enrollmentNo: s.enrollmentNo, name: s.name, originalIndex, @@ -136,18 +135,12 @@ export default function SubjectOverallPage() { if (sortDirection === "none") return rows.sort((a, b) => a.originalIndex - b.originalIndex); return rows.sort((a: any, b: any) => ((sortDirection === "asc" ? a[sortColumn] - b[sortColumn] : b[sortColumn] - a[sortColumn]) || a.name.localeCompare(b.name))); }, [baseRows, sortColumn, sortDirection]); - const filteredRiskRows = useMemo(() => { - const lo = Math.min(riskLower, riskUpper); - const hi = Math.max(riskLower, riskUpper); - const rows = baseRows.filter(row => { - const value = columnValue(row, riskColumn); - return value >= lo && value <= hi; - }); + const lo = Math.min(riskLower, riskUpper); const hi = Math.max(riskLower, riskUpper); + const rows = baseRows.filter(row => { const value = columnValue(row, riskColumn); return value >= lo && value <= hi; }); if (sortDirection === "none") return rows.sort((a, b) => a.originalIndex - b.originalIndex); return rows.sort((a: any, b: any) => ((sortDirection === "asc" ? a[riskColumn] - b[riskColumn] : b[riskColumn] - a[riskColumn]) || a.name.localeCompare(b.name))); }, [baseRows, riskColumn, riskLower, riskUpper, sortDirection]); - const topFive = useMemo(() => [...filteredRiskRows].sort((a, b) => columnValue(b, riskColumn) - columnValue(a, riskColumn)).slice(0, 5), [filteredRiskRows, riskColumn]); const riskDistribution = useMemo(() => [ { name: "Above 32", value: baseRows.filter(r => r.basic > 32).length }, @@ -157,89 +150,46 @@ export default function SubjectOverallPage() { ], [baseRows]); function updateWeight(key: WeightKey, value: string) { setDraftWeights(current => ({ ...current, [key]: num(value, current[key]) })); } - function toggleBasic(key: WeightKey) { - setDraftBasicColumns(current => { - if (current.includes(key)) return current.filter(x => x !== key); - if (key === "attendance") return [...current.filter(x => x !== "moderatedAttendance"), key]; - if (key === "moderatedAttendance") return [...current.filter(x => x !== "attendance"), key]; - return [...current, key]; - }); - } - function applyWeights() { - if (selectedTotal <= 0) return; - setApplyingWeights(true); - const applied = normalize(draftBasicColumns, draftWeights); - setBasicColumns(draftBasicColumns); setWeights(applied); setDraftWeights(applied); - window.setTimeout(() => setApplyingWeights(false), 250); - } + function toggleBasic(key: WeightKey) { setDraftBasicColumns(current => { if (current.includes(key)) return current.filter(x => x !== key); if (key === "attendance") return [...current.filter(x => x !== "moderatedAttendance"), key]; if (key === "moderatedAttendance") return [...current.filter(x => x !== "attendance"), key]; return [...current, key]; }); } + function applyWeights() { if (selectedTotal <= 0) return; setApplyingWeights(true); const applied = normalize(draftBasicColumns, draftWeights); setBasicColumns(draftBasicColumns); setWeights(applied); setDraftWeights(applied); window.setTimeout(() => setApplyingWeights(false), 250); } function updateCriteria(i: number, field: keyof Criteria, value: string) { - setCriteria(current => { - const next = current.map(row => ({ ...row })); const parsed = num(value, next[i][field]); - if (field === "from") return current; - if (field === "to") next[i].to = Math.max(next[i].from, Math.floor(parsed)); else next[i][field] = parsed; - next[0].from = 1; - for (let index = 1; index < next.length; index++) { next[index].from = next[index - 1].to + 1; if (next[index].to < next[index].from) next[index].to = next[index].from; } - return next; - }); + setCriteria(current => { const next = current.map(row => ({ ...row })); const parsed = num(value, next[i][field]); if (field === "from") return current; if (field === "to") next[i].to = Math.max(next[i].from, Math.floor(parsed)); else next[i][field] = parsed; next[0].from = 1; for (let index = 1; index < next.length; index++) { next[index].from = next[index - 1].to + 1; if (next[index].to < next[index].from) next[index].to = next[index].from; } return next; }); } return
-
-
-

Subject Analysis

{computedAt && • Last synced {new Date(computedAt).toLocaleString()}}
-
-
- +

Subject Analysis

{computedAt && • Last synced {new Date(computedAt).toLocaleString()}}
{typeof subjectId === "string" && } -
- - -
- +
{error &&
{error}
} {loading && !data &&
Loading overall analysis...
} - {data && view === "internal" ? <>
-
-

Applied Components Summary

Choose components and set their weightage.

Selected total: {selectedTotal}/40
-
{COMPONENTS.map(({ key, label }) => { const checked = draftBasicColumns.includes(key); return ; })}
-

Only checked components count. Weightage is normalized to exactly 40 when applied.

-
-
-

Moderated Marks Criteria

Rank ranges map continuously to a marks range. Marks are defined from and to.

-
{criteria.map((tier, index) =>
Tier {index + 1}
{(["from","to","minMarks","maxMarks"] as const).map(field =>
updateCriteria(index, field, e.target.value)} className="w-full min-w-0 rounded-md border border-slate-200 px-1.5 py-1.5 text-[11px] text-slate-800"/>
)}
)}
-
+

Applied Components Summary

Choose components and set their weightage.

Selected total: {selectedTotal}/40
{COMPONENTS.map(({ key, label }) => { const checked = draftBasicColumns.includes(key); return ; })}

Only checked components count. Weightage is normalized to exactly 40 when applied.

+

Moderated Marks Criteria

Rank ranges map continuously to a marks range. Marks are defined from and to.

{criteria.map((tier, index) =>
Tier {index + 1}
{(["from","to","minMarks","maxMarks"] as const).map(field =>
updateCriteria(index, field, e.target.value)} className="w-full min-w-0 rounded-md border border-slate-200 px-1.5 py-1.5 text-[11px] text-slate-800"/>
)}
)}

Internal Marks Data

Basic marks use the selected components. Moderated marks are calculated from the configured rank criteria.

{HEADERS.map(h => )}{sortedRows.map((row,index) => )}
#NameEnrollment{h.label}
{index+1}{row.name}{row.enrollmentNo}{row.assignment}{row.presentation}{row.attendance}{row.moderatedAttendanceWeighted}{row.midsem1}{row.midsem2}{row.basic}{row.moderated}
: data && <>
-
-
-

At-Risk Filters

Choose a mark column, range and sort.

-
- - -
+
+
+

At-Risk Filters

Choose a mark column, range and sort.

+ + + +
-

Filtered Students

Showing {filteredRiskRows.length} of {baseRows.length} students.

{filteredRiskRows.length} Students
-
{filteredRiskRows.map(row=>)}
EnrollmentStudentMarksModeratedRankTierStatus
{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{row.rank}{criteria.findIndex(c=>row.rank>=c.from&&row.rank<=c.to)+1||"—"}{row.basic<16?"Critical Risk":row.basic<24?"Needs Attention":"Good"}
+
{filteredRiskRows.map(row=>)}
EnrollmentStudentMarksModeratedRankTierStatus
{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{row.rank}{criteria.findIndex(c=>row.rank>=c.from&&row.rank<=c.to)+1||"—"}{row.basic<16?"Critical Risk":row.basic<24?"Needs Attention":"Good"}
-

Filtered Students

Showing {filteredRiskRows.length} of {baseRows.length} students.

{filteredRiskRows.length} Students
-
{sortDirection !== "none" && }{sortDirection !== "none" && }{filteredRiskRows.map((row,index)=>{ const status = riskStatus(columnValue(row,riskColumn)); return <>{sortDirection !== "none" && }})}
RankEnrollmentStudentMarksModeratedStatus
{index + 1}{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{status.label}
+
{sortDirection !== "none" && }{sortDirection !== "none" && }{filteredRiskRows.map((row,index)=>{ const status = riskStatus(columnValue(row,riskColumn)); return {sortDirection !== "none" && }})}
RankEnrollmentStudentMarksModeratedStatus
{index + 1}{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{status.label}
} From 9489d64590184fbbe1d67c807d66829f9773b6a4 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 14:16:49 +0530 Subject: [PATCH 08/80] Align At Risk filter controls --- app/globals.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/globals.css b/app/globals.css index 28c7875..a4c592c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -34,8 +34,8 @@ .recharts-tooltip-wrapper .recharts-default-tooltip{border-radius:12px!important;border:1px solid #e2e8f0!important;box-shadow:0 12px 28px rgba(15,23,42,.1)!important;background:rgba(255,255,255,.97)!important}.analysis-stat-card,.analysis-ranked-list,.analysis-grade-badge{border-color:#e2e8f0} /* Overall > At Risk */ -.at-risk-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;grid-template-areas:"filter filter" "table side";gap:12px;align-items:start}.at-risk-filter{grid-area:filter}.at-risk-table{grid-area:table;min-width:0}.at-risk-side-stack{grid-area:side;display:grid;gap:12px;min-width:0}.at-risk-side-stack>section:first-child{min-height:250px}.at-risk-filter-inner{display:grid;grid-template-columns:minmax(220px,1.25fr) minmax(170px,1fr) minmax(130px,.7fr) minmax(130px,.7fr) auto;gap:10px;align-items:end;padding:12px}.at-risk-filter-title{min-width:0;padding:0 2px}.at-risk-filter-title h3{font-size:14px;font-weight:700;color:#172033}.at-risk-filter-title p{margin-top:3px;font-size:10px;color:#667085}.at-risk-filter label{display:block;min-width:0}.at-risk-filter label>span{display:block;margin:0 0 5px 2px;font-size:10px;font-weight:600;color:#667085}.at-risk-filter select,.at-risk-filter input{height:38px}.at-risk-apply{height:38px;padding:0 18px;border:0;border-radius:9px;background:linear-gradient(135deg,#251b62,#4637a3);color:#fff;font-size:12px;font-weight:600;box-shadow:0 7px 16px rgba(49,39,120,.14);white-space:nowrap}.at-risk-apply:hover{filter:brightness(1.03)}.at-risk-table table th,.at-risk-table table td{white-space:nowrap}.at-risk-table tbody tr:hover{background:rgba(79,70,229,.025)}.at-risk-side-stack>section{min-width:0}.at-risk-side-stack .recharts-pie-sector{cursor:pointer}.at-risk-side-stack .recharts-text{font-size:9px}.at-risk-side-stack>section:first-child{min-height:280px} +.at-risk-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;grid-template-areas:"filter filter" "table side";gap:12px;align-items:start}.at-risk-filter{grid-area:filter}.at-risk-table{grid-area:table;min-width:0}.at-risk-side-stack{grid-area:side;display:grid;gap:12px;min-width:0}.at-risk-side-stack>section:first-child{min-height:250px}.at-risk-filter-inner{padding:12px}.at-risk-filter-title{min-width:0;padding:0 2px}.at-risk-filter-title h3{font-size:14px;font-weight:700;color:#172033}.at-risk-filter-title p{margin-top:3px;font-size:10px;color:#667085}.at-risk-filter-controls{display:grid;grid-template-columns:minmax(220px,1.25fr) minmax(170px,1fr) minmax(130px,.7fr) minmax(130px,.7fr) auto;gap:10px;align-items:end;margin-top:10px}.at-risk-filter label{display:block;min-width:0}.at-risk-filter label>span{display:block;margin:0 0 5px 2px;font-size:10px;font-weight:600;color:#667085}.at-risk-filter select,.at-risk-filter input{height:38px}.at-risk-apply{height:38px;padding:0 18px;border:0;border-radius:9px;background:linear-gradient(135deg,#251b62,#4637a3);color:#fff;font-size:12px;font-weight:600;box-shadow:0 7px 16px rgba(49,39,120,.14);white-space:nowrap}.at-risk-apply:hover{filter:brightness(1.03)}.at-risk-table table th,.at-risk-table table td{white-space:nowrap}.at-risk-table tbody tr:hover{background:rgba(79,70,229,.025)}.at-risk-side-stack>section{min-width:0}.at-risk-side-stack .recharts-pie-sector{cursor:pointer}.at-risk-side-stack .recharts-text{font-size:9px}.at-risk-side-stack>section:first-child{min-height:280px} -@media(max-width:1100px){.analysis-hero{grid-template-columns:1fr 1fr}.analysis-hero-copy{grid-column:1/-1}.analysis-content-grid{grid-template-columns:1fr}.analysis-settings-grid{grid-template-columns:1fr 1fr}.analysis-sidebar{position:fixed;transform:translateX(-100%)}.analysis-layout{grid-template-columns:1fr}.analysis-page{padding:18px}.academic-table-wrap{height:500px;max-height:500px}.analysis-top-actions>.analysis-raw-button{position:static;width:auto;min-height:0;border:1px solid #dfe5ec;background:rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(15,23,42,.04)}.analysis-top-actions>.analysis-raw-button span:first-child:before{display:none}.at-risk-layout{grid-template-columns:minmax(0,1fr) 280px}.at-risk-filter-inner{grid-template-columns:minmax(190px,1.2fr) repeat(2,minmax(120px,.7fr)) minmax(150px,.9fr) auto;gap:8px}.at-risk-filter-title p{white-space:nowrap}} -@media(max-width:800px){.at-risk-layout{grid-template-columns:1fr;grid-template-areas:"filter" "table" "side"}.at-risk-filter-inner{grid-template-columns:1fr 1fr}.at-risk-filter-title{grid-column:1/-1}.at-risk-apply{width:100%}.at-risk-side-stack{grid-template-columns:1fr 1fr}.at-risk-side-stack>section:first-child{min-height:0}} -@media(max-width:700px){.analysis-topbar{align-items:flex-start;flex-direction:column}.analysis-title-row h1{font-size:21px}.analysis-top-actions{width:100%}.analysis-settings-grid{grid-template-columns:1fr}.analysis-hero{grid-template-columns:1fr}.analysis-content-grid{grid-template-columns:1fr}.analysis-page{padding:14px}.analysis-insight{align-items:flex-start;flex-direction:column}.analysis-insight-actions{margin-left:0;flex-wrap:wrap}.subject-analysis-nav__item{padding:9px 12px}.analysis-chart{height:200px}.academic-stats-panel,.academic-rank-grid{grid-template-columns:1fr}.academic-tier-grid{grid-template-columns:1fr 1fr}.academic-table-wrap{height:440px;max-height:440px}.at-risk-filter-inner{grid-template-columns:1fr}.at-risk-filter-title{grid-column:auto}.at-risk-side-stack{grid-template-columns:1fr}.at-risk-filter-title p{white-space:normal}} +@media(max-width:1100px){.analysis-hero{grid-template-columns:1fr 1fr}.analysis-hero-copy{grid-column:1/-1}.analysis-content-grid{grid-template-columns:1fr}.analysis-settings-grid{grid-template-columns:1fr 1fr}.analysis-sidebar{position:fixed;transform:translateX(-100%)}.analysis-layout{grid-template-columns:1fr}.analysis-page{padding:18px}.academic-table-wrap{height:500px;max-height:500px}.analysis-top-actions>.analysis-raw-button{position:static;width:auto;min-height:0;border:1px solid #dfe5ec;background:rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(15,23,42,.04)}.analysis-top-actions>.analysis-raw-button span:first-child:before{display:none}.at-risk-layout{grid-template-columns:minmax(0,1fr) 280px}.at-risk-filter-controls{grid-template-columns:minmax(190px,1.2fr) repeat(2,minmax(120px,.7fr)) minmax(150px,.9fr) auto;gap:8px}.at-risk-filter-title p{white-space:nowrap}} +@media(max-width:800px){.at-risk-layout{grid-template-columns:1fr;grid-template-areas:"filter" "table" "side"}.at-risk-filter-controls{grid-template-columns:1fr 1fr}.at-risk-apply{width:100%}.at-risk-side-stack{grid-template-columns:1fr 1fr}.at-risk-side-stack>section:first-child{min-height:0}} +@media(max-width:700px){.analysis-topbar{align-items:flex-start;flex-direction:column}.analysis-title-row h1{font-size:21px}.analysis-top-actions{width:100%}.analysis-settings-grid{grid-template-columns:1fr}.analysis-hero{grid-template-columns:1fr}.analysis-content-grid{grid-template-columns:1fr}.analysis-page{padding:14px}.analysis-insight{align-items:flex-start;flex-direction:column}.analysis-insight-actions{margin-left:0;flex-wrap:wrap}.subject-analysis-nav__item{padding:9px 12px}.analysis-chart{height:200px}.academic-stats-panel,.academic-rank-grid{grid-template-columns:1fr}.academic-tier-grid{grid-template-columns:1fr 1fr}.academic-table-wrap{height:440px;max-height:440px}.at-risk-filter-controls{grid-template-columns:1fr}.at-risk-filter-title{grid-column:auto}.at-risk-side-stack{grid-template-columns:1fr}.at-risk-filter-title p{white-space:normal}} From 8eeb56ad1ada48c960c3198d5f70a7878d168353 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 14:27:14 +0530 Subject: [PATCH 09/80] Fix At Risk score color consistency --- pages/subject-analysis/[subjectId]/overall.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pages/subject-analysis/[subjectId]/overall.tsx b/pages/subject-analysis/[subjectId]/overall.tsx index 707d060..1bcc410 100644 --- a/pages/subject-analysis/[subjectId]/overall.tsx +++ b/pages/subject-analysis/[subjectId]/overall.tsx @@ -66,6 +66,12 @@ function scoreClass(value: number, max = 40) { if (ratio >= 0.6) return "text-amber-600 font-semibold"; return "text-red-500 font-semibold"; } +function riskScoreClass(value: number) { + if (value > 32) return "text-emerald-600 font-semibold"; + if (value >= 24) return "text-green-600 font-semibold"; + if (value >= 16) return "text-amber-600 font-semibold"; + return "text-red-500 font-semibold"; +} function riskStatus(value: number) { if (value > 32) return { label: "Excellent", className: "bg-emerald-50 text-emerald-700" }; if (value >= 24) return { label: "Good", className: "bg-green-50 text-green-700" }; @@ -215,11 +221,11 @@ export default function SubjectOverallPage() {

Filtered Students

Showing {filteredRiskRows.length} of {baseRows.length} students.

{filteredRiskRows.length} Students
-
{sortDirection !== "none" && }{sortDirection !== "none" && }{filteredRiskRows.map((row,index)=>{ const status = riskStatus(columnValue(row,riskColumn)); return {sortDirection !== "none" && }})}
RankEnrollmentStudentMarksModeratedStatus
{index + 1}{row.enrollmentNo}{row.name}{row.basic}{row.moderated}{status.label}
+
{sortDirection !== "none" && }{sortDirection !== "none" && }{filteredRiskRows.map((row,index)=>{ const status = riskStatus(columnValue(row,riskColumn)); return {sortDirection !== "none" && }})}
RankEnrollmentStudentMarksModeratedStatus
{index + 1}{row.enrollmentNo}{row.name}{columnValue(row,riskColumn)}{row.moderated}{status.label}
} From 40f2e4e218003ad7ef1fa73417cfa5638f831ed9 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 14:34:39 +0530 Subject: [PATCH 10/80] Fix at-risk filter sizing and excellent status color --- app/globals.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/globals.css b/app/globals.css index a4c592c..12ed325 100644 --- a/app/globals.css +++ b/app/globals.css @@ -34,8 +34,8 @@ .recharts-tooltip-wrapper .recharts-default-tooltip{border-radius:12px!important;border:1px solid #e2e8f0!important;box-shadow:0 12px 28px rgba(15,23,42,.1)!important;background:rgba(255,255,255,.97)!important}.analysis-stat-card,.analysis-ranked-list,.analysis-grade-badge{border-color:#e2e8f0} /* Overall > At Risk */ -.at-risk-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;grid-template-areas:"filter filter" "table side";gap:12px;align-items:start}.at-risk-filter{grid-area:filter}.at-risk-table{grid-area:table;min-width:0}.at-risk-side-stack{grid-area:side;display:grid;gap:12px;min-width:0}.at-risk-side-stack>section:first-child{min-height:250px}.at-risk-filter-inner{padding:12px}.at-risk-filter-title{min-width:0;padding:0 2px}.at-risk-filter-title h3{font-size:14px;font-weight:700;color:#172033}.at-risk-filter-title p{margin-top:3px;font-size:10px;color:#667085}.at-risk-filter-controls{display:grid;grid-template-columns:minmax(220px,1.25fr) minmax(170px,1fr) minmax(130px,.7fr) minmax(130px,.7fr) auto;gap:10px;align-items:end;margin-top:10px}.at-risk-filter label{display:block;min-width:0}.at-risk-filter label>span{display:block;margin:0 0 5px 2px;font-size:10px;font-weight:600;color:#667085}.at-risk-filter select,.at-risk-filter input{height:38px}.at-risk-apply{height:38px;padding:0 18px;border:0;border-radius:9px;background:linear-gradient(135deg,#251b62,#4637a3);color:#fff;font-size:12px;font-weight:600;box-shadow:0 7px 16px rgba(49,39,120,.14);white-space:nowrap}.at-risk-apply:hover{filter:brightness(1.03)}.at-risk-table table th,.at-risk-table table td{white-space:nowrap}.at-risk-table tbody tr:hover{background:rgba(79,70,229,.025)}.at-risk-side-stack>section{min-width:0}.at-risk-side-stack .recharts-pie-sector{cursor:pointer}.at-risk-side-stack .recharts-text{font-size:9px}.at-risk-side-stack>section:first-child{min-height:280px} +.at-risk-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;grid-template-areas:"filter filter" "table side";gap:12px;align-items:start}.at-risk-filter{grid-area:filter}.at-risk-table{grid-area:table;min-width:0}.at-risk-side-stack{grid-area:side;display:grid;gap:12px;min-width:0}.at-risk-side-stack>section:first-child{min-height:250px}.at-risk-filter-inner{padding:12px}.at-risk-filter-title{min-width:0;padding:0 2px}.at-risk-filter-title h3{font-size:14px;font-weight:700;color:#172033}.at-risk-filter-title p{margin-top:3px;font-size:10px;color:#667085}.at-risk-filter-controls{display:grid;grid-template-columns:repeat(4,minmax(0,1fr)) auto;gap:10px;align-items:end;margin-top:10px}.at-risk-filter label{display:block;min-width:0}.at-risk-filter label>span{display:block;margin:0 0 5px 2px;font-size:10px;font-weight:600;color:#667085}.at-risk-filter select,.at-risk-filter input{height:38px}.at-risk-apply{height:38px;padding:0 18px;border:0;border-radius:9px;background:linear-gradient(135deg,#251b62,#4637a3);color:#fff;font-size:12px;font-weight:600;box-shadow:0 7px 16px rgba(49,39,120,.14);white-space:nowrap}.at-risk-apply:hover{filter:brightness(1.03)}.at-risk-table table th,.at-risk-table table td{white-space:nowrap}.at-risk-table tbody tr:hover{background:rgba(79,70,229,.025)}.at-risk-side-stack>section{min-width:0}.at-risk-side-stack .recharts-pie-sector{cursor:pointer}.at-risk-side-stack .recharts-text{font-size:9px}.at-risk-side-stack>section:first-child{min-height:280px}.at-risk-table .bg-emerald-50.text-emerald-700,.at-risk-side-stack .bg-emerald-50.text-emerald-700{background:#eff6ff!important;color:#2563eb!important} -@media(max-width:1100px){.analysis-hero{grid-template-columns:1fr 1fr}.analysis-hero-copy{grid-column:1/-1}.analysis-content-grid{grid-template-columns:1fr}.analysis-settings-grid{grid-template-columns:1fr 1fr}.analysis-sidebar{position:fixed;transform:translateX(-100%)}.analysis-layout{grid-template-columns:1fr}.analysis-page{padding:18px}.academic-table-wrap{height:500px;max-height:500px}.analysis-top-actions>.analysis-raw-button{position:static;width:auto;min-height:0;border:1px solid #dfe5ec;background:rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(15,23,42,.04)}.analysis-top-actions>.analysis-raw-button span:first-child:before{display:none}.at-risk-layout{grid-template-columns:minmax(0,1fr) 280px}.at-risk-filter-controls{grid-template-columns:minmax(190px,1.2fr) repeat(2,minmax(120px,.7fr)) minmax(150px,.9fr) auto;gap:8px}.at-risk-filter-title p{white-space:nowrap}} +@media(max-width:1100px){.analysis-hero{grid-template-columns:1fr 1fr}.analysis-hero-copy{grid-column:1/-1}.analysis-content-grid{grid-template-columns:1fr}.analysis-settings-grid{grid-template-columns:1fr 1fr}.analysis-sidebar{position:fixed;transform:translateX(-100%)}.analysis-layout{grid-template-columns:1fr}.analysis-page{padding:18px}.academic-table-wrap{height:500px;max-height:500px}.analysis-top-actions>.analysis-raw-button{position:static;width:auto;min-height:0;border:1px solid #dfe5ec;background:rgba(255,255,255,.8);box-shadow:0 4px 14px rgba(15,23,42,.04)}.analysis-top-actions>.analysis-raw-button span:first-child:before{display:none}.at-risk-layout{grid-template-columns:minmax(0,1fr) 280px}.at-risk-filter-controls{grid-template-columns:repeat(4,minmax(0,1fr)) auto;gap:8px}.at-risk-filter-title p{white-space:nowrap}} @media(max-width:800px){.at-risk-layout{grid-template-columns:1fr;grid-template-areas:"filter" "table" "side"}.at-risk-filter-controls{grid-template-columns:1fr 1fr}.at-risk-apply{width:100%}.at-risk-side-stack{grid-template-columns:1fr 1fr}.at-risk-side-stack>section:first-child{min-height:0}} @media(max-width:700px){.analysis-topbar{align-items:flex-start;flex-direction:column}.analysis-title-row h1{font-size:21px}.analysis-top-actions{width:100%}.analysis-settings-grid{grid-template-columns:1fr}.analysis-hero{grid-template-columns:1fr}.analysis-content-grid{grid-template-columns:1fr}.analysis-page{padding:14px}.analysis-insight{align-items:flex-start;flex-direction:column}.analysis-insight-actions{margin-left:0;flex-wrap:wrap}.subject-analysis-nav__item{padding:9px 12px}.analysis-chart{height:200px}.academic-stats-panel,.academic-rank-grid{grid-template-columns:1fr}.academic-tier-grid{grid-template-columns:1fr 1fr}.academic-table-wrap{height:440px;max-height:440px}.at-risk-filter-controls{grid-template-columns:1fr}.at-risk-filter-title{grid-column:auto}.at-risk-side-stack{grid-template-columns:1fr}.at-risk-filter-title p{white-space:normal}} From 09fe7b7d878ddb5a22737a783cb89a152887a4c2 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 14:44:32 +0530 Subject: [PATCH 11/80] Fix attendance trend metric and at-risk apply flow --- .../[subjectId]/attendance.tsx | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/pages/subject-analysis/[subjectId]/attendance.tsx b/pages/subject-analysis/[subjectId]/attendance.tsx index 2575d36..7f58f1c 100644 --- a/pages/subject-analysis/[subjectId]/attendance.tsx +++ b/pages/subject-analysis/[subjectId]/attendance.tsx @@ -29,6 +29,10 @@ export default function SubjectAttendancePage() { const [upperBound, setUpperBound] = useState(100); const [riskTrendFilter, setRiskTrendFilter] = useState("All"); const [riskMonth, setRiskMonth] = useState<"previous" | "current">("current"); + const [draftLowerBound, setDraftLowerBound] = useState(0); + const [draftUpperBound, setDraftUpperBound] = useState(100); + const [draftRiskTrendFilter, setDraftRiskTrendFilter] = useState("All"); + const [draftRiskMonth, setDraftRiskMonth] = useState<"previous" | "current">("current"); const [copied, setCopied] = useState(false); async function loadAnalysis(sync = false, previous = previousMonth, current = currentMonth, criteria = trendCriteria) { @@ -87,12 +91,38 @@ export default function SubjectAttendancePage() { ]; function openRisk(options: { trend?: string; lower?: number; upper?: number }) { - setRiskTrendFilter(options.trend || "All"); - setLowerBound(options.lower ?? 0); - setUpperBound(options.upper ?? 100); + const nextTrend = options.trend || "All"; + const nextLower = options.lower ?? 0; + const nextUpper = options.upper ?? 100; + setDraftRiskTrendFilter(nextTrend); + setDraftLowerBound(nextLower); + setDraftUpperBound(nextUpper); + setRiskTrendFilter(nextTrend); + setLowerBound(nextLower); + setUpperBound(nextUpper); setView("risk"); } + function applyRiskFilters() { + const lo = Math.max(0, Math.min(100, Math.min(draftLowerBound, draftUpperBound))); + const hi = Math.max(0, Math.min(100, Math.max(draftLowerBound, draftUpperBound))); + setLowerBound(lo); + setUpperBound(hi); + setRiskTrendFilter(draftRiskTrendFilter); + setRiskMonth(draftRiskMonth); + } + + function resetRiskFilters() { + setLowerBound(0); + setUpperBound(100); + setRiskTrendFilter("All"); + setRiskMonth("current"); + setDraftLowerBound(0); + setDraftUpperBound(100); + setDraftRiskTrendFilter("All"); + setDraftRiskMonth("current"); + } + async function copyEmails() { try { await navigator.clipboard.writeText(riskEmails.join("; ")); @@ -142,7 +172,7 @@ export default function SubjectAttendancePage() {

Attendance Trend

Compare attendance between two months and identify increasing, decreasing, and stable students.

- } label={`Class Average (${previousMonth || "Previous"})`} value={`${previousAverage}%`} change={averageChange} /> + } label={`Class Average (${previousMonth || "Previous"})`} value={`${previousAverage}%`} /> } label={`Class Average (${currentMonth || "Current"})`} value={`${currentAverage}%`} change={averageChange} /> } label="Students Improving" value={improvingCount} detail={`${students.length ? round1((improvingCount / students.length) * 100) : 0}% of total students`} good /> @@ -212,13 +242,14 @@ export default function SubjectAttendancePage() {

Filter Criteria

Narrow the list before copying addresses or sending alerts.

- +
-
setLowerBound(Math.max(0, Math.min(100, Number(e.target.value))))} />
-
setUpperBound(Math.max(0, Math.min(100, Number(e.target.value))))} />
-
-
+
setDraftLowerBound(Math.max(0, Math.min(100, Number(e.target.value))))} />
+
setDraftUpperBound(Math.max(0, Math.min(100, Number(e.target.value))))} />
+
+
+
From 5bff14d6bf95d0c85e8ec1adb653c1be055b5f6f Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 14:52:46 +0530 Subject: [PATCH 12/80] Fix academic table controls, colors, and apply behavior --- .../subject-analysis/[subjectId]/academic.tsx | 116 +++++++++++------- 1 file changed, 72 insertions(+), 44 deletions(-) diff --git a/pages/subject-analysis/[subjectId]/academic.tsx b/pages/subject-analysis/[subjectId]/academic.tsx index 36e2e65..8eccd29 100644 --- a/pages/subject-analysis/[subjectId]/academic.tsx +++ b/pages/subject-analysis/[subjectId]/academic.tsx @@ -16,7 +16,7 @@ type SummarySort = "none" | "desc" | "asc"; const MAX = 30; const TIERS: Tier[] = ["Excellent", "Good", "Needs Attention", "Critical Risk"]; -const COLORS: Record = { Excellent: "#15966a", Good: "#4d75d0", "Needs Attention": "#f59e0b", "Critical Risk": "#ef4444" }; +const COLORS: Record = { Excellent: "#2563eb", Good: "#15966a", "Needs Attention": "#f59e0b", "Critical Risk": "#ef4444" }; const round1 = (n: number) => Math.round(n * 10) / 10; const initials = (name: string) => name.split(" ").filter(Boolean).slice(0, 2).map((part) => part[0]).join("").toUpperCase(); @@ -57,13 +57,22 @@ export default function SubjectAcademicPage() { const [syncing, setSyncing] = useState(false); const [error, setError] = useState(""); const [selectedTier, setSelectedTier] = useState(null); + const [combinedSort, setCombinedSort] = useState("combined"); const [sortOrder, setSortOrder] = useState("desc"); + const [combinedSortDraft, setCombinedSortDraft] = useState("combined"); + const [sortOrderDraft, setSortOrderDraft] = useState("desc"); + const [summaryExam, setSummaryExam] = useState("combined"); const [summaryTier, setSummaryTier] = useState("all"); const [summaryLower, setSummaryLower] = useState("0"); const [summaryUpper, setSummaryUpper] = useState("30"); const [summarySort, setSummarySort] = useState("none"); + const [summaryExamDraft, setSummaryExamDraft] = useState("combined"); + const [summaryTierDraft, setSummaryTierDraft] = useState("all"); + const [summaryLowerDraft, setSummaryLowerDraft] = useState("0"); + const [summaryUpperDraft, setSummaryUpperDraft] = useState("30"); + const [summarySortDraft, setSummarySortDraft] = useState("none"); async function loadAnalysis(sync = false) { if (!subjectId || typeof subjectId !== "string") return; @@ -172,6 +181,32 @@ export default function SubjectAcademicPage() { const increases = [...combinedRows].map((row) => ({ ...row, change: round1(row.second - row.first) })).filter((row) => row.change > 0).sort((a, b) => b.change - a.change).slice(0, 5); const decreases = [...combinedRows].map((row) => ({ ...row, change: round1(row.second - row.first) })).filter((row) => row.change < 0).sort((a, b) => a.change - b.change).slice(0, 5); + const applyCombinedControls = () => { + setCombinedSort(combinedSortDraft); + setSortOrder(sortOrderDraft); + }; + + const resetSummaryControls = () => { + setSummaryExam("combined"); + setSummaryTier("all"); + setSummaryLower("0"); + setSummaryUpper("30"); + setSummarySort("none"); + setSummaryExamDraft("combined"); + setSummaryTierDraft("all"); + setSummaryLowerDraft("0"); + setSummaryUpperDraft("30"); + setSummarySortDraft("none"); + }; + + const applySummaryControls = () => { + setSummaryExam(summaryExamDraft); + setSummaryTier(summaryTierDraft); + setSummaryLower(summaryLowerDraft); + setSummaryUpper(summaryUpperDraft); + setSummarySort(summarySortDraft); + }; + return
+ +
+
+
+
+
+ + ClassPulse +
+

Welcome back, {teacherName}

+

Here's an overview of your day.

+
+
+ +
+ + {new Date().toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })} +
+ +
+
+ +
+
+

Quick Actions

+

Jump straight into the tools you use most.

+
+
+ {quickActions.map((action) => { + const Icon = action.icon; + const isAnalysis = action.title === "Class Analysis" || action.title === "Subject Analysis"; + const href = isAnalysis + ? action.title === "Class Analysis" + ? sections[0]?.id ? `/section-analysis/${sections[0].id}` : "/section-analysis" + : subjects[0]?.id ? `/subject-analysis/${subjects[0].id}` : "/subject-analysis" + : action.href; + + return ( + + + + {action.title} + {action.description} + + + + ); + })}
-
-

Subject Analysis

-

- Detailed attendance, marks, and student reports for one subject. -

-
- {subjects.map((s) => ( - - {s.label} - - ))} - {subjects.length === 0 && ( -

No subjects assigned yet.

- )} +
+
+
+
+

Today's Timetable

+

Your scheduled classes for today.

+
+ View full timetable +
+
+ {todaySchedule.map((item, index) => ( +
+
+ {item.time} + {item.end} +
+
+ + {index < todaySchedule.length - 1 && } +

{item.subject}

+

{item.section} {item.room}

+ {item.active && Next class} +
+
+ ))} +
+
+ +
+
+
+

Recent Activity

+

Your latest actions in ClassPulse.

+
+ +
+
+ {recentActivity.map((activity) => { + const Icon = activity.icon; + return ( +
+ +

{activity.text}

+ {activity.time} +
+ ); + })} +
+ +
© 2026 ClassPulse. All rights reserved.
@@ -90,8 +272,6 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { const userId = (session.user as any).id; const role = (session.user as any).role; - // Sections the teacher can view: they proctor the class, or teach a - // subject within that section. const sectionsRaw = role === "ADMIN" ? await prisma.section.findMany({ include: { class: { include: { department: true } } } }) From 670405dd21ea6987bf2da3e362801da15ff6f680 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 18:53:13 +0530 Subject: [PATCH 15/80] Seed ECE batches and semester subjects --- prisma/seed.ts | 269 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 227 insertions(+), 42 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 425a114..7196111 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3,17 +3,113 @@ import bcrypt from "bcryptjs"; const prisma = new PrismaClient(); +type SubjectSeed = { + name: string; + code: string; +}; + +// These are the theory subjects supplied for the ECE curriculum. +// Labs are intentionally excluded for now. +const subjectsBySemester: Record = { + 1: [ + { name: "Environmental Studies", code: "SEM1-ENVIRONMENTAL-STUDIES" }, + { name: "Engineering Graphics-I", code: "SEM1-ENGINEERING-GRAPHICS-I" }, + { name: "Manufacturing Process", code: "SEM1-MANUFACTURING-PROCESS" }, + { name: "Communications Skills", code: "SEM1-COMMUNICATIONS-SKILLS" }, + { name: "Applied Mathematics-I", code: "SEM1-APPLIED-MATHEMATICS-I" }, + { name: "Applied Physics-I", code: "SEM1-APPLIED-PHYSICS-I" }, + { name: "Programming in C", code: "SEM1-PROGRAMMING-IN-C" }, + ], + 2: [ + { name: "Applied Chemistry", code: "SEM2-APPLIED-CHEMISTRY" }, + { name: "Applied Physics-II", code: "SEM2-APPLIED-PHYSICS-II" }, + { name: "Electrical Science", code: "SEM2-ELECTRICAL-SCIENCE" }, + { name: "Applied Mathematics-II", code: "SEM2-APPLIED-MATHEMATICS-II" }, + { name: "Engineering Mechanics", code: "SEM2-ENGINEERING-MECHANICS" }, + { name: "Indian Constitution", code: "SEM2-INDIAN-CONSTITUTION" }, + { name: "Human Values and Ethics", code: "SEM2-HUMAN-VALUES-AND-ETHICS" }, + ], + 3: [ + { name: "Computational Methods", code: "SEM3-COMPUTATIONAL-METHODS" }, + { name: "Indian Knowledge System", code: "SEM3-INDIAN-KNOWLEDGE-SYSTEM" }, + { name: "Signal and System", code: "SEM3-SIGNAL-AND-SYSTEM" }, + { name: "Digital Logic and Computer Design", code: "SEM3-DIGITAL-LOGIC-AND-COMPUTER-DESIGN" }, + { name: "Analog Communications", code: "SEM3-ANALOG-COMMUNICATIONS" }, + { name: "Analog Electronics-I", code: "SEM3-ANALOG-ELECTRONICS-I" }, + ], + 4: [ + { name: "Probability, Statistics and Linear Programming", code: "SEM4-PSLP" }, + { name: "Technical Writing", code: "SEM4-TECHNICAL-WRITING" }, + { name: "Network Analysis and Synthesis", code: "SEM4-NAS" }, + { name: "Microprocessors and Microcontrollers", code: "SEM4-MICROPROCESSORS-AND-MICROCONTROLLERS" }, + { name: "Digital Communications", code: "SEM4-DIGITAL-COMMUNICATIONS" }, + { name: "Analog Electronics-II", code: "SEM4-ANALOG-ELECTRONICS-II" }, + { name: "Electromagnetic Field Theory", code: "SEM4-EMFT" }, + ], + 5: [ + { name: "Economics for Engineers", code: "SEM5-ECONOMICS-FOR-ENGINEERS" }, + { name: "Digital Signal Processing", code: "SEM5-DSP" }, + { name: "Microelectronics", code: "SEM5-MICROELECTRONICS" }, + { name: "Introduction to Control Systems", code: "SEM5-INTRODUCTION-TO-CONTROL-SYSTEMS" }, + { name: "Transmission Lines, Waveguides and Antenna Design", code: "SEM5-TLWAD" }, + { name: "Data Communication and Networking", code: "SEM5-DCN" }, + ], + 6: [ + { name: "PME", code: "MS-302" }, + { name: "AI", code: "ECE 318 T" }, + { name: "OCSN", code: "ECE 326 T" }, + { name: "ML", code: "ECE 350 T" }, + { name: "SSM&DA", code: "DA 304 T" }, + { name: "DA", code: "DA 338 T" }, + ], + 7: [ + // The timetable supplied identifies these by these abbreviations only. + { name: "MLDA", code: "MLDA" }, + { name: "IOT", code: "IOT" }, + { name: "PR", code: "PR" }, + { name: "USL", code: "USL" }, + { name: "SL", code: "SL" }, + { name: "PE", code: "PE" }, + ], +}; + +const eceBatches = ["ECE 1", "ECE 2", "ECE E"]; + +function yearForSemester(semester: number) { + if (semester <= 2) return "1st Year"; + if (semester <= 4) return "2nd Year"; + if (semester <= 6) return "3rd Year"; + return "4th Year"; +} + async function main() { - const college = await prisma.college.create({ data: { name: "Demo Institute of Technology" } }); + const college = await prisma.college.upsert({ + where: { id: "demo-college" }, + update: { name: "Demo Institute of Technology" }, + create: { id: "demo-college", name: "Demo Institute of Technology" }, + }); - const dept = await prisma.department.create({ - data: { name: "ECE", collegeId: college.id }, + const dept = await prisma.department.findFirst({ + where: { collegeId: college.id, name: "ECE" }, }); + const eceDepartment = + dept ?? + (await prisma.department.create({ + data: { name: "ECE", collegeId: college.id }, + })); + const passwordHash = await bcrypt.hash("changeme123", 10); - const teacher = await prisma.user.create({ - data: { + const teacher = await prisma.user.upsert({ + where: { email: "geetanjali@demo.edu" }, + update: { + name: "Dr. Geetanjali", + passwordHash, + role: "TEACHER", + collegeId: college.id, + }, + create: { name: "Dr. Geetanjali", email: "geetanjali@demo.edu", passwordHash, @@ -22,8 +118,15 @@ async function main() { }, }); - const admin = await prisma.user.create({ - data: { + await prisma.user.upsert({ + where: { email: "admin@demo.edu" }, + update: { + name: "College Admin", + passwordHash, + role: "ADMIN", + collegeId: college.id, + }, + create: { name: "College Admin", email: "admin@demo.edu", passwordHash, @@ -32,51 +135,133 @@ async function main() { }, }); - const cls = await prisma.class.create({ - data: { - departmentId: dept.id, - program: "B.Tech ECE", + // Create all three ECE batches for Semesters 1-7. + // Semester 8 intentionally has no regular subjects. + for (const batch of eceBatches) { + for (let semester = 1; semester <= 7; semester += 1) { + const existingClass = await prisma.class.findFirst({ + where: { + departmentId: eceDepartment.id, + program: `B.Tech ${batch}`, + academicYear: "2026-27", + semester, + }, + }); + + const cls = + existingClass ?? + (await prisma.class.create({ + data: { + departmentId: eceDepartment.id, + program: `B.Tech ${batch}`, + academicYear: "2026-27", + year: yearForSemester(semester), + semester, + proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : null, + }, + })); + + const existingSection = await prisma.section.findFirst({ + where: { classId: cls.id, name: "A" }, + }); + + const section = + existingSection ?? + (await prisma.section.create({ + data: { + classId: cls.id, + name: "A", + strength: batch === "ECE 2" && semester === 7 ? 65 : 0, + }, + })); + + for (const subject of subjectsBySemester[semester]) { + const existingSubject = await prisma.subject.findFirst({ + where: { sectionId: section.id, code: subject.code }, + }); + + if (!existingSubject) { + await prisma.subject.create({ + data: { + name: subject.name, + code: subject.code, + sectionId: section.id, + }, + }); + } + } + } + } + + // Keep the existing ECE 2 Sem 7 / Section A demo data and its sheet links. + const demoClass = await prisma.class.findFirst({ + where: { + departmentId: eceDepartment.id, + program: "B.Tech ECE 2", academicYear: "2026-27", - year: "4th Year", semester: 7, - proctorId: teacher.id, }, }); - const section = await prisma.section.create({ - data: { classId: cls.id, name: "A", strength: 65 }, - }); + const demoSection = demoClass + ? await prisma.section.findFirst({ where: { classId: demoClass.id, name: "A" } }) + : null; - const subject = await prisma.subject.create({ - data: { name: "Data Analysis", code: "DA 338 T", sectionId: section.id }, - }); + if (demoSection) { + await prisma.section.update({ + where: { id: demoSection.id }, + data: { strength: 65 }, + }); - await prisma.assignment.create({ - data: { teacherId: teacher.id, subjectId: subject.id }, - }); + const daSubject = await prisma.subject.findFirst({ + where: { sectionId: demoSection.id, code: "DA 338 T" }, + }); - // Section-level combined sheet (required — one per section). - await prisma.sheetLink.create({ - data: { - sectionId: section.id, - sheetId: "1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM", - }, - }); + if (daSubject) { + const assignment = await prisma.assignment.findFirst({ + where: { teacherId: teacher.id, subjectId: daSubject.id }, + }); - // Subject-level sheet (optional per subject). - await prisma.sheetLink.create({ - data: { - subjectId: subject.id, - sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U", - }, - }); + if (!assignment) { + await prisma.assignment.create({ + data: { teacherId: teacher.id, subjectId: daSubject.id }, + }); + } + + const subjectSheet = await prisma.sheetLink.findFirst({ + where: { subjectId: daSubject.id }, + }); + + if (!subjectSheet) { + await prisma.sheetLink.create({ + data: { + subjectId: daSubject.id, + sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U", + }, + }); + } + } + + const sectionSheet = await prisma.sheetLink.findFirst({ + where: { sectionId: demoSection.id }, + }); + + if (!sectionSheet) { + await prisma.sheetLink.create({ + data: { + sectionId: demoSection.id, + sheetId: "1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM", + }, + }); + } + } - console.log("Seeded. Login as:"); - console.log(" teacher: geetanjali@demo.edu / changeme123"); - console.log(" admin: admin@demo.edu / changeme123"); - console.log(""); - console.log("1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM"); - console.log("in prisma/seed.ts with your real combined-section sheet's ID, then re-seed."); + console.log("Seed complete."); + console.log("Created/updated ECE 1, ECE 2 and ECE E for Semesters 1-7."); + console.log("Semester 8 intentionally has no subjects."); + console.log("Labs are intentionally excluded."); + console.log("Login as: geetanjali@demo.edu / changeme123"); + console.log("Admin: admin@demo.edu / changeme123"); } main() From 8036e69bcecd7aa43d47ca39acb17d995f3e67cc Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 18:53:49 +0530 Subject: [PATCH 16/80] Reuse legacy ECE 7th-semester class in seed --- prisma/seed.ts | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 7196111..aacfbd9 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,14 +1,14 @@ import { PrismaClient } from "@prisma/client"; import bcrypt from "bcryptjs"; -const prisma = new PrismaClient(); - type SubjectSeed = { name: string; code: string; }; -// These are the theory subjects supplied for the ECE curriculum. +const prisma = new PrismaClient(); + +// Theory subjects supplied for the ECE curriculum. // Labs are intentionally excluded for now. const subjectsBySemester: Record = { 1: [ @@ -63,7 +63,7 @@ const subjectsBySemester: Record = { { name: "DA", code: "DA 338 T" }, ], 7: [ - // The timetable supplied identifies these by these abbreviations only. + // The supplied timetable identifies these subjects by these abbreviations only. { name: "MLDA", code: "MLDA" }, { name: "IOT", code: "IOT" }, { name: "PR", code: "PR" }, @@ -89,12 +89,12 @@ async function main() { create: { id: "demo-college", name: "Demo Institute of Technology" }, }); - const dept = await prisma.department.findFirst({ + const existingDepartment = await prisma.department.findFirst({ where: { collegeId: college.id, name: "ECE" }, }); const eceDepartment = - dept ?? + existingDepartment ?? (await prisma.department.create({ data: { name: "ECE", collegeId: college.id }, })); @@ -139,12 +139,20 @@ async function main() { // Semester 8 intentionally has no regular subjects. for (const batch of eceBatches) { for (let semester = 1; semester <= 7; semester += 1) { + const program = `B.Tech ${batch}`; + const isLegacyEce2Sem7 = batch === "ECE 2" && semester === 7; + + // The original demo seed created "B.Tech ECE" for Sem 7. + // Reuse that record as ECE 2 instead of leaving a duplicate class behind. const existingClass = await prisma.class.findFirst({ where: { departmentId: eceDepartment.id, - program: `B.Tech ${batch}`, academicYear: "2026-27", semester, + OR: [ + { program }, + ...(isLegacyEce2Sem7 ? [{ program: "B.Tech ECE" }] : []), + ], }, }); @@ -153,7 +161,7 @@ async function main() { (await prisma.class.create({ data: { departmentId: eceDepartment.id, - program: `B.Tech ${batch}`, + program, academicYear: "2026-27", year: yearForSemester(semester), semester, @@ -161,12 +169,21 @@ async function main() { }, })); - const existingSection = await prisma.section.findFirst({ - where: { classId: cls.id, name: "A" }, - }); + if (cls.program !== program) { + await prisma.class.update({ + where: { id: cls.id }, + data: { + program, + year: yearForSemester(semester), + proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : cls.proctorId, + }, + }); + } const section = - existingSection ?? + (await prisma.section.findFirst({ + where: { classId: cls.id, name: "A" }, + })) ?? (await prisma.section.create({ data: { classId: cls.id, @@ -175,6 +192,13 @@ async function main() { }, })); + if (batch === "ECE 2" && semester === 7) { + await prisma.section.update({ + where: { id: section.id }, + data: { strength: 65 }, + }); + } + for (const subject of subjectsBySemester[semester]) { const existingSubject = await prisma.subject.findFirst({ where: { sectionId: section.id, code: subject.code }, From bb4ffdb85ea5a09adf36f417f2a410f4f054ead1 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 18:57:01 +0530 Subject: [PATCH 17/80] Add complete ECE batch curriculum seed --- prisma/seed.ts | 143 ++++++++++++++----------------------------------- 1 file changed, 39 insertions(+), 104 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index aacfbd9..5c75915 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,15 +1,11 @@ import { PrismaClient } from "@prisma/client"; import bcrypt from "bcryptjs"; -type SubjectSeed = { - name: string; - code: string; -}; +type SubjectSeed = { name: string; code: string }; const prisma = new PrismaClient(); -// Theory subjects supplied for the ECE curriculum. -// Labs are intentionally excluded for now. +// Theory subjects supplied for the ECE curriculum. Labs are intentionally excluded. const subjectsBySemester: Record = { 1: [ { name: "Environmental Studies", code: "SEM1-ENVIRONMENTAL-STUDIES" }, @@ -63,7 +59,6 @@ const subjectsBySemester: Record = { { name: "DA", code: "DA 338 T" }, ], 7: [ - // The supplied timetable identifies these subjects by these abbreviations only. { name: "MLDA", code: "MLDA" }, { name: "IOT", code: "IOT" }, { name: "PR", code: "PR" }, @@ -89,26 +84,15 @@ async function main() { create: { id: "demo-college", name: "Demo Institute of Technology" }, }); - const existingDepartment = await prisma.department.findFirst({ - where: { collegeId: college.id, name: "ECE" }, - }); - const eceDepartment = - existingDepartment ?? - (await prisma.department.create({ - data: { name: "ECE", collegeId: college.id }, - })); + (await prisma.department.findFirst({ where: { collegeId: college.id, name: "ECE" } })) ?? + (await prisma.department.create({ data: { name: "ECE", collegeId: college.id } })); const passwordHash = await bcrypt.hash("changeme123", 10); const teacher = await prisma.user.upsert({ where: { email: "geetanjali@demo.edu" }, - update: { - name: "Dr. Geetanjali", - passwordHash, - role: "TEACHER", - collegeId: college.id, - }, + update: { name: "Dr. Geetanjali", passwordHash, role: "TEACHER", collegeId: college.id }, create: { name: "Dr. Geetanjali", email: "geetanjali@demo.edu", @@ -120,12 +104,7 @@ async function main() { await prisma.user.upsert({ where: { email: "admin@demo.edu" }, - update: { - name: "College Admin", - passwordHash, - role: "ADMIN", - collegeId: college.id, - }, + update: { name: "College Admin", passwordHash, role: "ADMIN", collegeId: college.id }, create: { name: "College Admin", email: "admin@demo.edu", @@ -135,29 +114,25 @@ async function main() { }, }); - // Create all three ECE batches for Semesters 1-7. + // Create the three ECE batches for Semesters 1-7. // Semester 8 intentionally has no regular subjects. for (const batch of eceBatches) { for (let semester = 1; semester <= 7; semester += 1) { const program = `B.Tech ${batch}`; const isLegacyEce2Sem7 = batch === "ECE 2" && semester === 7; - // The original demo seed created "B.Tech ECE" for Sem 7. - // Reuse that record as ECE 2 instead of leaving a duplicate class behind. - const existingClass = await prisma.class.findFirst({ - where: { - departmentId: eceDepartment.id, - academicYear: "2026-27", - semester, - OR: [ - { program }, - ...(isLegacyEce2Sem7 ? [{ program: "B.Tech ECE" }] : []), - ], - }, - }); - const cls = - existingClass ?? + (await prisma.class.findFirst({ + where: { + departmentId: eceDepartment.id, + academicYear: "2026-27", + semester, + OR: [ + { program }, + ...(isLegacyEce2Sem7 ? [{ program: "B.Tech ECE" }] : []), + ], + }, + })) ?? (await prisma.class.create({ data: { departmentId: eceDepartment.id, @@ -169,7 +144,7 @@ async function main() { }, })); - if (cls.program !== program) { + if (cls.program !== program || cls.year !== yearForSemester(semester)) { await prisma.class.update({ where: { id: cls.id }, data: { @@ -181,9 +156,7 @@ async function main() { } const section = - (await prisma.section.findFirst({ - where: { classId: cls.id, name: "A" }, - })) ?? + (await prisma.section.findFirst({ where: { classId: cls.id, name: "A" } })) ?? (await prisma.section.create({ data: { classId: cls.id, @@ -192,32 +165,24 @@ async function main() { }, })); - if (batch === "ECE 2" && semester === 7) { - await prisma.section.update({ - where: { id: section.id }, - data: { strength: 65 }, - }); + if (batch === "ECE 2" && semester === 7 && section.strength !== 65) { + await prisma.section.update({ where: { id: section.id }, data: { strength: 65 } }); } for (const subject of subjectsBySemester[semester]) { - const existingSubject = await prisma.subject.findFirst({ + const existing = await prisma.subject.findFirst({ where: { sectionId: section.id, code: subject.code }, }); - - if (!existingSubject) { + if (!existing) { await prisma.subject.create({ - data: { - name: subject.name, - code: subject.code, - sectionId: section.id, - }, + data: { name: subject.name, code: subject.code, sectionId: section.id }, }); } } } } - // Keep the existing ECE 2 Sem 7 / Section A demo data and its sheet links. + // Preserve the existing ECE 2 / Sem 7 / Section A demo SheetLinks. const demoClass = await prisma.class.findFirst({ where: { departmentId: eceDepartment.id, @@ -226,65 +191,37 @@ async function main() { semester: 7, }, }); - const demoSection = demoClass ? await prisma.section.findFirst({ where: { classId: demoClass.id, name: "A" } }) : null; if (demoSection) { - await prisma.section.update({ - where: { id: demoSection.id }, - data: { strength: 65 }, - }); - const daSubject = await prisma.subject.findFirst({ where: { sectionId: demoSection.id, code: "DA 338 T" }, }); - if (daSubject) { - const assignment = await prisma.assignment.findFirst({ - where: { teacherId: teacher.id, subjectId: daSubject.id }, + await prisma.assignment.upsert({ + where: { teacherId_subjectId: { teacherId: teacher.id, subjectId: daSubject.id } }, + update: {}, + create: { teacherId: teacher.id, subjectId: daSubject.id }, }); - - if (!assignment) { - await prisma.assignment.create({ - data: { teacherId: teacher.id, subjectId: daSubject.id }, - }); - } - - const subjectSheet = await prisma.sheetLink.findFirst({ + await prisma.sheetLink.upsert({ where: { subjectId: daSubject.id }, + update: {}, + create: { subjectId: daSubject.id, sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U" }, }); - - if (!subjectSheet) { - await prisma.sheetLink.create({ - data: { - subjectId: daSubject.id, - sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U", - }, - }); - } } - - const sectionSheet = await prisma.sheetLink.findFirst({ + await prisma.sheetLink.upsert({ where: { sectionId: demoSection.id }, + update: {}, + create: { sectionId: demoSection.id, sheetId: "1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM" }, }); - - if (!sectionSheet) { - await prisma.sheetLink.create({ - data: { - sectionId: demoSection.id, - sheetId: "1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM", - }, - }); - } } console.log("Seed complete."); console.log("Created/updated ECE 1, ECE 2 and ECE E for Semesters 1-7."); - console.log("Semester 8 intentionally has no subjects."); - console.log("Labs are intentionally excluded."); - console.log("Login as: geetanjali@demo.edu / changeme123"); + console.log("Semester 8 intentionally has no subjects; labs are excluded."); + console.log("Login: geetanjali@demo.edu / changeme123"); console.log("Admin: admin@demo.edu / changeme123"); } @@ -293,6 +230,4 @@ main() console.error(e); process.exit(1); }) - .finally(async () => { - await prisma.$disconnect(); - }); + .finally(async () => prisma.$disconnect()); From a0c545f1a4e8c4b6b76c54a11c1e5c5800f11611 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 19:08:31 +0530 Subject: [PATCH 18/80] Add teacher-scoped subject analysis chooser --- pages/subject-analysis/index.tsx | 157 +++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 pages/subject-analysis/index.tsx diff --git a/pages/subject-analysis/index.tsx b/pages/subject-analysis/index.tsx new file mode 100644 index 0000000..76a2cbe --- /dev/null +++ b/pages/subject-analysis/index.tsx @@ -0,0 +1,157 @@ +import { GetServerSideProps } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../../lib/authOptions"; +import { prisma } from "../../lib/prisma"; +import Link from "next/link"; +import { BookOpen, ArrowLeft, ArrowRight, BarChart3 } from "lucide-react"; + +interface SubjectOption { + id: string; + sectionId: string; + name: string; + code: string; +} + +interface ClassOption { + id: string; + label: string; +} + +interface Props { + teacherName: string; + classes: ClassOption[]; + subjects: SubjectOption[]; +} + +export default function SubjectAnalysisChooser({ teacherName, classes, subjects }: Props) { + const firstClass = classes[0]?.id || ""; + const [selectedClass, setSelectedClass] = require("react").useState(firstClass); + const selectedSubjects = subjects.filter((subject) => subject.sectionId === selectedClass); + + return ( +
+ + +
+
+ + Back to Dashboard + + +
+

Subject Analysis

+

Choose a class and subject

+

+ Select a class you are assigned to, then choose one of your assigned subjects for that class. +

+
+ +
+
+ + +
+ +
+
+ + {selectedSubjects.length} available +
+ + {selectedSubjects.length === 0 ? ( +
+ No subjects are assigned to you for this class. +
+ ) : ( +
+ {selectedSubjects.map((subject) => ( + + + + + + {subject.name} + {subject.code} + + + + ))} +
+ )} +
+
+ +

Signed in as {teacherName}. Only your assigned classes and subjects are shown.

+
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const session = await getServerSession(ctx.req, ctx.res, authOptions); + if (!session?.user) { + return { redirect: { destination: "/login", permanent: false } }; + } + + const userId = (session.user as any).id; + const role = (session.user as any).role; + + const subjectsRaw = await prisma.subject.findMany({ + where: role === "ADMIN" ? {} : { assignments: { some: { teacherId: userId } } }, + include: { + section: { include: { class: { include: { department: true } } } }, + }, + orderBy: [{ section: { class: { semester: "asc" } } }, { name: "asc" }], + }); + + const classMap = new Map(); + for (const subject of subjectsRaw) { + const sectionClass = subject.section.class; + classMap.set(sectionClass.id, { + id: subject.section.id, + label: `${sectionClass.department.name} — ${sectionClass.program}, Sem ${sectionClass.semester}, Section ${subject.section.name}`, + }); + } + + return { + props: { + teacherName: session.user.name || session.user.email, + classes: Array.from(classMap.values()), + subjects: subjectsRaw.map((subject) => ({ + id: subject.id, + sectionId: subject.section.id, + name: subject.name, + code: subject.code, + })), + }, + }; +}; From 404141965e90d3575d1c63b22a5a37304bedf20e Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 19:09:11 +0530 Subject: [PATCH 19/80] Fix subject analysis class scoping --- pages/subject-analysis/index.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pages/subject-analysis/index.tsx b/pages/subject-analysis/index.tsx index 76a2cbe..e25b1dc 100644 --- a/pages/subject-analysis/index.tsx +++ b/pages/subject-analysis/index.tsx @@ -3,6 +3,7 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "../../lib/authOptions"; import { prisma } from "../../lib/prisma"; import Link from "next/link"; +import { useState } from "react"; import { BookOpen, ArrowLeft, ArrowRight, BarChart3 } from "lucide-react"; interface SubjectOption { @@ -24,8 +25,7 @@ interface Props { } export default function SubjectAnalysisChooser({ teacherName, classes, subjects }: Props) { - const firstClass = classes[0]?.id || ""; - const [selectedClass, setSelectedClass] = require("react").useState(firstClass); + const [selectedClass, setSelectedClass] = useState(classes[0]?.id || ""); const selectedSubjects = subjects.filter((subject) => subject.sectionId === selectedClass); return ( @@ -135,10 +135,11 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { const classMap = new Map(); for (const subject of subjectsRaw) { - const sectionClass = subject.section.class; - classMap.set(sectionClass.id, { - id: subject.section.id, - label: `${sectionClass.department.name} — ${sectionClass.program}, Sem ${sectionClass.semester}, Section ${subject.section.name}`, + const section = subject.section; + const sectionClass = section.class; + classMap.set(section.id, { + id: section.id, + label: `${sectionClass.department.name} — ${sectionClass.program}, Sem ${sectionClass.semester}, Section ${section.name}`, }); } From f14de4e55ddb1c9be4e574051007883147b255cb Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 19:09:58 +0530 Subject: [PATCH 20/80] Route dashboard Subject Analysis through teacher chooser --- pages/dashboard.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 9904e84..2229489 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -11,7 +11,6 @@ import { BookOpen, CalendarDays, CheckCircle2, - ClipboardCheck, Clock3, Database, FileText, @@ -184,12 +183,11 @@ export default function Dashboard({ teacherName, sections, subjects }: Props) {
{quickActions.map((action) => { const Icon = action.icon; - const isAnalysis = action.title === "Class Analysis" || action.title === "Subject Analysis"; - const href = isAnalysis - ? action.title === "Class Analysis" - ? sections[0]?.id ? `/section-analysis/${sections[0].id}` : "/section-analysis" - : subjects[0]?.id ? `/subject-analysis/${subjects[0].id}` : "/subject-analysis" - : action.href; + const href = action.title === "Class Analysis" + ? sections[0]?.id ? `/section-analysis/${sections[0].id}` : "/section-analysis" + : action.title === "Subject Analysis" + ? "/subject-analysis" + : action.href; return ( @@ -237,15 +235,14 @@ export default function Dashboard({ teacherName, sections, subjects }: Props) {

Recent Activity

-

Your latest actions in ClassPulse.

+

Your latest updates and actions.

-
-
- {recentActivity.map((activity) => { +
+ {recentActivity.map((activity, index) => { const Icon = activity.icon; return ( -
+

{activity.text}

{activity.time} From 30803abb42a1d1f779fe14212129a2eb80cd2af8 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 19:16:07 +0530 Subject: [PATCH 21/80] test assignment update --- prisma/seed.ts | 237 +------------------------------------------------ 1 file changed, 4 insertions(+), 233 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 5c75915..6e3cd99 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,233 +1,4 @@ -import { PrismaClient } from "@prisma/client"; -import bcrypt from "bcryptjs"; - -type SubjectSeed = { name: string; code: string }; - -const prisma = new PrismaClient(); - -// Theory subjects supplied for the ECE curriculum. Labs are intentionally excluded. -const subjectsBySemester: Record = { - 1: [ - { name: "Environmental Studies", code: "SEM1-ENVIRONMENTAL-STUDIES" }, - { name: "Engineering Graphics-I", code: "SEM1-ENGINEERING-GRAPHICS-I" }, - { name: "Manufacturing Process", code: "SEM1-MANUFACTURING-PROCESS" }, - { name: "Communications Skills", code: "SEM1-COMMUNICATIONS-SKILLS" }, - { name: "Applied Mathematics-I", code: "SEM1-APPLIED-MATHEMATICS-I" }, - { name: "Applied Physics-I", code: "SEM1-APPLIED-PHYSICS-I" }, - { name: "Programming in C", code: "SEM1-PROGRAMMING-IN-C" }, - ], - 2: [ - { name: "Applied Chemistry", code: "SEM2-APPLIED-CHEMISTRY" }, - { name: "Applied Physics-II", code: "SEM2-APPLIED-PHYSICS-II" }, - { name: "Electrical Science", code: "SEM2-ELECTRICAL-SCIENCE" }, - { name: "Applied Mathematics-II", code: "SEM2-APPLIED-MATHEMATICS-II" }, - { name: "Engineering Mechanics", code: "SEM2-ENGINEERING-MECHANICS" }, - { name: "Indian Constitution", code: "SEM2-INDIAN-CONSTITUTION" }, - { name: "Human Values and Ethics", code: "SEM2-HUMAN-VALUES-AND-ETHICS" }, - ], - 3: [ - { name: "Computational Methods", code: "SEM3-COMPUTATIONAL-METHODS" }, - { name: "Indian Knowledge System", code: "SEM3-INDIAN-KNOWLEDGE-SYSTEM" }, - { name: "Signal and System", code: "SEM3-SIGNAL-AND-SYSTEM" }, - { name: "Digital Logic and Computer Design", code: "SEM3-DIGITAL-LOGIC-AND-COMPUTER-DESIGN" }, - { name: "Analog Communications", code: "SEM3-ANALOG-COMMUNICATIONS" }, - { name: "Analog Electronics-I", code: "SEM3-ANALOG-ELECTRONICS-I" }, - ], - 4: [ - { name: "Probability, Statistics and Linear Programming", code: "SEM4-PSLP" }, - { name: "Technical Writing", code: "SEM4-TECHNICAL-WRITING" }, - { name: "Network Analysis and Synthesis", code: "SEM4-NAS" }, - { name: "Microprocessors and Microcontrollers", code: "SEM4-MICROPROCESSORS-AND-MICROCONTROLLERS" }, - { name: "Digital Communications", code: "SEM4-DIGITAL-COMMUNICATIONS" }, - { name: "Analog Electronics-II", code: "SEM4-ANALOG-ELECTRONICS-II" }, - { name: "Electromagnetic Field Theory", code: "SEM4-EMFT" }, - ], - 5: [ - { name: "Economics for Engineers", code: "SEM5-ECONOMICS-FOR-ENGINEERS" }, - { name: "Digital Signal Processing", code: "SEM5-DSP" }, - { name: "Microelectronics", code: "SEM5-MICROELECTRONICS" }, - { name: "Introduction to Control Systems", code: "SEM5-INTRODUCTION-TO-CONTROL-SYSTEMS" }, - { name: "Transmission Lines, Waveguides and Antenna Design", code: "SEM5-TLWAD" }, - { name: "Data Communication and Networking", code: "SEM5-DCN" }, - ], - 6: [ - { name: "PME", code: "MS-302" }, - { name: "AI", code: "ECE 318 T" }, - { name: "OCSN", code: "ECE 326 T" }, - { name: "ML", code: "ECE 350 T" }, - { name: "SSM&DA", code: "DA 304 T" }, - { name: "DA", code: "DA 338 T" }, - ], - 7: [ - { name: "MLDA", code: "MLDA" }, - { name: "IOT", code: "IOT" }, - { name: "PR", code: "PR" }, - { name: "USL", code: "USL" }, - { name: "SL", code: "SL" }, - { name: "PE", code: "PE" }, - ], -}; - -const eceBatches = ["ECE 1", "ECE 2", "ECE E"]; - -function yearForSemester(semester: number) { - if (semester <= 2) return "1st Year"; - if (semester <= 4) return "2nd Year"; - if (semester <= 6) return "3rd Year"; - return "4th Year"; -} - -async function main() { - const college = await prisma.college.upsert({ - where: { id: "demo-college" }, - update: { name: "Demo Institute of Technology" }, - create: { id: "demo-college", name: "Demo Institute of Technology" }, - }); - - const eceDepartment = - (await prisma.department.findFirst({ where: { collegeId: college.id, name: "ECE" } })) ?? - (await prisma.department.create({ data: { name: "ECE", collegeId: college.id } })); - - const passwordHash = await bcrypt.hash("changeme123", 10); - - const teacher = await prisma.user.upsert({ - where: { email: "geetanjali@demo.edu" }, - update: { name: "Dr. Geetanjali", passwordHash, role: "TEACHER", collegeId: college.id }, - create: { - name: "Dr. Geetanjali", - email: "geetanjali@demo.edu", - passwordHash, - role: "TEACHER", - collegeId: college.id, - }, - }); - - await prisma.user.upsert({ - where: { email: "admin@demo.edu" }, - update: { name: "College Admin", passwordHash, role: "ADMIN", collegeId: college.id }, - create: { - name: "College Admin", - email: "admin@demo.edu", - passwordHash, - role: "ADMIN", - collegeId: college.id, - }, - }); - - // Create the three ECE batches for Semesters 1-7. - // Semester 8 intentionally has no regular subjects. - for (const batch of eceBatches) { - for (let semester = 1; semester <= 7; semester += 1) { - const program = `B.Tech ${batch}`; - const isLegacyEce2Sem7 = batch === "ECE 2" && semester === 7; - - const cls = - (await prisma.class.findFirst({ - where: { - departmentId: eceDepartment.id, - academicYear: "2026-27", - semester, - OR: [ - { program }, - ...(isLegacyEce2Sem7 ? [{ program: "B.Tech ECE" }] : []), - ], - }, - })) ?? - (await prisma.class.create({ - data: { - departmentId: eceDepartment.id, - program, - academicYear: "2026-27", - year: yearForSemester(semester), - semester, - proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : null, - }, - })); - - if (cls.program !== program || cls.year !== yearForSemester(semester)) { - await prisma.class.update({ - where: { id: cls.id }, - data: { - program, - year: yearForSemester(semester), - proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : cls.proctorId, - }, - }); - } - - const section = - (await prisma.section.findFirst({ where: { classId: cls.id, name: "A" } })) ?? - (await prisma.section.create({ - data: { - classId: cls.id, - name: "A", - strength: batch === "ECE 2" && semester === 7 ? 65 : 0, - }, - })); - - if (batch === "ECE 2" && semester === 7 && section.strength !== 65) { - await prisma.section.update({ where: { id: section.id }, data: { strength: 65 } }); - } - - for (const subject of subjectsBySemester[semester]) { - const existing = await prisma.subject.findFirst({ - where: { sectionId: section.id, code: subject.code }, - }); - if (!existing) { - await prisma.subject.create({ - data: { name: subject.name, code: subject.code, sectionId: section.id }, - }); - } - } - } - } - - // Preserve the existing ECE 2 / Sem 7 / Section A demo SheetLinks. - const demoClass = await prisma.class.findFirst({ - where: { - departmentId: eceDepartment.id, - program: "B.Tech ECE 2", - academicYear: "2026-27", - semester: 7, - }, - }); - const demoSection = demoClass - ? await prisma.section.findFirst({ where: { classId: demoClass.id, name: "A" } }) - : null; - - if (demoSection) { - const daSubject = await prisma.subject.findFirst({ - where: { sectionId: demoSection.id, code: "DA 338 T" }, - }); - if (daSubject) { - await prisma.assignment.upsert({ - where: { teacherId_subjectId: { teacherId: teacher.id, subjectId: daSubject.id } }, - update: {}, - create: { teacherId: teacher.id, subjectId: daSubject.id }, - }); - await prisma.sheetLink.upsert({ - where: { subjectId: daSubject.id }, - update: {}, - create: { subjectId: daSubject.id, sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U" }, - }); - } - await prisma.sheetLink.upsert({ - where: { sectionId: demoSection.id }, - update: {}, - create: { sectionId: demoSection.id, sheetId: "1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM" }, - }); - } - - console.log("Seed complete."); - console.log("Created/updated ECE 1, ECE 2 and ECE E for Semesters 1-7."); - console.log("Semester 8 intentionally has no subjects; labs are excluded."); - console.log("Login: geetanjali@demo.edu / changeme123"); - console.log("Admin: admin@demo.edu / changeme123"); -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => prisma.$disconnect()); +// ASSIGNMENTS +// Dr. Geetanjali is assigned to six ECE classes from Semesters 1, 3, 5 and 7. +// Selected classes: ECE 1 Sem 1, ECE E Sem 1, ECE 2 Sem 3, +// ECE 1 Sem 5, ECE E Sem 5, ECE 2 Sem 7. From 80ae7f451be4f1d628b86b5fc43811bdb1cd02e2 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 19:16:43 +0530 Subject: [PATCH 22/80] Assign Geetanjali to six ECE classes --- prisma/seed.ts | 275 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 271 insertions(+), 4 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 6e3cd99..b4dd1c1 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,271 @@ -// ASSIGNMENTS -// Dr. Geetanjali is assigned to six ECE classes from Semesters 1, 3, 5 and 7. -// Selected classes: ECE 1 Sem 1, ECE E Sem 1, ECE 2 Sem 3, -// ECE 1 Sem 5, ECE E Sem 5, ECE 2 Sem 7. +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +type SubjectSeed = { name: string; code: string }; + +const prisma = new PrismaClient(); + +const subjectsBySemester: Record = { + 1: [ + { name: "Environmental Studies", code: "SEM1-ENVIRONMENTAL-STUDIES" }, + { name: "Engineering Graphics-I", code: "SEM1-ENGINEERING-GRAPHICS-I" }, + { name: "Manufacturing Process", code: "SEM1-MANUFACTURING-PROCESS" }, + { name: "Communications Skills", code: "SEM1-COMMUNICATIONS-SKILLS" }, + { name: "Applied Mathematics-I", code: "SEM1-APPLIED-MATHEMATICS-I" }, + { name: "Applied Physics-I", code: "SEM1-APPLIED-PHYSICS-I" }, + { name: "Programming in C", code: "SEM1-PROGRAMMING-IN-C" }, + ], + 2: [ + { name: "Applied Chemistry", code: "SEM2-APPLIED-CHEMISTRY" }, + { name: "Applied Physics-II", code: "SEM2-APPLIED-PHYSICS-II" }, + { name: "Electrical Science", code: "SEM2-ELECTRICAL-SCIENCE" }, + { name: "Applied Mathematics-II", code: "SEM2-APPLIED-MATHEMATICS-II" }, + { name: "Engineering Mechanics", code: "SEM2-ENGINEERING-MECHANICS" }, + { name: "Indian Constitution", code: "SEM2-INDIAN-CONSTITUTION" }, + { name: "Human Values and Ethics", code: "SEM2-HUMAN-VALUES-AND-ETHICS" }, + ], + 3: [ + { name: "Computational Methods", code: "SEM3-COMPUTATIONAL-METHODS" }, + { name: "Indian Knowledge System", code: "SEM3-INDIAN-KNOWLEDGE-SYSTEM" }, + { name: "Signal and System", code: "SEM3-SIGNAL-AND-SYSTEM" }, + { name: "Digital Logic and Computer Design", code: "SEM3-DIGITAL-LOGIC-AND-COMPUTER-DESIGN" }, + { name: "Analog Communications", code: "SEM3-ANALOG-COMMUNICATIONS" }, + { name: "Analog Electronics-I", code: "SEM3-ANALOG-ELECTRONICS-I" }, + ], + 4: [ + { name: "Probability, Statistics and Linear Programming", code: "SEM4-PSLP" }, + { name: "Technical Writing", code: "SEM4-TECHNICAL-WRITING" }, + { name: "Network Analysis and Synthesis", code: "SEM4-NAS" }, + { name: "Microprocessors and Microcontrollers", code: "SEM4-MICROPROCESSORS-AND-MICROCONTROLLERS" }, + { name: "Digital Communications", code: "SEM4-DIGITAL-COMMUNICATIONS" }, + { name: "Analog Electronics-II", code: "SEM4-ANALOG-ELECTRONICS-II" }, + { name: "Electromagnetic Field Theory", code: "SEM4-EMFT" }, + ], + 5: [ + { name: "Economics for Engineers", code: "SEM5-ECONOMICS-FOR-ENGINEERS" }, + { name: "Digital Signal Processing", code: "SEM5-DSP" }, + { name: "Microelectronics", code: "SEM5-MICROELECTRONICS" }, + { name: "Introduction to Control Systems", code: "SEM5-INTRODUCTION-TO-CONTROL-SYSTEMS" }, + { name: "Transmission Lines, Waveguides and Antenna Design", code: "SEM5-TLWAD" }, + { name: "Data Communication and Networking", code: "SEM5-DCN" }, + ], + 6: [ + { name: "PME", code: "MS-302" }, + { name: "AI", code: "ECE 318 T" }, + { name: "OCSN", code: "ECE 326 T" }, + { name: "ML", code: "ECE 350 T" }, + { name: "SSM&DA", code: "DA 304 T" }, + { name: "DA", code: "DA 338 T" }, + ], + 7: [ + { name: "MLDA", code: "MLDA" }, + { name: "IOT", code: "IOT" }, + { name: "PR", code: "PR" }, + { name: "USL", code: "USL" }, + { name: "SL", code: "SL" }, + { name: "PE", code: "PE" }, + ], +}; + +const eceBatches = ["ECE 1", "ECE 2", "ECE E"]; + +function yearForSemester(semester: number) { + if (semester <= 2) return "1st Year"; + if (semester <= 4) return "2nd Year"; + if (semester <= 6) return "3rd Year"; + return "4th Year"; +} + +async function main() { + const college = await prisma.college.upsert({ + where: { id: "demo-college" }, + update: { name: "Demo Institute of Technology" }, + create: { id: "demo-college", name: "Demo Institute of Technology" }, + }); + + const eceDepartment = + (await prisma.department.findFirst({ where: { collegeId: college.id, name: "ECE" } })) ?? + (await prisma.department.create({ data: { name: "ECE", collegeId: college.id } })); + + const passwordHash = await bcrypt.hash("changeme123", 10); + + const teacher = await prisma.user.upsert({ + where: { email: "geetanjali@demo.edu" }, + update: { name: "Dr. Geetanjali", passwordHash, role: "TEACHER", collegeId: college.id }, + create: { + name: "Dr. Geetanjali", + email: "geetanjali@demo.edu", + passwordHash, + role: "TEACHER", + collegeId: college.id, + }, + }); + + await prisma.user.upsert({ + where: { email: "admin@demo.edu" }, + update: { name: "College Admin", passwordHash, role: "ADMIN", collegeId: college.id }, + create: { + name: "College Admin", + email: "admin@demo.edu", + passwordHash, + role: "ADMIN", + collegeId: college.id, + }, + }); + + for (const batch of eceBatches) { + for (let semester = 1; semester <= 7; semester += 1) { + const program = `B.Tech ${batch}`; + const isLegacyEce2Sem7 = batch === "ECE 2" && semester === 7; + + const cls = + (await prisma.class.findFirst({ + where: { + departmentId: eceDepartment.id, + academicYear: "2026-27", + semester, + OR: [ + { program }, + ...(isLegacyEce2Sem7 ? [{ program: "B.Tech ECE" }] : []), + ], + }, + })) ?? + (await prisma.class.create({ + data: { + departmentId: eceDepartment.id, + program, + academicYear: "2026-27", + year: yearForSemester(semester), + semester, + proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : null, + }, + })); + + if (cls.program !== program || cls.year !== yearForSemester(semester)) { + await prisma.class.update({ + where: { id: cls.id }, + data: { + program, + year: yearForSemester(semester), + proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : cls.proctorId, + }, + }); + } + + const section = + (await prisma.section.findFirst({ where: { classId: cls.id, name: "A" } })) ?? + (await prisma.section.create({ + data: { + classId: cls.id, + name: "A", + strength: batch === "ECE 2" && semester === 7 ? 65 : 0, + }, + })); + + if (batch === "ECE 2" && semester === 7 && section.strength !== 65) { + await prisma.section.update({ where: { id: section.id }, data: { strength: 65 } }); + } + + for (const subject of subjectsBySemester[semester]) { + const existing = await prisma.subject.findFirst({ + where: { sectionId: section.id, code: subject.code }, + }); + if (!existing) { + await prisma.subject.create({ + data: { name: subject.name, code: subject.code, sectionId: section.id }, + }); + } + } + } + } + + // Dr. Geetanjali gets exactly six classes from Semesters 1, 3, 5 and 7. + // One subject is assigned in each selected class for the class/subject selector. + const teacherClassAssignments = [ + { batch: "ECE 1", semester: 1, subjectIndex: 0 }, + { batch: "ECE E", semester: 1, subjectIndex: 4 }, + { batch: "ECE 2", semester: 3, subjectIndex: 3 }, + { batch: "ECE 1", semester: 5, subjectIndex: 1 }, + { batch: "ECE E", semester: 5, subjectIndex: 4 }, + { batch: "ECE 2", semester: 7, subjectIndex: 5 }, + ]; + + for (const item of teacherClassAssignments) { + const cls = await prisma.class.findFirst({ + where: { + departmentId: eceDepartment.id, + program: `B.Tech ${item.batch}`, + academicYear: "2026-27", + semester: item.semester, + }, + }); + if (!cls) continue; + + const section = await prisma.section.findFirst({ + where: { classId: cls.id, name: "A" }, + }); + const subjectSeed = subjectsBySemester[item.semester][item.subjectIndex]; + if (!section || !subjectSeed) continue; + + const subject = await prisma.subject.findFirst({ + where: { sectionId: section.id, code: subjectSeed.code }, + }); + if (!subject) continue; + + await prisma.assignment.upsert({ + where: { teacherId_subjectId: { teacherId: teacher.id, subjectId: subject.id } }, + update: {}, + create: { teacherId: teacher.id, subjectId: subject.id }, + }); + } + + // Preserve the existing ECE 2 / Sem 7 / Section A demo SheetLinks. + const demoClass = await prisma.class.findFirst({ + where: { + departmentId: eceDepartment.id, + program: "B.Tech ECE 2", + academicYear: "2026-27", + semester: 7, + }, + }); + const demoSection = demoClass + ? await prisma.section.findFirst({ where: { classId: demoClass.id, name: "A" } }) + : null; + + if (demoSection) { + const daSubject = await prisma.subject.findFirst({ + where: { sectionId: demoSection.id, code: "DA 338 T" }, + }); + if (daSubject) { + await prisma.assignment.upsert({ + where: { teacherId_subjectId: { teacherId: teacher.id, subjectId: daSubject.id } }, + update: {}, + create: { teacherId: teacher.id, subjectId: daSubject.id }, + }); + await prisma.sheetLink.upsert({ + where: { subjectId: daSubject.id }, + update: {}, + create: { subjectId: daSubject.id, sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U" }, + }); + } + await prisma.sheetLink.upsert({ + where: { sectionId: demoSection.id }, + update: {}, + create: { sectionId: demoSection.id, sheetId: "1DM3brNxfdWl0I4r9cbffjh_PlXaWaTsM" }, + }); + } + + console.log("Seed complete."); + console.log("Created/updated ECE 1, ECE 2 and ECE E for Semesters 1-7."); + console.log("Assigned Dr. Geetanjali to 6 classes from Semesters 1, 3, 5 and 7."); + console.log("Semester 8 intentionally has no subjects; labs are excluded."); + console.log("Login: geetanjali@demo.edu / changeme123"); + console.log("Admin: admin@demo.edu / changeme123"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => prisma.$disconnect()); From a80706130e5436679600712e6c39d3ad2dd9cc77 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 20:04:43 +0530 Subject: [PATCH 23/80] Add theory and lab subject types --- prisma/schema.prisma | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9959b4c..47ca37c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,11 @@ enum Role { TEACHER } +enum SubjectType { + THEORY + LAB +} + model College { id String @id @default(cuid()) name String @@ -89,6 +94,7 @@ model Subject { id String @id @default(cuid()) name String code String + type SubjectType @default(THEORY) section Section @relation(fields: [sectionId], references: [id]) sectionId String sheetLink SheetLink? From e43dbccbba8857b6f44fa54743788393b9e986f1 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 20:05:49 +0530 Subject: [PATCH 24/80] Seed ECE faculty and theory/lab subjects --- prisma/seed.ts | 241 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 168 insertions(+), 73 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index b4dd1c1..c60d988 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,72 +1,141 @@ import { PrismaClient } from "@prisma/client"; import bcrypt from "bcryptjs"; -type SubjectSeed = { name: string; code: string }; +type SubjectSeed = { + name: string; + code: string; + type: "THEORY" | "LAB"; +}; const prisma = new PrismaClient(); const subjectsBySemester: Record = { 1: [ - { name: "Environmental Studies", code: "SEM1-ENVIRONMENTAL-STUDIES" }, - { name: "Engineering Graphics-I", code: "SEM1-ENGINEERING-GRAPHICS-I" }, - { name: "Manufacturing Process", code: "SEM1-MANUFACTURING-PROCESS" }, - { name: "Communications Skills", code: "SEM1-COMMUNICATIONS-SKILLS" }, - { name: "Applied Mathematics-I", code: "SEM1-APPLIED-MATHEMATICS-I" }, - { name: "Applied Physics-I", code: "SEM1-APPLIED-PHYSICS-I" }, - { name: "Programming in C", code: "SEM1-PROGRAMMING-IN-C" }, + { name: "Environmental Studies", code: "SEM1-ENVIRONMENTAL-STUDIES", type: "THEORY" }, + { name: "Engineering Graphics-I", code: "SEM1-ENGINEERING-GRAPHICS-I", type: "THEORY" }, + { name: "Manufacturing Process", code: "SEM1-MANUFACTURING-PROCESS", type: "THEORY" }, + { name: "Communications Skills", code: "SEM1-COMMUNICATIONS-SKILLS", type: "THEORY" }, + { name: "Applied Mathematics-I", code: "SEM1-APPLIED-MATHEMATICS-I", type: "THEORY" }, + { name: "Applied Physics-I", code: "SEM1-APPLIED-PHYSICS-I", type: "THEORY" }, + { name: "Programming in C", code: "SEM1-PROGRAMMING-IN-C", type: "THEORY" }, + { name: "Environmental Studies Lab", code: "SEM1-ENVIRONMENTAL-STUDIES-LAB", type: "LAB" }, + { name: "Programming in C Lab", code: "SEM1-PROGRAMMING-IN-C-LAB", type: "LAB" }, + { name: "Physics-I Lab", code: "SEM1-PHYSICS-I-LAB", type: "LAB" }, ], 2: [ - { name: "Applied Chemistry", code: "SEM2-APPLIED-CHEMISTRY" }, - { name: "Applied Physics-II", code: "SEM2-APPLIED-PHYSICS-II" }, - { name: "Electrical Science", code: "SEM2-ELECTRICAL-SCIENCE" }, - { name: "Applied Mathematics-II", code: "SEM2-APPLIED-MATHEMATICS-II" }, - { name: "Engineering Mechanics", code: "SEM2-ENGINEERING-MECHANICS" }, - { name: "Indian Constitution", code: "SEM2-INDIAN-CONSTITUTION" }, - { name: "Human Values and Ethics", code: "SEM2-HUMAN-VALUES-AND-ETHICS" }, + { name: "Applied Chemistry", code: "SEM2-APPLIED-CHEMISTRY", type: "THEORY" }, + { name: "Applied Physics-II", code: "SEM2-APPLIED-PHYSICS-II", type: "THEORY" }, + { name: "Electrical Science", code: "SEM2-ELECTRICAL-SCIENCE", type: "THEORY" }, + { name: "Applied Mathematics-II", code: "SEM2-APPLIED-MATHEMATICS-II", type: "THEORY" }, + { name: "Engineering Mechanics", code: "SEM2-ENGINEERING-MECHANICS", type: "THEORY" }, + { name: "Indian Constitution", code: "SEM2-INDIAN-CONSTITUTION", type: "THEORY" }, + { name: "Human Values and Ethics", code: "SEM2-HUMAN-VALUES-AND-ETHICS", type: "THEORY" }, + { name: "Physics-III Lab", code: "SEM2-PHYSICS-III-LAB", type: "LAB" }, + { name: "Applied Chemistry Lab", code: "SEM2-APPLIED-CHEMISTRY-LAB", type: "LAB" }, + { name: "Engineering Graphics-II", code: "SEM2-ENGINEERING-GRAPHICS-II", type: "LAB" }, + { name: "Electrical Science Lab", code: "SEM2-ELECTRICAL-SCIENCE-LAB", type: "LAB" }, + { name: "Workshop Practice", code: "SEM2-WORKSHOP-PRACTICE", type: "LAB" }, ], 3: [ - { name: "Computational Methods", code: "SEM3-COMPUTATIONAL-METHODS" }, - { name: "Indian Knowledge System", code: "SEM3-INDIAN-KNOWLEDGE-SYSTEM" }, - { name: "Signal and System", code: "SEM3-SIGNAL-AND-SYSTEM" }, - { name: "Digital Logic and Computer Design", code: "SEM3-DIGITAL-LOGIC-AND-COMPUTER-DESIGN" }, - { name: "Analog Communications", code: "SEM3-ANALOG-COMMUNICATIONS" }, - { name: "Analog Electronics-I", code: "SEM3-ANALOG-ELECTRONICS-I" }, + { name: "Computational Methods", code: "SEM3-COMPUTATIONAL-METHODS", type: "THEORY" }, + { name: "Indian Knowledge System", code: "SEM3-INDIAN-KNOWLEDGE-SYSTEM", type: "THEORY" }, + { name: "Signal and System", code: "SEM3-SIGNAL-AND-SYSTEM", type: "THEORY" }, + { name: "Digital Logic and Computer Design", code: "SEM3-DIGITAL-LOGIC-AND-COMPUTER-DESIGN", type: "THEORY" }, + { name: "Analog Communications", code: "SEM3-ANALOG-COMMUNICATIONS", type: "THEORY" }, + { name: "Analog Electronics-I", code: "SEM3-ANALOG-ELECTRONICS-I", type: "THEORY" }, + { name: "Computational Methods Lab", code: "SEM3-COMPUTATIONAL-METHODS-LAB", type: "LAB" }, + { name: "Digital Logic and Computer Design Lab", code: "SEM3-DIGITAL-LOGIC-AND-COMPUTER-DESIGN-LAB", type: "LAB" }, + { name: "Analog Communications Lab", code: "SEM3-ANALOG-COMMUNICATIONS-LAB", type: "LAB" }, + { name: "Analog Electronics-I Lab", code: "SEM3-ANALOG-ELECTRONICS-I-LAB", type: "LAB" }, + { name: "Signal and System Lab", code: "SEM3-SIGNAL-AND-SYSTEM-LAB", type: "LAB" }, ], 4: [ - { name: "Probability, Statistics and Linear Programming", code: "SEM4-PSLP" }, - { name: "Technical Writing", code: "SEM4-TECHNICAL-WRITING" }, - { name: "Network Analysis and Synthesis", code: "SEM4-NAS" }, - { name: "Microprocessors and Microcontrollers", code: "SEM4-MICROPROCESSORS-AND-MICROCONTROLLERS" }, - { name: "Digital Communications", code: "SEM4-DIGITAL-COMMUNICATIONS" }, - { name: "Analog Electronics-II", code: "SEM4-ANALOG-ELECTRONICS-II" }, - { name: "Electromagnetic Field Theory", code: "SEM4-EMFT" }, + { name: "Probability, Statistics and Linear Programming", code: "SEM4-PSLP", type: "THEORY" }, + { name: "Technical Writing", code: "SEM4-TECHNICAL-WRITING", type: "THEORY" }, + { name: "Network Analysis and Synthesis", code: "SEM4-NAS", type: "THEORY" }, + { name: "Microprocessors and Microcontrollers", code: "SEM4-MICROPROCESSORS-AND-MICROCONTROLLERS", type: "THEORY" }, + { name: "Digital Communications", code: "SEM4-DIGITAL-COMMUNICATIONS", type: "THEORY" }, + { name: "Analog Electronics-II", code: "SEM4-ANALOG-ELECTRONICS-II", type: "THEORY" }, + { name: "Electromagnetic Field Theory", code: "SEM4-EMFT", type: "THEORY" }, + { name: "Probability, Statistics and Linear Programming Lab", code: "SEM4-PSLP-LAB", type: "LAB" }, + { name: "Microprocessors and Microcontrollers Lab", code: "SEM4-MPMC-LAB", type: "LAB" }, + { name: "Digital Communications Lab", code: "SEM4-DIGITAL-COMMUNICATIONS-LAB", type: "LAB" }, + { name: "Analog Electronics-II Lab", code: "SEM4-ANALOG-ELECTRONICS-II-LAB", type: "LAB" }, + { name: "Network Analysis and Synthesis Lab", code: "SEM4-NAS-LAB", type: "LAB" }, ], 5: [ - { name: "Economics for Engineers", code: "SEM5-ECONOMICS-FOR-ENGINEERS" }, - { name: "Digital Signal Processing", code: "SEM5-DSP" }, - { name: "Microelectronics", code: "SEM5-MICROELECTRONICS" }, - { name: "Introduction to Control Systems", code: "SEM5-INTRODUCTION-TO-CONTROL-SYSTEMS" }, - { name: "Transmission Lines, Waveguides and Antenna Design", code: "SEM5-TLWAD" }, - { name: "Data Communication and Networking", code: "SEM5-DCN" }, + { name: "Economics for Engineers", code: "SEM5-ECONOMICS-FOR-ENGINEERS", type: "THEORY" }, + { name: "Digital Signal Processing", code: "SEM5-DSP", type: "THEORY" }, + { name: "Microelectronics", code: "SEM5-MICROELECTRONICS", type: "THEORY" }, + { name: "Introduction to Control Systems", code: "SEM5-INTRODUCTION-TO-CONTROL-SYSTEMS", type: "THEORY" }, + { name: "Transmission Lines, Waveguides and Antenna Design", code: "SEM5-TLWAD", type: "THEORY" }, + { name: "Data Communication and Networking", code: "SEM5-DCN", type: "THEORY" }, + { name: "Digital Signal Processing Lab", code: "SEM5-DSP-LAB", type: "LAB" }, + { name: "Microelectronics Lab", code: "SEM5-MICROELECTRONICS-LAB", type: "LAB" }, + { name: "Introduction to Control Systems Lab", code: "SEM5-INTRODUCTION-TO-CONTROL-SYSTEMS-LAB", type: "LAB" }, + { name: "Transmission Lines, Waveguides and Antenna Design Lab", code: "SEM5-TLWAD-LAB", type: "LAB" }, + { name: "Data Communication and Networking Lab", code: "SEM5-DCN-LAB", type: "LAB" }, ], 6: [ - { name: "PME", code: "MS-302" }, - { name: "AI", code: "ECE 318 T" }, - { name: "OCSN", code: "ECE 326 T" }, - { name: "ML", code: "ECE 350 T" }, - { name: "SSM&DA", code: "DA 304 T" }, - { name: "DA", code: "DA 338 T" }, + { name: "Principles of Management for Engineers", code: "SEM6-PME", type: "THEORY" }, + { name: "Universal Human Values", code: "SEM6-UHV", type: "THEORY" }, + { name: "Statistics, Statistical Modelling & Data Analytics", code: "DA 304 T", type: "THEORY" }, + { name: "Artificial Intelligence", code: "ECE 318 T", type: "THEORY" }, + { name: "Optical Communication Systems and Network", code: "ECE 326 T", type: "THEORY" }, + { name: "Data Analytics", code: "DA 338 T", type: "THEORY" }, + { name: "Machine Learning", code: "ECE 350 T", type: "THEORY" }, + { name: "Statistics, Statistical Modelling & Data Analytics Lab", code: "DA 304 P", type: "LAB" }, + { name: "Artificial Intelligence Lab", code: "ECE 318 P", type: "LAB" }, + { name: "Optical Communication Systems and Networks Lab", code: "ECE 326 P", type: "LAB" }, + { name: "Data Analytics Lab", code: "DA 338 P", type: "LAB" }, + { name: "Machine Learning Lab", code: "ECE 350 P", type: "LAB" }, ], 7: [ - { name: "MLDA", code: "MLDA" }, - { name: "IOT", code: "IOT" }, - { name: "PR", code: "PR" }, - { name: "USL", code: "USL" }, - { name: "SL", code: "SL" }, - { name: "PE", code: "PE" }, + { name: "Machine Learning and Data Analytics Frameworks", code: "MLDA", type: "THEORY" }, + { name: "Internet of Things", code: "IOT", type: "THEORY" }, + { name: "Pattern Recognition", code: "PR", type: "THEORY" }, + { name: "Principles of Entrepreneurship", code: "PE", type: "THEORY" }, + { name: "Unsupervised Learning", code: "USL", type: "THEORY" }, + { name: "Supervised and Deep Learning", code: "SL", type: "THEORY" }, + { name: "Unsupervised Learning Lab", code: "USL-LAB", type: "LAB" }, + { name: "Supervised and Deep Learning Lab", code: "SL-LAB", type: "LAB" }, + { name: "Machine Learning and Data Analytics Frameworks Lab", code: "MLDA-LAB", type: "LAB" }, + { name: "Internet of Things Lab", code: "IOT-LAB", type: "LAB" }, + { name: "Pattern Recognition Lab", code: "PR-LAB", type: "LAB" }, ], }; +const teacherSeeds = [ + { name: "Dr. Sudesh Pahal", email: "sudesh.pahal@demo.edu" }, + { name: "Dr. Archana Balyan", email: "archana.balyan@demo.edu" }, + { name: "Dr. Pardeep Sangwan", email: "pardeep.sangwan@demo.edu" }, + { name: "Dr. Puneet Azad", email: "puneet.azad@demo.edu" }, + { name: "Dr. Neeru Rathi", email: "neeru.rathi@demo.edu" }, + { name: "Dr. Meena Rao", email: "meena.rao@demo.edu" }, + { name: "Dr. Shafali Madan Arora", email: "shafali.arora@demo.edu" }, + { name: "Dr. Richa Gupta", email: "richa.gupta@demo.edu" }, + { name: "Dr. Aman Kumari Dahiya", email: "aman.dahiya@demo.edu" }, + { name: "Dr. Dinesh Sheoran", email: "dinesh.sheoran@demo.edu" }, + { name: "Mr. Deepak Goyal", email: "deepak.goyal@demo.edu" }, + { name: "Dr. Deepti Deshwal", email: "deepti.deshwal@demo.edu" }, + { name: "Ms. Neelam Nehra", email: "neelam.nehra@demo.edu" }, + { name: "Dr. Geetanjali Sharma", email: "geetanjali@demo.edu" }, + { name: "Dr. Sakshi Rajput", email: "sakshi.rajput@demo.edu" }, + { name: "Dr. Nishtha", email: "nishtha@demo.edu" }, + { name: "Dr. Neetu", email: "neetu@demo.edu" }, + { name: "Dr. Upma Singh", email: "upma.singh@demo.edu" }, + { name: "Ms. Jasmine Chhikara", email: "jasmine.chhikara@demo.edu" }, + { name: "Ms. Prinkle Talan", email: "prinkle.talan@demo.edu" }, + { name: "Ms. Garima", email: "garima@demo.edu" }, + { name: "Ms. Vishakha Tomar", email: "vishakha.tomar@demo.edu" }, + { name: "Dr. Suman Lata", email: "suman.lata@demo.edu" }, + { name: "Ms. Anjali Balyan", email: "anjali.balyan@demo.edu" }, + { name: "Dr. Neelam Barak", email: "neelam.barak@demo.edu" }, + { name: "Ms. Himani", email: "himani@demo.edu" }, + { name: "Ms. Sonia", email: "sonia@demo.edu" }, + { name: "Ms. Neha", email: "neha@demo.edu" }, +]; + const eceBatches = ["ECE 1", "ECE 2", "ECE E"]; function yearForSemester(semester: number) { @@ -89,17 +158,27 @@ async function main() { const passwordHash = await bcrypt.hash("changeme123", 10); - const teacher = await prisma.user.upsert({ - where: { email: "geetanjali@demo.edu" }, - update: { name: "Dr. Geetanjali", passwordHash, role: "TEACHER", collegeId: college.id }, - create: { - name: "Dr. Geetanjali", - email: "geetanjali@demo.edu", - passwordHash, - role: "TEACHER", - collegeId: college.id, - }, - }); + const teachers = new Map(); + for (const teacherSeed of teacherSeeds) { + const teacher = await prisma.user.upsert({ + where: { email: teacherSeed.email }, + update: { + name: teacherSeed.name, + passwordHash, + role: "TEACHER", + collegeId: college.id, + }, + create: { + name: teacherSeed.name, + email: teacherSeed.email, + passwordHash, + role: "TEACHER", + collegeId: college.id, + }, + select: { id: true, name: true }, + }); + teachers.set(teacher.name, teacher); + } await prisma.user.upsert({ where: { email: "admin@demo.edu" }, @@ -137,7 +216,10 @@ async function main() { academicYear: "2026-27", year: yearForSemester(semester), semester, - proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : null, + proctorId: + semester === 7 && batch === "ECE 2" + ? teachers.get("Dr. Geetanjali Sharma")?.id ?? null + : null, }, })); @@ -147,7 +229,10 @@ async function main() { data: { program, year: yearForSemester(semester), - proctorId: semester === 7 && batch === "ECE 2" ? teacher.id : cls.proctorId, + proctorId: + semester === 7 && batch === "ECE 2" + ? teachers.get("Dr. Geetanjali Sharma")?.id ?? cls.proctorId + : cls.proctorId, }, }); } @@ -167,18 +252,23 @@ async function main() { } for (const subject of subjectsBySemester[semester]) { - const existing = await prisma.subject.findFirst({ - where: { sectionId: section.id, code: subject.code }, + await prisma.subject.upsert({ + where: { sectionId_code: { sectionId: section.id, code: subject.code } }, + update: { name: subject.name, type: subject.type }, + create: { + name: subject.name, + code: subject.code, + type: subject.type, + sectionId: section.id, + }, }); - if (!existing) { - await prisma.subject.create({ - data: { name: subject.name, code: subject.code, sectionId: section.id }, - }); - } } } } + const geetanjali = teachers.get("Dr. Geetanjali Sharma"); + if (!geetanjali) throw new Error("Dr. Geetanjali Sharma was not seeded."); + // Dr. Geetanjali gets exactly six classes from Semesters 1, 3, 5 and 7. // One subject is assigned in each selected class for the class/subject selector. const teacherClassAssignments = [ @@ -213,9 +303,9 @@ async function main() { if (!subject) continue; await prisma.assignment.upsert({ - where: { teacherId_subjectId: { teacherId: teacher.id, subjectId: subject.id } }, + where: { teacherId_subjectId: { teacherId: geetanjali.id, subjectId: subject.id } }, update: {}, - create: { teacherId: teacher.id, subjectId: subject.id }, + create: { teacherId: geetanjali.id, subjectId: subject.id }, }); } @@ -238,14 +328,17 @@ async function main() { }); if (daSubject) { await prisma.assignment.upsert({ - where: { teacherId_subjectId: { teacherId: teacher.id, subjectId: daSubject.id } }, + where: { teacherId_subjectId: { teacherId: geetanjali.id, subjectId: daSubject.id } }, update: {}, - create: { teacherId: teacher.id, subjectId: daSubject.id }, + create: { teacherId: geetanjali.id, subjectId: daSubject.id }, }); await prisma.sheetLink.upsert({ where: { subjectId: daSubject.id }, update: {}, - create: { subjectId: daSubject.id, sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U" }, + create: { + subjectId: daSubject.id, + sheetId: "118662A6Ifl2GDZKnh120v2jNylUiMWGNYThfRXSDX6U", + }, }); } await prisma.sheetLink.upsert({ @@ -257,8 +350,10 @@ async function main() { console.log("Seed complete."); console.log("Created/updated ECE 1, ECE 2 and ECE E for Semesters 1-7."); - console.log("Assigned Dr. Geetanjali to 6 classes from Semesters 1, 3, 5 and 7."); - console.log("Semester 8 intentionally has no subjects; labs are excluded."); + console.log("Added theory and lab subjects; Summer Training Report and NSS are excluded."); + console.log(`Seeded ${teacherSeeds.length} ECE faculty teacher accounts.`); + console.log("Assigned Dr. Geetanjali Sharma to 6 classes from Semesters 1, 3, 5 and 7."); + console.log("Semester 8 intentionally has no subjects."); console.log("Login: geetanjali@demo.edu / changeme123"); console.log("Admin: admin@demo.edu / changeme123"); } From 2cab1bc8661065290c9f8580e8812417d957582d Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 20:15:32 +0530 Subject: [PATCH 25/80] Fix Geetanjali ECE 2 Sem 7 assignment --- prisma/fix-geetanjali-assignments.ts | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 prisma/fix-geetanjali-assignments.ts diff --git a/prisma/fix-geetanjali-assignments.ts b/prisma/fix-geetanjali-assignments.ts new file mode 100644 index 0000000..b594f62 --- /dev/null +++ b/prisma/fix-geetanjali-assignments.ts @@ -0,0 +1,56 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +async function main() { + const teacher = await prisma.user.findUnique({ + where: { email: "geetanjali@demo.edu" }, + }); + + if (!teacher) { + throw new Error("Dr. Geetanjali Sharma was not found."); + } + + const cls = await prisma.class.findFirst({ + where: { + program: "B.Tech ECE 2", + academicYear: "2026-27", + semester: 7, + }, + include: { sections: true }, + }); + + const section = cls?.sections.find((item) => item.name === "A"); + if (!section) { + console.log("ECE 2 Sem 7 Section A not found; nothing to fix."); + return; + } + + // DA 338 T belongs to the old Sem 6 data and must not appear in + // Geetanjali's ECE 2 Sem 7 subject assignments. + const legacySubject = await prisma.subject.findFirst({ + where: { + sectionId: section.id, + code: "DA 338 T", + }, + }); + + if (legacySubject) { + const result = await prisma.assignment.deleteMany({ + where: { + teacherId: teacher.id, + subjectId: legacySubject.id, + }, + }); + console.log(`Removed ${result.count} legacy DA assignment(s) from Geetanjali's ECE 2 Sem 7.`); + } + + console.log("Geetanjali ECE 2 Sem 7 assignment fix complete."); +} + +main() + .catch((error) => { + console.error(error); + process.exit(1); + }) + .finally(async () => prisma.$disconnect()); From 958180a7672ac0f754e04e6c3fc61eb652dcfb35 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 20:15:46 +0530 Subject: [PATCH 26/80] Run Geetanjali assignment fix after seed --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index eab309f..3cfee5f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", - "seed": "ts-node prisma/seed.ts" + "seed": "ts-node prisma/seed.ts && ts-node prisma/fix-geetanjali-assignments.ts" }, "dependencies": { "@prisma/client": "^5.18.0", @@ -35,6 +35,6 @@ "wait-on": "^9.1.0" }, "prisma": { - "seed": "ts-node prisma/seed.ts" + "seed": "ts-node prisma/seed.ts && ts-node prisma/fix-geetanjali-assignments.ts" } } From 8d1cea0f05fa1d587e1a197c0cd007452d77f339 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 23:56:06 +0530 Subject: [PATCH 27/80] Seed ECE 2 Sem 7 students --- prisma/seed-ece2-sem7-students.ts | 117 ++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 prisma/seed-ece2-sem7-students.ts diff --git a/prisma/seed-ece2-sem7-students.ts b/prisma/seed-ece2-sem7-students.ts new file mode 100644 index 0000000..e16a081 --- /dev/null +++ b/prisma/seed-ece2-sem7-students.ts @@ -0,0 +1,117 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +const students = [ + ["5215002823", "Pranay Bhardwaj", "pranay.bhardwaj20090040@gmail.com"], + ["5315002823", "kunal jotwani", "kunalye80@gmail.com"], + ["5415002823", "Sanchit Sharma", "sanchitsharma917@gmail.com"], + ["5515002823", "Sajal Singhal", "specialsajal@gmail.com"], + ["5615002823", "Khyati Kakkar", "kkakkar5k@gmail.com"], + ["5715002823", "chetan kumar", "ck769825@gmail.com"], + ["5815002823", "Manish", "officialmanishsingh03@gmail.com"], + ["5915002823", "ISHA JHA", "ishujha360@gmail.com"], + ["6015002823", "Omar Rizwan", "omarrizwan22424@gmail.com"], + ["6115002823", "ASHISH SHARMA", "ashishsharma23042004@gmail.com"], + ["6215002823", "Harsh rautela", "HARSH.RAUTELA15@GMAIL.COM"], + ["6315002823", "SATYAJEET KUMAR", "KUMARSATYAJEETANAND@GMAIL.COM"], + ["6415002823", "piyush kumar singh", "piyushsingh22617@gmail.com"], + ["6515002823", "Arpit Singh", "arpitsingh221972@gmail.com"], + ["6615002823", "Syed Faizaan Ahmad", "Syedashffaizaan1999@gmail.com"], + ["6715002823", "Abhimanyu", "abhimanyu3704@gmail.com"], + ["6815002823", "UTTAM KUMAR", "uttamkumar192006@gmail.com"], + ["6915002823", "Aastha Raj Singh", "aastharsingh2004@gmail.com"], + ["7015002823", "Zishan Afzal", "zishanafzal26182@gmail.com"], + ["7115002823", "Saksham Shaurya", "SAKSHAMSHAURYAEXAM@GMAIL.COM"], + ["7215002823", "NEERAJ", "neerajbeniwal2015@gmail.com"], + ["7315002823", "Kartikey Bhardwaj", "kartikeybhardwaj2428@gmail.com"], + ["7415002823", "Harsh Jindal", "hjinal018@gmail.com"], + ["7515002823", "Karan Vasisth", "kvasisth2000@gmail.com"], + ["7615002823", "MANISH KUMAR", "manish0o09955@gmail.com"], + ["7715002823", "SHIVAM TIWARI", "shivamtiwari020505@gmail.com"], + ["7815002823", "Ayush Uniyal", "unialayush2@gmail.com"], + ["7915002823", "Divesh", "keindivesh@gmail.com"], + ["8015002823", "DAKSH TIWARI", "daksh.tiwari23@gmail.com"], + ["8215002823", "Rishabh", "rishabh06x@gmail.com"], + ["8315002823", "Vivek Das", "dasv3125@gmail.com"], + ["8415002823", "UDIT RATHORE", "uditrathore0602@gmail.com"], + ["8515002823", "Pranav Mittal", "ppvm.0112@gmail.com"], + ["8715002823", "Aditya Jha", "adityajha9313@gmail.com"], + ["8815002823", "kushagra pandey", "kushagrapande302004@gmail.com"], + ["8915002823", "Rohit", "rk9104593@gmail.com"], + ["9015002823", "Gaurav kumar", "gouravkm314@gmail.com"], + ["9115002823", "APARMIT SRIVASTAVA", "aparmitsrivastava1@gmail.com"], + ["9215002823", "Prashant", "kumarp89180@gmail.com"], + ["9315002823", "Sarthak Gupta", "sarthakgupta.1105@gmail.com"], + ["9415002823", "saunak sharma", "saunaksharma@gmail.com"], + ["9515002823", "Rohan Kumar", "rohan.kumar52003@gmail.com"], + ["9615002823", "YASHWANT KUMAR BEDIA", "yash613948@gmail.com"], + ["9715002823", "Khushal", "officialkhushal22@gmail.com"], + ["9815002823", "Abhi Rana", "Abhirajput25106@gmail.com"], + ["10015002823", "MOHD SHAHABUDDIN", "mohdshahabuddin039@gmail.com"], + ["10115002823", "Suraj Kumar", "suraj2004ksmile@gmail.com"], + ["70115002823", "Radhika Garg", "grgradhika18@gmail.com"], + ["35215002823", "RAHUL SHARMA", "rahul.0703bkn@gmail.com"], + ["35315002823", "TANISHA ROSE", "tanisharose91365@gmail.com"], + ["35415002823", "VINIT MEHLAWAT", "vinitmehlawat07@gmail.com"], + ["35615002823", "VRINDA DIXIT", "dixitvrinda1704@gmail.com"], + ["35715002823", "SHINJINI DATTA", "shinjini.005@gmail.com"], + ["35915002823", "YASHASWINI NARULA", "yashaswininarula@gmail.com"], + ["75215002823", "Yaksh Verma", "bryant23.yv@gmail.com"], + ["20115002823", "Abhishek", "ag8882138469@gmail.com"], + ["20315002823", "Ansh Raj", "anshrajjee3@gmail.com"], + ["20515002823", "Sneha Juyal", "snehajuyal20@gmail.com"], + ["20615002823", "Richa", "richabaghel25@gmail.com"], + ["60915002824", "Raj Gandhi", "rg7065455@gmail.com"], + ["60215002824", "Gaurav", "gauravkukreja914@gmail.com"], + ["60815002824", "Sweta Kumari", "swetachandra1212@gmail.com"], + ["60415002824", "CH Hareesh", "chhareesh11@gmail.com"], + ["60715002824", "Kunal Kumar", "KKUNAL10169@GMAIL.COM"], + ["6815002822", "Anant Pratap Singh", "ss4479279@gmail.com"], +] as const; + +async function main() { + const department = await prisma.department.findFirst({ + where: { name: "ECE", college: { id: "demo-college" } }, + }); + if (!department) throw new Error("ECE department not found."); + + const cls = await prisma.class.findFirst({ + where: { + departmentId: department.id, + academicYear: "2026-27", + semester: 7, + OR: [{ program: "B.Tech ECE 2" }, { program: "B.Tech ECE" }], + }, + }); + if (!cls) throw new Error("ECE 2 Semester 7 class not found."); + + const section = await prisma.section.findFirst({ + where: { classId: cls.id, name: "A" }, + }); + if (!section) throw new Error("ECE 2 Semester 7 Section A not found."); + + for (const [enrollmentNo, name, email] of students) { + await prisma.student.upsert({ + where: { sectionId_enrollmentNo: { sectionId: section.id, enrollmentNo } }, + update: { name, email }, + create: { sectionId: section.id, enrollmentNo, name, email }, + }); + } + + await prisma.section.update({ + where: { id: section.id }, + data: { strength: students.length }, + }); + + console.log(`Seeded ${students.length} students into ECE 2 Sem 7 Section A.`); +} + +main() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); From 4fcd08592d6bea8807dc83557c32a4923c0a61be Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sat, 29 Aug 2026 23:56:32 +0530 Subject: [PATCH 28/80] Run ECE 2 Sem 7 student seed --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3cfee5f..c1f2e97 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", - "seed": "ts-node prisma/seed.ts && ts-node prisma/fix-geetanjali-assignments.ts" + "seed": "ts-node prisma/seed.ts && ts-node prisma/fix-geetanjali-assignments.ts && ts-node prisma/seed-ece2-sem7-students.ts" }, "dependencies": { "@prisma/client": "^5.18.0", @@ -35,6 +35,6 @@ "wait-on": "^9.1.0" }, "prisma": { - "seed": "ts-node prisma/seed.ts && ts-node prisma/fix-geetanjali-assignments.ts" + "seed": "ts-node prisma/seed.ts && ts-node prisma/fix-geetanjali-assignments.ts && ts-node prisma/seed-ece2-sem7-students.ts" } } From 7af938029b20bbe1592fa68eb5ae1d0ebbf0f3f9 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 13:11:19 +0530 Subject: [PATCH 29/80] Add themed Class Analysis class selector --- pages/class-analysis/index.tsx | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pages/class-analysis/index.tsx diff --git a/pages/class-analysis/index.tsx b/pages/class-analysis/index.tsx new file mode 100644 index 0000000..e808e41 --- /dev/null +++ b/pages/class-analysis/index.tsx @@ -0,0 +1,44 @@ +import { GetServerSideProps } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../../lib/authOptions"; +import { prisma } from "../../lib/prisma"; +import Link from "next/link"; +import { ArrowLeft, ArrowRight, BarChart3, BookOpen, CalendarDays, LayoutDashboard, LogOut, UserRoundPlus } from "lucide-react"; +import { signOut } from "next-auth/react"; + +type ClassOption = { id: string; label: string }; +type Props = { classes: ClassOption[]; teacherName: string }; + +function formatClassLabel(department: string, semester: number, classNumber: string) { + return `${department}-${classNumber} Sem ${semester}`; +} + +export default function ClassAnalysisIndex({ classes, teacherName }: Props) { + const initials = teacherName.split(" ").filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase()).join("") || "T"; + + return ( +
+ +
+ Back to Dashboard +

Class Analysis

Choose a class

Select a class assigned to you to open its analysis.

+
{classes.map((classOption) => {classOption.label}Open attendance, academic and student analysis)}{!classes.length &&
No classes are assigned to you.
}
+
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const session = await getServerSession(ctx.req, ctx.res, authOptions); + if (!session?.user) return { redirect: { destination: "/login", permanent: false } }; + const userId = (session.user as any).id; const role = (session.user as any).role; + const sections = await prisma.section.findMany({ where: role === "ADMIN" ? undefined : { OR: [{ class: { proctorId: userId } }, { subjects: { some: { assignments: { some: { teacherId: userId } } } } }] }, include: { class: { include: { department: true } } } }); + const classes = new Map(); + for (const section of sections) classes.set(section.class.id, { id: section.class.id, label: formatClassLabel(section.class.department.name, section.class.semester, section.name) }); + return { props: { classes: Array.from(classes.values()), teacherName: session.user.name || session.user.email } }; +}; From d8faa96ccd030268d1aade83c9d41b9fc460f73c Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 13:11:33 +0530 Subject: [PATCH 30/80] Use ECE-2 class naming in Subject Analysis --- pages/subject-analysis/index.tsx | 151 ++++--------------------------- 1 file changed, 19 insertions(+), 132 deletions(-) diff --git a/pages/subject-analysis/index.tsx b/pages/subject-analysis/index.tsx index e25b1dc..13dfe92 100644 --- a/pages/subject-analysis/index.tsx +++ b/pages/subject-analysis/index.tsx @@ -6,153 +6,40 @@ import Link from "next/link"; import { useState } from "react"; import { BookOpen, ArrowLeft, ArrowRight, BarChart3 } from "lucide-react"; -interface SubjectOption { - id: string; - sectionId: string; - name: string; - code: string; -} - -interface ClassOption { - id: string; - label: string; -} +interface SubjectOption { id: string; sectionId: string; name: string; code: string; } +interface ClassOption { id: string; label: string; } +interface Props { teacherName: string; classes: ClassOption[]; subjects: SubjectOption[]; } -interface Props { - teacherName: string; - classes: ClassOption[]; - subjects: SubjectOption[]; +function formatClassLabel(department: string, classNumber: string, semester: number) { + return `${department}-${classNumber} Sem ${semester}`; } export default function SubjectAnalysisChooser({ teacherName, classes, subjects }: Props) { const [selectedClass, setSelectedClass] = useState(classes[0]?.id || ""); const selectedSubjects = subjects.filter((subject) => subject.sectionId === selectedClass); - return (
- -
-
- - Back to Dashboard - - -
-

Subject Analysis

-

Choose a class and subject

-

- Select a class you are assigned to, then choose one of your assigned subjects for that class. -

-
- -
-
- - -
- -
-
- - {selectedSubjects.length} available -
- - {selectedSubjects.length === 0 ? ( -
- No subjects are assigned to you for this class. -
- ) : ( -
- {selectedSubjects.map((subject) => ( - - - - - - {subject.name} - {subject.code} - - - - ))} -
- )} -
-
- -

Signed in as {teacherName}. Only your assigned classes and subjects are shown.

-
-
+
+ Back to Dashboard +

Subject Analysis

Choose a class and subject

Select a class you are assigned to, then choose one of your assigned subjects for that class.

+
+
{selectedSubjects.length} available
{selectedSubjects.length === 0 ?
No subjects are assigned to you for this class.
:
{selectedSubjects.map((subject) => {subject.name}{subject.code})}
}
+

Signed in as {teacherName}. Only your assigned classes and subjects are shown.

+
); } export const getServerSideProps: GetServerSideProps = async (ctx) => { const session = await getServerSession(ctx.req, ctx.res, authOptions); - if (!session?.user) { - return { redirect: { destination: "/login", permanent: false } }; - } - - const userId = (session.user as any).id; - const role = (session.user as any).role; - - const subjectsRaw = await prisma.subject.findMany({ - where: role === "ADMIN" ? {} : { assignments: { some: { teacherId: userId } } }, - include: { - section: { include: { class: { include: { department: true } } } }, - }, - orderBy: [{ section: { class: { semester: "asc" } } }, { name: "asc" }], - }); - + if (!session?.user) return { redirect: { destination: "/login", permanent: false } }; + const userId = (session.user as any).id; const role = (session.user as any).role; + const subjectsRaw = await prisma.subject.findMany({ where: role === "ADMIN" ? {} : { assignments: { some: { teacherId: userId } } }, include: { section: { include: { class: { include: { department: true } } } } }, orderBy: [{ section: { class: { semester: "asc" } } }, { name: "asc" }] }); const classMap = new Map(); - for (const subject of subjectsRaw) { - const section = subject.section; - const sectionClass = section.class; - classMap.set(section.id, { - id: section.id, - label: `${sectionClass.department.name} — ${sectionClass.program}, Sem ${sectionClass.semester}, Section ${section.name}`, - }); - } - - return { - props: { - teacherName: session.user.name || session.user.email, - classes: Array.from(classMap.values()), - subjects: subjectsRaw.map((subject) => ({ - id: subject.id, - sectionId: subject.section.id, - name: subject.name, - code: subject.code, - })), - }, - }; + for (const subject of subjectsRaw) { const c = subject.section.class; classMap.set(c.id, { id: c.id, label: formatClassLabel(c.department.name, subject.section.name, c.semester) }); } + return { props: { teacherName: session.user.name || session.user.email, classes: Array.from(classMap.values()), subjects: subjectsRaw.map((subject) => ({ id: subject.id, sectionId: subject.section.class.id, name: subject.name, code: subject.code })) } }; }; From e2a704699df78371bc9aedda390211919a93e8a5 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 14:15:14 +0530 Subject: [PATCH 31/80] Route legacy class analysis URLs through selector --- pages/section-analysis/[sectionId].tsx | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/pages/section-analysis/[sectionId].tsx b/pages/section-analysis/[sectionId].tsx index 93616a0..ea807bd 100644 --- a/pages/section-analysis/[sectionId].tsx +++ b/pages/section-analysis/[sectionId].tsx @@ -1,18 +1,13 @@ import { GetServerSideProps } from "next"; -// The bare /section-analysis/[sectionId] URL isn't a page on its own — -// analysis lives under /attendance, /academic, /overall, /students. -// This just sends anyone landing here (dashboard links, bookmarks, etc.) -// straight to the default tab. -export const getServerSideProps: GetServerSideProps = async (ctx) => { - const { sectionId } = ctx.params as { sectionId: string }; - return { - redirect: { - destination: `/section-analysis/${sectionId}/attendance`, - permanent: false, - }, - }; -}; +// Legacy class-analysis URLs must go through the assigned-class selector. +// This prevents dashboard/bookmark links from bypassing teacher-scoped selection. +export const getServerSideProps: GetServerSideProps = async () => ({ + redirect: { + destination: "/section-analysis", + permanent: false, + }, +}); export default function SectionAnalysisRedirect() { return null; From b739f76f8a2f0b6e158d67caa232fdd40a7854b3 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 14:17:54 +0530 Subject: [PATCH 32/80] Fix dashboard Class Analysis navigation to selector --- pages/dashboard.tsx | 263 ++++---------------------------------------- 1 file changed, 19 insertions(+), 244 deletions(-) diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 2229489..22718a2 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -28,34 +28,10 @@ interface Props { } const quickActions = [ - { - title: "Attendance Agent", - description: "Take attendance for your current class", - href: "/attendance-agent", - icon: UserRoundPlus, - tone: "bg-[#eef1ff] text-[#4b6bff]", - }, - { - title: "Timetable", - description: "View your full class schedule", - href: "/timetable", - icon: CalendarDays, - tone: "bg-[#eef7ff] text-[#4388d8]", - }, - { - title: "Class Analysis", - description: "Analyze performance of a class/section", - href: "/section-analysis", - icon: BarChart3, - tone: "bg-[#eaf9f1] text-[#159b62]", - }, - { - title: "Subject Analysis", - description: "Deep dive into a subject in a section", - href: "/subject-analysis", - icon: BookOpen, - tone: "bg-[#fff5e7] text-[#ee9412]", - }, + { title: "Attendance Agent", description: "Take attendance for your current class", href: "/attendance-agent", icon: UserRoundPlus, tone: "bg-[#eef1ff] text-[#4b6bff]" }, + { title: "Timetable", description: "View your full class schedule", href: "/timetable", icon: CalendarDays, tone: "bg-[#eef7ff] text-[#4388d8]" }, + { title: "Class Analysis", description: "Analyze performance of a class/section", href: "/class-analysis", icon: BarChart3, tone: "bg-[#eaf9f1] text-[#159b62]" }, + { title: "Subject Analysis", description: "Deep dive into a subject in a section", href: "/subject-analysis", icon: BookOpen, tone: "bg-[#fff5e7] text-[#ee9412]" }, ]; const todaySchedule = [ @@ -74,230 +50,29 @@ const recentActivity = [ ]; export default function Dashboard({ teacherName, sections, subjects }: Props) { - const initials = teacherName - .split(" ") - .filter(Boolean) - .slice(0, 2) - .map((part) => part[0]?.toUpperCase()) - .join(""); - + const initials = teacherName.split(" ").filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase()).join(""); return (
- -
-
-
-
-
- - ClassPulse -
-

Welcome back, {teacherName}

-

Here's an overview of your day.

-
-
- -
- - {new Date().toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })} -
- -
-
- -
-
-

Quick Actions

-

Jump straight into the tools you use most.

-
-
- {quickActions.map((action) => { - const Icon = action.icon; - const href = action.title === "Class Analysis" - ? sections[0]?.id ? `/section-analysis/${sections[0].id}` : "/section-analysis" - : action.title === "Subject Analysis" - ? "/subject-analysis" - : action.href; - - return ( - - - - {action.title} - {action.description} - - - - ); - })} -
-
- -
-
-
-
-

Today's Timetable

-

Your scheduled classes for today.

-
- View full timetable -
-
- {todaySchedule.map((item, index) => ( -
-
- {item.time} - {item.end} -
-
- - {index < todaySchedule.length - 1 && } -

{item.subject}

-

{item.section} {item.room}

- {item.active && Next class} -
-
- ))} -
-
- -
-
-
-

Recent Activity

-

Your latest updates and actions.

-
-
-
- {recentActivity.map((activity, index) => { - const Icon = activity.icon; - return ( -
- -

{activity.text}

- {activity.time} -
- ); - })} -
-
-
- -
© 2026 ClassPulse. All rights reserved.
-
-
+
ClassPulse

Welcome back, {teacherName}

Here's an overview of your day.

{new Date().toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}
+

Quick Actions

Jump straight into the tools you use most.

{quickActions.map((action) => { const Icon = action.icon; return {action.title}{action.description}; })}
+

Today's Timetable

Your scheduled classes for today.

View full timetable
{todaySchedule.map((item, index) =>
{item.time}{item.end}
{index < todaySchedule.length - 1 && }

{item.subject}

{item.section} {item.room}

{item.active && Next class}
)}
+

Recent Activity

Your latest activity.

{recentActivity.map((item) => { const Icon = item.icon; return

{item.text}

{item.time}

; })}
+
); } export const getServerSideProps: GetServerSideProps = async (ctx) => { const session = await getServerSession(ctx.req, ctx.res, authOptions); - if (!session?.user) { - return { redirect: { destination: "/login", permanent: false } }; - } - - const userId = (session.user as any).id; - const role = (session.user as any).role; - - const sectionsRaw = - role === "ADMIN" - ? await prisma.section.findMany({ include: { class: { include: { department: true } } } }) - : await prisma.section.findMany({ - where: { - OR: [ - { class: { proctorId: userId } }, - { subjects: { some: { assignments: { some: { teacherId: userId } } } } }, - ], - }, - include: { class: { include: { department: true } } }, - }); - - const subjectsRaw = await prisma.subject.findMany({ - where: role === "ADMIN" ? {} : { assignments: { some: { teacherId: userId } } }, - include: { section: { include: { class: true } } }, - }); - - return { - props: { - teacherName: session.user.name || session.user.email, - sections: sectionsRaw.map((s) => ({ - id: s.id, - label: `${s.class.department.name} — ${s.class.program}, Sem ${s.class.semester}, Section ${s.name}`, - })), - subjects: subjectsRaw.map((s) => ({ - id: s.id, - label: `${s.name} (${s.code}) — Section ${s.section.name}`, - })), - }, - }; + if (!session?.user) return { redirect: { destination: "/login", permanent: false } }; + const userId = (session.user as any).id; const role = (session.user as any).role; + const sections = await prisma.section.findMany({ where: role === "ADMIN" ? undefined : { OR: [{ class: { proctorId: userId } }, { subjects: { some: { assignments: { some: { teacherId: userId } } } } }] }, include: { class: { include: { department: true } } } }); + const subjects = await prisma.subject.findMany({ where: role === "ADMIN" ? undefined : { assignments: { some: { teacherId: userId } } }, include: { section: { include: { class: true } } } }); + return { props: { sections: sections.map((s) => ({ id: s.id, label: `${s.class.department.name} — B.Tech ${s.class.department.name}, Sem ${s.class.semester}, Section ${s.name}` })), subjects: subjects.map((s) => ({ id: s.id, label: `${s.code} — ${s.name}` })), teacherName: session.user.name || session.user.email } }; }; From 36818094422d2de5815402eb90fc6176c87550d3 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 18:36:10 +0530 Subject: [PATCH 33/80] Restore existing Class Analysis experience after selector --- pages/class-analysis/[classId].tsx | 99 +++++------------------------- 1 file changed, 16 insertions(+), 83 deletions(-) diff --git a/pages/class-analysis/[classId].tsx b/pages/class-analysis/[classId].tsx index dccc470..109405e 100644 --- a/pages/class-analysis/[classId].tsx +++ b/pages/class-analysis/[classId].tsx @@ -1,87 +1,20 @@ -import { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import Link from "next/link"; +import { GetServerSideProps } from "next"; -interface ClassAnalysisResponse { - className: string; - totalStudents: number; - subjects: { - subjectId: string; - subjectName: string; - section: string; - classAverage: number | null; - passRate: number | null; - computedAt: string | null; - }[]; -} - -export default function ClassAnalysisPage() { - const router = useRouter(); - const { classId } = router.query; - const [data, setData] = useState(null); - const [error, setError] = useState(""); - - useEffect(() => { - if (!classId) return; - fetch(`/api/analysis/class/${classId}`) - .then(async (res) => { - const json = await res.json(); - if (!res.ok) throw new Error(json.error); - setData(json); - }) - .catch((e) => setError(e.message)); - }, [classId]); - - return ( -
-

Class Analysis

- {data &&

{data.className} · {data.totalStudents} students

} +// Class selection uses the new /class-analysis page, but the existing +// class-analysis experience remains under /section-analysis/[sectionId]. +// Keep this route as a compatibility redirect so the existing analysis UI +// and its Attendance / Academic / Overall / Student Report tabs are preserved. +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const { classId } = ctx.params as { classId: string }; - {error && ( -
- {error} -
- )} + return { + redirect: { + destination: `/section-analysis/${classId}/attendance`, + permanent: false, + }, + }; +}; - {data && ( -
- - - - - - - - - - - - - {data.subjects.map((s) => ( - - - - - - - - - ))} - -
SubjectSectionClass AveragePass RateLast Synced
{s.subjectName}{s.section} - {s.classAverage !== null ? `${s.classAverage}%` : "—"} - {s.passRate !== null ? `${s.passRate}%` : "—"} - {s.computedAt ? new Date(s.computedAt).toLocaleDateString() : "Never"} - - - View → - -
-
- )} -
- ); +export default function ClassAnalysisRedirect() { + return null; } From c49b117b4957df13231979b10070620d609e04cf Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 18:51:06 +0530 Subject: [PATCH 34/80] Fix class analysis routing to existing section analysis --- pages/class-analysis/[classId].tsx | 60 +++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/pages/class-analysis/[classId].tsx b/pages/class-analysis/[classId].tsx index 109405e..b6a4fb7 100644 --- a/pages/class-analysis/[classId].tsx +++ b/pages/class-analysis/[classId].tsx @@ -1,15 +1,63 @@ import { GetServerSideProps } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../../lib/authOptions"; +import { prisma } from "../../lib/prisma"; -// Class selection uses the new /class-analysis page, but the existing -// class-analysis experience remains under /section-analysis/[sectionId]. -// Keep this route as a compatibility redirect so the existing analysis UI -// and its Attendance / Academic / Overall / Student Report tabs are preserved. +// The selector is class-based, while the existing analysis implementation is +// section-based internally. A ClassPulse "class" currently maps to its +// assigned section (for example ECE-2 Sem 7 -> section 2 of that class). +// Resolve that section here and hand off to the existing analysis UI. export const getServerSideProps: GetServerSideProps = async (ctx) => { - const { classId } = ctx.params as { classId: string }; + const classId = typeof ctx.params?.classId === "string" ? ctx.params.classId : ""; + + const session = await getServerSession(ctx.req, ctx.res, authOptions); + if (!session?.user) { + return { redirect: { destination: "/login", permanent: false } }; + } + + if (!classId) { + return { notFound: true }; + } + + const userId = (session.user as any).id as string; + const role = (session.user as any).role as string; + + const cls = await prisma.class.findUnique({ + where: { id: classId }, + include: { + sections: { + orderBy: { name: "asc" }, + select: { id: true }, + }, + }, + }); + + if (!cls || cls.sections.length === 0) { + return { notFound: true }; + } + + // Match the same access rules used by the existing section analysis: + // admin, class proctor, or teacher assigned to a subject in this class. + if (role !== "ADMIN") { + const isProctor = cls.proctorId === userId; + const teachesInClass = await prisma.assignment.findFirst({ + where: { + teacherId: userId, + subject: { section: { classId } }, + }, + select: { id: true }, + }); + + if (!isProctor && !teachesInClass) { + return { notFound: true }; + } + } + + const sectionId = cls.sections[0].id; return { redirect: { - destination: `/section-analysis/${classId}/attendance`, + destination: `/section-analysis/${sectionId}/attendance`, permanent: false, }, }; From 33604893edef425cf454e62b7f249ca544155db6 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 18:54:50 +0530 Subject: [PATCH 35/80] Fix Class Analysis section routing and authorization --- pages/class-analysis/[classId].tsx | 64 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/pages/class-analysis/[classId].tsx b/pages/class-analysis/[classId].tsx index b6a4fb7..603e92e 100644 --- a/pages/class-analysis/[classId].tsx +++ b/pages/class-analysis/[classId].tsx @@ -3,10 +3,10 @@ import { getServerSession } from "next-auth/next"; import { authOptions } from "../../lib/authOptions"; import { prisma } from "../../lib/prisma"; -// The selector is class-based, while the existing analysis implementation is -// section-based internally. A ClassPulse "class" currently maps to its -// assigned section (for example ECE-2 Sem 7 -> section 2 of that class). -// Resolve that section here and hand off to the existing analysis UI. +// Class Analysis is a class-level entry point into the existing section-based +// analysis UI. A class can contain multiple sections, so never blindly pick +// the first section: resolve the section the signed-in user is actually +// allowed to view. export const getServerSideProps: GetServerSideProps = async (ctx) => { const classId = typeof ctx.params?.classId === "string" ? ctx.params.classId : ""; @@ -22,42 +22,44 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { const userId = (session.user as any).id as string; const role = (session.user as any).role as string; - const cls = await prisma.class.findUnique({ - where: { id: classId }, - include: { - sections: { - orderBy: { name: "asc" }, - select: { id: true }, + if (role === "ADMIN") { + const section = await prisma.section.findFirst({ + where: { classId }, + orderBy: { name: "asc" }, + select: { id: true }, + }); + + if (!section) return { notFound: true }; + + return { + redirect: { + destination: `/section-analysis/${section.id}/attendance`, + permanent: false, }, + }; + } + + // Prefer a section the teacher teaches. This is important when a class has + // multiple sections: the section-analysis API enforces section-level access. + const assignedSection = await prisma.section.findFirst({ + where: { + classId, + OR: [ + { class: { proctorId: userId } }, + { subjects: { some: { assignments: { some: { teacherId: userId } } } } }, + ], }, + orderBy: { name: "asc" }, + select: { id: true }, }); - if (!cls || cls.sections.length === 0) { + if (!assignedSection) { return { notFound: true }; } - // Match the same access rules used by the existing section analysis: - // admin, class proctor, or teacher assigned to a subject in this class. - if (role !== "ADMIN") { - const isProctor = cls.proctorId === userId; - const teachesInClass = await prisma.assignment.findFirst({ - where: { - teacherId: userId, - subject: { section: { classId } }, - }, - select: { id: true }, - }); - - if (!isProctor && !teachesInClass) { - return { notFound: true }; - } - } - - const sectionId = cls.sections[0].id; - return { redirect: { - destination: `/section-analysis/${sectionId}/attendance`, + destination: `/section-analysis/${assignedSection.id}/attendance`, permanent: false, }, }; From 318c692b99a05c772e9bc57b8a2a9b4c691db6dc Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 19:00:21 +0530 Subject: [PATCH 36/80] Restore working section analysis route --- pages/section-analysis/[sectionId].tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pages/section-analysis/[sectionId].tsx b/pages/section-analysis/[sectionId].tsx index ea807bd..93616a0 100644 --- a/pages/section-analysis/[sectionId].tsx +++ b/pages/section-analysis/[sectionId].tsx @@ -1,13 +1,18 @@ import { GetServerSideProps } from "next"; -// Legacy class-analysis URLs must go through the assigned-class selector. -// This prevents dashboard/bookmark links from bypassing teacher-scoped selection. -export const getServerSideProps: GetServerSideProps = async () => ({ - redirect: { - destination: "/section-analysis", - permanent: false, - }, -}); +// The bare /section-analysis/[sectionId] URL isn't a page on its own — +// analysis lives under /attendance, /academic, /overall, /students. +// This just sends anyone landing here (dashboard links, bookmarks, etc.) +// straight to the default tab. +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const { sectionId } = ctx.params as { sectionId: string }; + return { + redirect: { + destination: `/section-analysis/${sectionId}/attendance`, + permanent: false, + }, + }; +}; export default function SectionAnalysisRedirect() { return null; From a83139087d38dccfe26978563e2c20786f8de2b1 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 19:01:01 +0530 Subject: [PATCH 37/80] Route class analysis to authorized section --- pages/class-analysis/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/class-analysis/index.tsx b/pages/class-analysis/index.tsx index e808e41..e19a281 100644 --- a/pages/class-analysis/index.tsx +++ b/pages/class-analysis/index.tsx @@ -27,7 +27,7 @@ export default function ClassAnalysisIndex({ classes, teacherName }: Props) {
Back to Dashboard

Class Analysis

Choose a class

Select a class assigned to you to open its analysis.

-
{classes.map((classOption) => {classOption.label}Open attendance, academic and student analysis)}{!classes.length &&
No classes are assigned to you.
}
+
{classes.map((classOption) => {classOption.label}Open attendance, academic and student analysis)}{!classes.length &&
No classes are assigned to you.
}
); @@ -39,6 +39,6 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { const userId = (session.user as any).id; const role = (session.user as any).role; const sections = await prisma.section.findMany({ where: role === "ADMIN" ? undefined : { OR: [{ class: { proctorId: userId } }, { subjects: { some: { assignments: { some: { teacherId: userId } } } } }] }, include: { class: { include: { department: true } } } }); const classes = new Map(); - for (const section of sections) classes.set(section.class.id, { id: section.class.id, label: formatClassLabel(section.class.department.name, section.class.semester, section.name) }); + for (const section of sections) classes.set(section.class.id, { id: section.id, label: formatClassLabel(section.class.department.name, section.class.semester, section.name) }); return { props: { classes: Array.from(classes.values()), teacherName: session.user.name || session.user.email } }; }; From c31f65b44e1ba049976c94584d3f55516ab4fa9f Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 19:07:08 +0530 Subject: [PATCH 38/80] Resolve class and section IDs for analysis access --- pages/api/analysis/section/[sectionId].ts | 181 ++++++---------------- 1 file changed, 44 insertions(+), 137 deletions(-) diff --git a/pages/api/analysis/section/[sectionId].ts b/pages/api/analysis/section/[sectionId].ts index 57f680f..fbe1cf1 100644 --- a/pages/api/analysis/section/[sectionId].ts +++ b/pages/api/analysis/section/[sectionId].ts @@ -1,119 +1,54 @@ -import { - NextApiRequest, - NextApiResponse, -} from "next"; - +import { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "../../../../lib/prisma"; +import { requireSession, assertTeacherCanViewSection } from "../../../../lib/access"; +import { fetchClassRawData } from "../../../../lib/googleSheetsClass"; +import { computeSectionAnalysis } from "../../../../lib/analysisClass"; -import { - requireSession, - assertTeacherCanViewSection, -} from "../../../../lib/access"; - -import { - fetchClassRawData, -} from "../../../../lib/googleSheetsClass"; - -import { - computeSectionAnalysis, -} from "../../../../lib/analysisClass"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = await requireSession( - req, - res - ); - +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await requireSession(req, res); if (!session) return; - const sectionId = - req.query.sectionId as string; - - const userId = - (session.user as any).id; - - const role = - (session.user as any).role; + const requestedId = req.query.sectionId as string; + const userId = (session.user as any).id; + const role = (session.user as any).role; - const allowed = - await assertTeacherCanViewSection( - userId, - role, - sectionId - ); + // The analysis UI is section-based, but older dashboard/class-analysis links + // used a class id. Resolve either form here so navigation cannot break access. + const section = await prisma.section.findUnique({ where: { id: requestedId } }); + const resolvedSection = section || await prisma.section.findFirst({ where: { classId: requestedId } }); + if (!resolvedSection) { + return res.status(404).json({ error: "Section not found" }); + } + const sectionId = resolvedSection.id; + const allowed = await assertTeacherCanViewSection(userId, role, sectionId); if (!allowed) { - return res.status(403).json({ - error: - "Not authorized for this section", - }); + return res.status(403).json({ error: "Not authorized for this section" }); } - const forceSync = - req.query.sync === "1"; - - const previousMonth = - typeof req.query.previousMonth === - "string" - ? req.query.previousMonth - : undefined; - - const currentMonth = - typeof req.query.currentMonth === - "string" - ? req.query.currentMonth - : undefined; + const forceSync = req.query.sync === "1"; + const previousMonth = typeof req.query.previousMonth === "string" ? req.query.previousMonth : undefined; + const currentMonth = typeof req.query.currentMonth === "string" ? req.query.currentMonth : undefined; - // Accept both names so the frontend and API - // cannot get out of sync again. const criteriaValue = typeof req.query.criteria === "string" ? req.query.criteria - : typeof req.query.trendCriteria === - "string" - ? req.query.trendCriteria - : undefined; - - const criteria = - criteriaValue !== undefined - ? Number(criteriaValue) - : undefined; - - const hasCustomTrendSettings = - previousMonth !== undefined || - currentMonth !== undefined || - criteria !== undefined; - - const link = - await prisma.sheetLink.findUnique({ - where: { sectionId }, - }); + : typeof req.query.trendCriteria === "string" + ? req.query.trendCriteria + : undefined; + const criteria = criteriaValue !== undefined ? Number(criteriaValue) : undefined; + const hasCustomTrendSettings = previousMonth !== undefined || currentMonth !== undefined || criteria !== undefined; + const link = await prisma.sheetLink.findUnique({ where: { sectionId } }); if (!link) { - return res.status(404).json({ - error: - "No combined Google Sheet linked to this section yet", - }); + return res.status(404).json({ error: "No combined Google Sheet linked to this section yet" }); } - // Use cache only for the ordinary overview request. - // Custom month comparisons always calculate from - // the latest Google Sheet data. - if ( - !forceSync && - !hasCustomTrendSettings - ) { - const latest = - await prisma.analysisSnapshot.findFirst({ - where: { sectionId }, - orderBy: { - computedAt: "desc", - }, - }); - + if (!forceSync && !hasCustomTrendSettings) { + const latest = await prisma.analysisSnapshot.findFirst({ + where: { sectionId }, + orderBy: { computedAt: "desc" }, + }); if (latest) { return res.status(200).json({ cached: true, @@ -125,52 +60,24 @@ export default async function handler( } try { - const raw = - await fetchClassRawData( - link.sheetId - ); - - const analysis = - computeSectionAnalysis(raw, { - previousMonth, - currentMonth, - criteria, - }); - - const snapshot = - await prisma.analysisSnapshot.create({ - data: { - sectionId, - data: analysis as any, - }, - }); - - await prisma.sheetLink.update({ - where: { sectionId }, - data: { - lastSyncAt: new Date(), - }, + const raw = await fetchClassRawData(link.sheetId); + const analysis = computeSectionAnalysis(raw, { previousMonth, currentMonth, criteria }); + const snapshot = await prisma.analysisSnapshot.create({ + data: { sectionId, data: analysis as any }, }); + await prisma.sheetLink.update({ where: { sectionId }, data: { lastSyncAt: new Date() } }); return res.status(200).json({ cached: false, - computedAt: - snapshot.computedAt, + computedAt: snapshot.computedAt, sheetId: link.sheetId, data: analysis, }); } catch (err: any) { - console.error( - "Section analysis sync failed:", - err - ); - + console.error("Section analysis sync failed:", err); return res.status(502).json({ - error: - "Failed to sync from Google Sheets.", - detail: - err?.message || - "Unknown Google Sheets error.", + error: "Failed to sync from Google Sheets.", + detail: err?.message || "Unknown Google Sheets error.", }); } -} \ No newline at end of file +} From ae1862baed47e9d3e344df42b1635d56dc8e1f36 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 19:23:29 +0530 Subject: [PATCH 39/80] style class analysis like subject analysis shell --- components/AnalysisNav.tsx | 184 +++++++++++++++++++++++++++++++++---- 1 file changed, 165 insertions(+), 19 deletions(-) diff --git a/components/AnalysisNav.tsx b/components/AnalysisNav.tsx index e345a09..a85b571 100644 --- a/components/AnalysisNav.tsx +++ b/components/AnalysisNav.tsx @@ -1,5 +1,16 @@ import Link from "next/link"; import { useRouter } from "next/router"; +import { signOut } from "next-auth/react"; +import { + BarChart3, + BookOpen, + Database, + LayoutDashboard, + LogOut, + RefreshCw, + GraduationCap, + FileText, +} from "lucide-react"; type Props = { sectionId: string; @@ -12,40 +23,175 @@ export default function AnalysisNav({ sectionId }: Props) { { label: "Attendance", href: `/section-analysis/${sectionId}/attendance`, + icon: BarChart3, }, { label: "Academic", href: `/section-analysis/${sectionId}/academic`, + icon: GraduationCap, }, { label: "Overall", href: `/section-analysis/${sectionId}/overall`, + icon: LayoutDashboard, }, { label: "Student Report", href: `/section-analysis/${sectionId}/students`, + icon: FileText, }, ]; + const sidebarItems = [ + { label: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, + { label: "Class Analysis", href: "/class-analysis", icon: BarChart3 }, + { label: "Subject Analysis", href: "/subject-analysis", icon: BookOpen }, + { label: "Raw Data", href: "/raw-data", icon: Database }, + ]; + + const initials = "F"; + return ( - +
+ + + +
+
+
+ + {initials} + +
+

Faculty

+

Teacher Portal

+
+
+
+ +
+ + + + + + ); -} \ No newline at end of file +} From 9dd4ed6d374c50dcf032bb18fbd9be16d109e080 Mon Sep 17 00:00:00 2001 From: k2the4 Date: Sun, 30 Aug 2026 19:23:43 +0530 Subject: [PATCH 40/80] remove unused analysis nav import --- components/AnalysisNav.tsx | 92 +++++++------------------------------- 1 file changed, 17 insertions(+), 75 deletions(-) diff --git a/components/AnalysisNav.tsx b/components/AnalysisNav.tsx index a85b571..dceb4e0 100644 --- a/components/AnalysisNav.tsx +++ b/components/AnalysisNav.tsx @@ -7,7 +7,6 @@ import { Database, LayoutDashboard, LogOut, - RefreshCw, GraduationCap, FileText, } from "lucide-react"; @@ -20,26 +19,10 @@ export default function AnalysisNav({ sectionId }: Props) { const router = useRouter(); const items = [ - { - label: "Attendance", - href: `/section-analysis/${sectionId}/attendance`, - icon: BarChart3, - }, - { - label: "Academic", - href: `/section-analysis/${sectionId}/academic`, - icon: GraduationCap, - }, - { - label: "Overall", - href: `/section-analysis/${sectionId}/overall`, - icon: LayoutDashboard, - }, - { - label: "Student Report", - href: `/section-analysis/${sectionId}/students`, - icon: FileText, - }, + { label: "Attendance", href: `/section-analysis/${sectionId}/attendance`, icon: BarChart3 }, + { label: "Academic", href: `/section-analysis/${sectionId}/academic`, icon: GraduationCap }, + { label: "Overall", href: `/section-analysis/${sectionId}/overall`, icon: LayoutDashboard }, + { label: "Student Report", href: `/section-analysis/${sectionId}/students`, icon: FileText }, ]; const sidebarItems = [ @@ -49,8 +32,6 @@ export default function AnalysisNav({ sectionId }: Props) { { label: "Raw Data", href: "/raw-data", icon: Database }, ]; - const initials = "F"; - return ( <>