From 906e9c757b1a4d377e064a72ddd0f860a93c5061 Mon Sep 17 00:00:00 2001 From: Anbu-dev-creator Date: Fri, 21 Aug 2026 09:26:47 +0530 Subject: [PATCH 1/2] New --- package-lock.json | 28 +- src/constants/index.ts | 4 - src/pages/Faculty/Faculty.tsx | 2 +- src/pages/Toolkit/Toolkit.tsx | 794 ++++++++++++++++++++++------------ 4 files changed, 535 insertions(+), 293 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0674289..9bbdf1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1636,17 +1637,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -4432,6 +4422,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.3.tgz", "integrity": "sha512-ZjgcRwmQUSCythDPj6pE2NKFi9bpQegxIBxgt3hPytXI1ElFHNaa3A7aWW8vFaLCIRjPv467rbREYHbxcdgQbg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.101.3" }, @@ -4517,6 +4508,7 @@ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -4527,6 +4519,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4537,6 +4530,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4612,6 +4606,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4870,6 +4865,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", @@ -6545,7 +6541,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/leven": { "version": "3.1.0", @@ -7173,6 +7170,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", @@ -7216,6 +7214,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7225,6 +7224,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7237,6 +7237,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.82.0.tgz", "integrity": "sha512-Zw/uFZ2dO+02GHlBn7JFGn8kZJ7LdM33B/0BXOovzFay+CMhf94JMw5BVu+F1tVkUKjNvBuaE3fz5BJhga10Tg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7543,6 +7544,7 @@ "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -8088,6 +8090,7 @@ "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -8413,6 +8416,7 @@ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", diff --git a/src/constants/index.ts b/src/constants/index.ts index 213e909..fdff766 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -3832,9 +3832,5 @@ export const DEPARTMENTS = [ 'Computer Science & Engineering', 'Electronics & Communication Engineering', 'Electronics Engineering VLSI (Design and Technology)', - 'Humanities & Sciences', - 'M.E. (VLSI Design)', - 'M.Tech (Data Science)', - 'Mathematics', 'Mechanical Engineering', ]; diff --git a/src/pages/Faculty/Faculty.tsx b/src/pages/Faculty/Faculty.tsx index 64405f0..52637cc 100644 --- a/src/pages/Faculty/Faculty.tsx +++ b/src/pages/Faculty/Faculty.tsx @@ -294,7 +294,7 @@ export default function Faculty() { className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]" style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }} > - {DEPARTMENTS.filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => { + {Array.from(new Set(['All Departments', ...DEPARTMENTS, ...FACULTY_DATA.map(f => f.department)])).filter(dept => dept === 'All Departments' || FACULTY_DATA.filter((f) => f.department === dept).length > 0).map((dept) => { const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length; return ; })} diff --git a/src/pages/Toolkit/Toolkit.tsx b/src/pages/Toolkit/Toolkit.tsx index 6226283..2af01d3 100644 --- a/src/pages/Toolkit/Toolkit.tsx +++ b/src/pages/Toolkit/Toolkit.tsx @@ -670,8 +670,24 @@ export default function Toolkit() { const [calculationType, setCalculationType] = useState<'gpa' | 'internal' | null>(null); const [gpaStep, setGpaStep] = useState<1 | 2>(1); const [calculatedScore, setCalculatedScore] = useState(null); + const resultCardRef = useRef(null); const [imsData, setImsData] = useState(null); const [isImsAutoFilled, setIsImsAutoFilled] = useState(false); + const [cat1Input, setCat1Input] = useState(''); + const [cat2Input, setCat2Input] = useState(''); + const [cat3Input, setCat3Input] = useState(''); + const [assignmentInput, setAssignmentInput] = useState(''); + const [internalError, setInternalError] = useState(null); + const [showWeightages, setShowWeightages] = useState>({ + cat1: false, + cat2: false, + cat3: false, + assignment: false, + }); + + const toggleWeightage = (key: string) => { + setShowWeightages((prev) => ({ ...prev, [key]: !prev[key] })); + }; useEffect(() => { const session = getStoredImsSession(); @@ -699,6 +715,7 @@ export default function Toolkit() { grade: string; cat1: string; cat2: string; + cat3: string; assignment: string; attendance: string; isElective?: boolean; @@ -706,13 +723,7 @@ export default function Toolkit() { electiveCode?: string; } - const [subjects, setSubjects] = useState([ - { name: 'Subject 1', credits: 4, grade: 'O', cat1: '45', cat2: '48', assignment: '10', attendance: '95' }, - { name: 'Subject 2', credits: 3, grade: 'A+', cat1: '42', cat2: '44', assignment: '9', attendance: '92' }, - { name: 'Subject 3', credits: 3, grade: 'A', cat1: '38', cat2: '40', assignment: '8', attendance: '88' }, - { name: 'Subject 4', credits: 4, grade: 'O', cat1: '46', cat2: '47', assignment: '10', attendance: '96' }, - { name: 'Subject 5', credits: 2, grade: 'A+', cat1: '44', cat2: '45', assignment: '9', attendance: '94' }, - ]); + const [subjects, setSubjects] = useState([]); const handleProceedToStep2 = (type: 'gpa' | 'internal') => { if (!gpaDepartment || !gpaSemester) return; @@ -766,62 +777,47 @@ export default function Toolkit() { foundAnyImsMatch = true; } - const gradeVal = matchedGrade?.grade || 'O'; - const cat1Val = matchedCat?.cat1 ? String(matchedCat.cat1) : '45'; - const cat2Val = matchedCat?.cat2 ? String(matchedCat.cat2) : '45'; - const assgnVal = matchedCat?.assignment ? String(matchedCat.assignment) : '9'; - - if (isElective) { - const slot = s.electiveSlot || electiveCounter++; - const defaultElectiveObj = PROFESSIONAL_ELECTIVES_LIST[(slot - 1) % PROFESSIONAL_ELECTIVES_LIST.length] || PROFESSIONAL_ELECTIVES_LIST[0]; - return { - name: `${defaultElectiveObj.code} - ${defaultElectiveObj.name}`, - credits: s.credits, - grade: gradeVal, - cat1: cat1Val, - cat2: cat2Val, - assignment: assgnVal, - attendance: '95', - isElective: true, - electiveSlot: slot, - electiveCode: defaultElectiveObj.code, - }; - } + const gradeVal = matchedGrade?.grade || ''; + const cat1Val = matchedCat?.cat1 ? String(matchedCat.cat1) : ''; + const cat2Val = matchedCat?.cat2 ? String(matchedCat.cat2) : ''; + const cat3Val = matchedCat?.cat3 ? String(matchedCat.cat3) : ''; + const assgnVal = matchedCat?.assignment ? String(matchedCat.assignment) : ''; return { name: s.name, credits: s.credits, grade: gradeVal, cat1: cat1Val, cat2: cat2Val, + cat3: cat3Val, assignment: assgnVal, attendance: '95', - isElective: false, }; }); setSubjects(loaded); if (type === 'gpa') { - setIsImsAutoFilled(foundAnyImsMatch || imsGrades.length > 0); - let totalCredits = 0; - let totalPoints = 0; - const GRADE_POINTS: Record = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'RA': 0 }; - loaded.forEach((sub: any) => { - const pts = GRADE_POINTS[sub.grade] ?? 10; - totalCredits += sub.credits; - totalPoints += pts * sub.credits; - }); - if (totalCredits > 0) { - setCalculatedScore((totalPoints / totalCredits).toFixed(2)); + setIsImsAutoFilled(foundAnyImsMatch && imsGrades.length > 0); + if (foundAnyImsMatch && imsGrades.length > 0) { + let totalCredits = 0; + let totalPoints = 0; + const GRADE_POINTS: Record = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'UA': 0, 'RA': 0 }; + loaded.forEach((sub: any) => { + if (sub.grade) { + const pts = GRADE_POINTS[sub.grade] ?? 0; + totalCredits += sub.credits; + totalPoints += pts * sub.credits; + } + }); + if (totalCredits > 0) { + setCalculatedScore((totalPoints / totalCredits).toFixed(2)); + } + } else { + setCalculatedScore(null); } } } else { - setSubjects([ - { name: 'Subject 1', credits: 4, grade: 'O', cat1: '45', cat2: '48', assignment: '10', attendance: '95' }, - { name: 'Subject 2', credits: 3, grade: 'A+', cat1: '42', cat2: '44', assignment: '9', attendance: '92' }, - { name: 'Subject 3', credits: 3, grade: 'A', cat1: '38', cat2: '40', assignment: '8', attendance: '88' }, - { name: 'Subject 4', credits: 4, grade: 'O', cat1: '46', cat2: '47', assignment: '10', attendance: '96' }, - { name: 'Subject 5', credits: 2, grade: 'A+', cat1: '44', cat2: '45', assignment: '9', attendance: '94' }, - ]); + setSubjects([]); + setCalculatedScore(null); } setGpaStep(2); @@ -946,21 +942,105 @@ export default function Toolkit() { {/* Main Content Area */} {selectedToolkit === 'gpa' ? ( - {gpaStep === 1 ? ( - /* ── STEP 1: Department and Semester Selection ── */ + {calculationType === null ? ( + /* ── INITIAL MODE SELECTION: Calculate Internal Mark vs Calculate GPA ── */ +
+
+ +
+

+ GPA & Internal Calculator +

+

+ Select what you would like to calculate to begin. +

+
+ +
+ {/* Option 1: Internal Mark Calculator */} +
{ + setCalculationType('internal'); + setGpaStep(2); + setCalculatedScore(null); + setInternalError(null); + }} + className="p-6 md:p-8 rounded-3xl bg-gradient-to-br from-[#F5F3FF] via-[#FAF5FF] to-white border-2 border-[#DDD6FE] hover:border-[#7C3AED] hover:shadow-lg transition-all cursor-pointer group flex flex-col justify-between" + > +
+
+ +
+
+

+ Calculate Internal Mark +

+

+ Calculate your converted internal mark out of 40 directly from CAT I, CAT II, CAT III, and Assignment marks. Direct calculation with no department or subject inputs needed. +

+
+
+
+ Open Internal Calculator + +
+
+ + {/* Option 2: GPA Calculator */} +
{ + setCalculationType('gpa'); + setGpaStep(1); + setCalculatedScore(null); + setInternalError(null); + }} + className="p-6 md:p-8 rounded-3xl bg-gradient-to-br from-[#FFF7ED] via-[#FFFBEB] to-white border-2 border-[#FED7AA] hover:border-[#FF6B00] hover:shadow-lg transition-all cursor-pointer group flex flex-col justify-between" + > +
+
+ +
+
+

+ Calculate GPA +

+

+ Select your department and semester to load curriculum subjects, enter your grades, and compute your Semester Grade Point Average (SGPA / 10.00). +

+
+
+
+ Select Department & Semester + +
+
+
+
+ ) : calculationType === 'gpa' && gpaStep === 1 ? ( + /* ── STEP 1: Department and Semester Selection for GPA ── */
+ +

- GPA & Internal Calculator + GPA Calculator

- Step 1 of 2: Select your department and semester to begin. + Select your department and semester to load curriculum subjects.

@@ -1002,26 +1082,11 @@ export default function Toolkit() {
- {/* Two Action Buttons to proceed to Step 2 */} -
- - +
) : ( - /* ── STEP 2: Subject Input and Marks Section ── */ + /* ── STEP 2: Calculation Form (GPA or Direct Internal) ── */

- {calculationType === 'gpa' ? 'GPA Calculation' : 'Internal Marks Calculation'} + {calculationType === 'gpa' ? 'GPA Calculation' : 'Internal Mark Calculator'}

-

- {gpaDepartment} — {gpaSemester} -

+ {calculationType === 'gpa' && ( +

+ {gpaDepartment} — {gpaSemester} +

+ )}
{calculationType === 'gpa' ? : } @@ -1086,190 +1160,273 @@ export default function Toolkit() { Grade Achieved
- {(() => { - const selectedElectiveCodes = new Set(subjects.filter((s) => s.isElective && s.electiveCode).map((s) => s.electiveCode)); - - return subjects.filter(s => s.credits > 0).map((sub, idx) => ( -
-
- {sub.isElective ? ( -
-
- Professional Elective {sub.electiveSlot} -
- -
- ) : ( -

- {sub.name} -

- )} - - {sub.credits} {sub.credits === 1 ? 'Credit' : 'Credits'} - -
-
- -
+ {subjects.filter(s => s.credits > 0).map((sub, idx) => ( +
+
+

+ {sub.name} +

+ + {sub.credits} {sub.credits === 1 ? 'Credit' : 'Credits'} +
- )); - })()} -
- ) : ( - /* ── Internal Marks Layout ── */ -
-
- Subject Details - {subjects.length} Subjects -
- - {subjects.map((sub, idx) => ( -
-
- + { - const updated = [...subjects]; - updated[idx].cat1 = e.target.value; - setSubjects(updated); - setCalculatedScore(null); - }} - className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none" - /> -
-
- - { - const updated = [...subjects]; - updated[idx].cat2 = e.target.value; - setSubjects(updated); - setCalculatedScore(null); - }} - className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none" - /> -
-
- - { - const updated = [...subjects]; - updated[idx].assignment = e.target.value; - setSubjects(updated); - setCalculatedScore(null); - }} - className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none" - /> -
-
- - { - const updated = [...subjects]; - updated[idx].attendance = e.target.value; - setSubjects(updated); - setCalculatedScore(null); - }} - className="w-full px-2.5 py-1.5 rounded-xl bg-white border border-[#CBD5E1] text-xs font-medium text-[#1E293B] outline-none" - /> -
+ className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${!sub.grade ? 'border-amber-300 bg-amber-50/20 text-[#64748B]' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#FF6B00]'} text-xs font-bold focus:ring-2 focus:ring-[#FF6B00]/20 outline-none cursor-pointer shadow-2xs`} + style={{ fontFamily: 'Inter, sans-serif' }} + > + + + + + + + + +
))} +
+ ) : ( + /* ── Direct Internal Marks Layout (Assigned 40-Mark Weightages & Per-Field Weightage Calculations) ── */ + (() => { + const n1 = cat1Input === '' ? NaN : Number(cat1Input); + const isCat1Err = cat1Input !== '' && (isNaN(n1) || n1 < 0 || n1 > 50); - + const n2 = cat2Input === '' ? NaN : Number(cat2Input); + const isCat2Err = cat2Input !== '' && (isNaN(n2) || n2 < 0 || n2 > 25); + + const n3 = cat3Input === '' ? NaN : Number(cat3Input); + const isCat3Err = cat3Input !== '' && (isNaN(n3) || n3 < 0 || n3 > 50); + + const nAss = assignmentInput === '' ? NaN : Number(assignmentInput); + const isAssErr = assignmentInput !== '' && (isNaN(nAss) || nAss < 0 || nAss > 50); + + return ( +
+
+ {/* CAT I */} +
+
+ CAT I Mark +
+ + Max 50 + + + Assigned: 11.43 / 40 + +
+
+ { + setCat1Input(e.target.value); + setCalculatedScore(null); + setInternalError(null); + setShowWeightages((prev) => ({ ...prev, cat1: false })); + }} + placeholder="Enter CAT 1 mark (0 - 50)" + className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isCat1Err ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`} + /> + {isCat1Err ? ( +

+ Enter a valid mark between 0 and 50 +

+ ) : ( +
+ + {showWeightages['cat1'] && !isNaN(n1) && n1 >= 0 && n1 <= 50 && ( + + Obtained: {((n1 / 50) * 11.43).toFixed(2)} / 11.43 + + )} +
+ )} +
+ + {/* CAT II */} +
+
+ CAT II Mark +
+ + Max 25 + + + Assigned: 5.71 / 40 + +
+
+ { + setCat2Input(e.target.value); + setCalculatedScore(null); + setInternalError(null); + setShowWeightages((prev) => ({ ...prev, cat2: false })); + }} + placeholder="Enter CAT 2 mark (0 - 25)" + className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isCat2Err ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`} + /> + {isCat2Err ? ( +

+ Enter a valid mark between 0 and 25 +

+ ) : ( +
+ + {showWeightages['cat2'] && !isNaN(n2) && n2 >= 0 && n2 <= 25 && ( + + Obtained: {((n2 / 25) * 5.71).toFixed(2)} / 5.71 + + )} +
+ )} +
+ + {/* CAT III */} +
+
+ CAT III Mark +
+ + Max 50 + + + Assigned: 11.43 / 40 + +
+
+ { + setCat3Input(e.target.value); + setCalculatedScore(null); + setInternalError(null); + setShowWeightages((prev) => ({ ...prev, cat3: false })); + }} + placeholder="Enter CAT 3 mark (0 - 50)" + className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isCat3Err ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`} + /> + {isCat3Err ? ( +

+ Enter a valid mark between 0 and 50 +

+ ) : ( +
+ + {showWeightages['cat3'] && !isNaN(n3) && n3 >= 0 && n3 <= 50 && ( + + Obtained: {((n3 / 50) * 11.43).toFixed(2)} / 11.43 + + )} +
+ )} +
+ + {/* Assignment */} +
+
+ Assignment Mark +
+ + Max 50 + + + Assigned: 11.43 / 40 + +
+
+ { + setAssignmentInput(e.target.value); + setCalculatedScore(null); + setInternalError(null); + setShowWeightages((prev) => ({ ...prev, assignment: false })); + }} + placeholder="Enter Assignment mark (0 - 50)" + className={`w-full px-3.5 py-2.5 rounded-xl bg-white border ${isAssErr ? 'border-red-500 text-red-600 focus:ring-red-500/20' : 'border-[#CBD5E1] text-[#1E293B] focus:border-[#8B5CF6] focus:ring-[#8B5CF6]/20'} text-sm font-bold focus:ring-2 outline-none`} + /> + {isAssErr ? ( +

+ Enter a valid mark between 0 and 50 +

+ ) : ( +
+ + {showWeightages['assignment'] && !isNaN(nAss) && nAss >= 0 && nAss <= 50 && ( + + Obtained: {((nAss / 50) * 11.43).toFixed(2)} / 11.43 + + )} +
+ )} +
+
+
+ ); + })() + )} + + {internalError && ( +
+ + {internalError}
)} @@ -1277,26 +1434,52 @@ export default function Toolkit() { {/* Calculated Result Card */} {calculatedScore !== null && (
- {calculationType === 'gpa' ? 'Calculated SGPA' : 'Average Internal Score'} - - - {gpaSemester} + {calculationType === 'gpa' ? 'Calculated SGPA' : 'Total Internal Mark'} + {gpaSemester && ( + + {gpaSemester} + + )}
-
+
-

{gpaDepartment}

+

+ {calculationType === 'gpa' ? gpaDepartment : 'Converted Internal Score'} +

{calculatedScore} {calculationType === 'gpa' ? '/ 10.00' : '/ 40.0'}
- - {calculationType === 'gpa' - ? (Number(calculatedScore) >= 9.0 ? 'First Class with Distinction (O)' : Number(calculatedScore) >= 8.5 ? 'First Class with Distinction' : Number(calculatedScore) >= 7.0 ? 'First Class' : Number(calculatedScore) >= 5.0 ? 'Second Class' : 'UA / Reappear') - : (Number(calculatedScore) >= 35 ? 'Excellent' : 'Good')} - +
+ {calculationType === 'internal' && (() => { + const c1 = Math.min(50, Math.max(0, Number(cat1Input) || 0)); + const c2 = Math.min(25, Math.max(0, Number(cat2Input) || 0)); + const c3 = Math.min(50, Math.max(0, Number(cat3Input) || 0)); + const ass = Math.min(50, Math.max(0, Number(assignmentInput) || 0)); + const totalRaw = c1 + c2 + c3 + ass; + return ( +
+

Total Raw Marks Obtained

+

+ {totalRaw} / 175 Marks +

+
+ ); + })()} +
+ + {calculationType === 'internal' && (() => { + const c1 = Math.min(50, Math.max(0, Number(cat1Input) || 0)); + const c2 = Math.min(25, Math.max(0, Number(cat2Input) || 0)); + const c3 = Math.min(50, Math.max(0, Number(cat3Input) || 0)); + const ass = Math.min(50, Math.max(0, Number(assignmentInput) || 0)); + + const w1 = ((c1 / 50) * 11.43).toFixed(2); + const w2 = ((c2 / 25) * 5.71).toFixed(2); + const w3 = ((c3 / 50) * 11.43).toFixed(2); + const wAss = ((ass / 50) * 11.43).toFixed(2); + + return ( +
+

+ Assigned 40-Mark Weightage Breakdown +

+
+
+

CAT I (11.43)

+

{w1} / 11.43

+

{c1}/50

+
+
+

CAT II (5.71)

+

{w2} / 5.71

+

{c2}/25

+
+
+

CAT III (11.43)

+

{w3} / 11.43

+

{c3}/50

+
+
+

Assignment (11.43)

+

{wAss} / 11.43

+

{ass}/50

+
+
+
+ ); + })()} )}
From d9ed15b48189ae1b8edceebfdfb5815607efcfe4 Mon Sep 17 00:00:00 2001 From: Amudieshwar A G Date: Fri, 21 Aug 2026 12:10:26 +0530 Subject: [PATCH 2/2] Redesign Admin UI theme, add Merge Routes & Bus Status Manager features --- package-lock.json | 28 +- src/pages/Admin/AdminDashboard.tsx | 1374 +++++++++++++++++++++------- telegram-bot/config.json | 10 +- 3 files changed, 1063 insertions(+), 349 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bbdf1f..0674289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,7 +102,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1637,6 +1636,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -4422,7 +4432,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.3.tgz", "integrity": "sha512-ZjgcRwmQUSCythDPj6pE2NKFi9bpQegxIBxgt3hPytXI1ElFHNaa3A7aWW8vFaLCIRjPv467rbREYHbxcdgQbg==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.101.3" }, @@ -4508,7 +4517,6 @@ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -4519,7 +4527,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4530,7 +4537,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4606,7 +4612,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4865,7 +4870,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", @@ -6541,8 +6545,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/leven": { "version": "3.1.0", @@ -7170,7 +7173,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", @@ -7214,7 +7216,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7224,7 +7225,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7237,7 +7237,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.82.0.tgz", "integrity": "sha512-Zw/uFZ2dO+02GHlBn7JFGn8kZJ7LdM33B/0BXOovzFay+CMhf94JMw5BVu+F1tVkUKjNvBuaE3fz5BJhga10Tg==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7544,7 +7543,6 @@ "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -8090,7 +8088,6 @@ "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -8416,7 +8413,6 @@ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", diff --git a/src/pages/Admin/AdminDashboard.tsx b/src/pages/Admin/AdminDashboard.tsx index 7f78434..99c38ea 100644 --- a/src/pages/Admin/AdminDashboard.tsx +++ b/src/pages/Admin/AdminDashboard.tsx @@ -6,7 +6,8 @@ import { MessageCircle, BookOpen, Bot, Edit, MapPin, Clock, Palette, KeyRound, ExternalLink, ArrowRight, Code2, Check, HelpCircle, Users, Activity, - UserCheck, Building2, GraduationCap, Calculator, Search + UserCheck, Building2, GraduationCap, Calculator, Search, GitMerge, + Power, Play, SlidersHorizontal, ToggleLeft, ToggleRight } from 'lucide-react'; import { getBackendUrl } from '@/lib/utils'; import { Link } from 'react-router-dom'; @@ -31,6 +32,7 @@ interface BusRouteItem { departureTime: string; arrivalTime: string; color: string; + status?: 'ACTIVE' | 'STOPPED'; stops?: BusStopItem[]; } @@ -153,6 +155,26 @@ export default function AdminDashboard() { stops: [{ name: '', time: '07:15 AM' }], }); + // Merge Routes State + const [isMergeModalOpen, setIsMergeModalOpen] = useState(false); + const [selectedRouteIdsForMerge, setSelectedRouteIdsForMerge] = useState([]); + const [mergedRouteFormData, setMergedRouteFormData] = useState({ + number: '', + name: '', + from: '', + to: 'RIT Campus', + departureTime: '07:00 AM', + arrivalTime: '08:30 AM', + color: '#FF6B00', + deactivateOriginals: false, + stops: [] as { name: string; time: string; sourceRoute?: string }[], + }); + + // Bus Status Manager State + const [isBusStatusModalOpen, setIsBusStatusModalOpen] = useState(false); + const [busStatusTabFilter, setBusStatusTabFilter] = useState<'ALL' | 'ACTIVE' | 'STOPPED'>('ALL'); + const [gridStatusFilter, setGridStatusFilter] = useState<'ALL' | 'ACTIVE' | 'STOPPED'>('ACTIVE'); + // Telegram Q&A State const [telegramConfig, setTelegramConfig] = useState(null); const [loadingTelegram, setLoadingTelegram] = useState(false); @@ -455,6 +477,163 @@ export default function AdminDashboard() { setRouteFormData({ ...routeFormData, stops: updated }); }; + // ───────────────────────────────────────────────────────────── + // MERGE ROUTES HANDLERS + // ───────────────────────────────────────────────────────────── + const openMergeRoutesModal = () => { + setSelectedRouteIdsForMerge([]); + setMergedRouteFormData({ + number: '', + name: '', + from: '', + to: 'RIT Campus', + departureTime: '07:00 AM', + arrivalTime: '08:30 AM', + color: '#FF6B00', + deactivateOriginals: false, + stops: [], + }); + setIsMergeModalOpen(true); + }; + + const toggleSelectRouteForMerge = (routeId: number) => { + let updatedIds: number[]; + if (selectedRouteIdsForMerge.includes(routeId)) { + updatedIds = selectedRouteIdsForMerge.filter((id) => id !== routeId); + } else { + updatedIds = [...selectedRouteIdsForMerge, routeId]; + } + setSelectedRouteIdsForMerge(updatedIds); + + // Re-calculate combined stops & default route details + const selectedBuses = routes.filter((r) => updatedIds.includes(r.id)); + if (selectedBuses.length > 0) { + const mergedStops: { name: string; time: string; sourceRoute: string }[] = []; + selectedBuses.forEach((b) => { + (b.stops || []).forEach((st) => { + mergedStops.push({ + name: st.name, + time: st.time || b.departureTime, + sourceRoute: b.number, + }); + }); + }); + + const routeNums = selectedBuses.map((b) => b.number).join(' + '); + const routeNames = selectedBuses.map((b) => b.name.replace(/ Route$/i, '')).join(' + ') + ' Combined'; + + setMergedRouteFormData((prev) => ({ + ...prev, + number: `${selectedBuses[0].number}-MERGED`, + name: routeNames, + from: selectedBuses[0].from, + to: selectedBuses[selectedBuses.length - 1].to || 'RIT Campus', + departureTime: selectedBuses[0].departureTime, + arrivalTime: selectedBuses[selectedBuses.length - 1].arrivalTime || '08:30 AM', + stops: mergedStops, + })); + } else { + setMergedRouteFormData((prev) => ({ + ...prev, + number: '', + name: '', + stops: [], + })); + } + }; + + const handleRemoveMergedStop = (index: number) => { + const updated = mergedRouteFormData.stops.filter((_, i) => i !== index); + setMergedRouteFormData({ ...mergedRouteFormData, stops: updated }); + }; + + const handleAddMergedStopField = () => { + setMergedRouteFormData({ + ...mergedRouteFormData, + stops: [...mergedRouteFormData.stops, { name: '', time: '07:30 AM', sourceRoute: 'Custom' }], + }); + }; + + const handleSaveMergedRoute = (e: React.FormEvent) => { + e.preventDefault(); + if (selectedRouteIdsForMerge.length < 2) { + showToast('error', 'Please select at least 2 bus routes to merge.'); + return; + } + if (!mergedRouteFormData.number.trim() || !mergedRouteFormData.name.trim()) { + showToast('error', 'Please enter a Merged Route Number and Name.'); + return; + } + + const cleanedStops = mergedRouteFormData.stops + .filter((s) => s.name.trim().length > 0) + .map((s, idx) => ({ id: idx + 1, name: s.name, time: s.time, stopOrder: idx + 1 })); + + const newId = routes.length > 0 ? Math.max(...routes.map((r) => r.id || 0)) + 1 : 1; + const newRoute: BusRouteItem = { + id: newId, + number: mergedRouteFormData.number, + name: mergedRouteFormData.name, + from: mergedRouteFormData.from || 'Multi-Origin', + to: mergedRouteFormData.to || 'RIT Campus', + departureTime: mergedRouteFormData.departureTime, + arrivalTime: mergedRouteFormData.arrivalTime, + color: mergedRouteFormData.color || '#FF6B00', + stops: cleanedStops, + }; + + let updatedRoutes = [newRoute, ...routes]; + if (mergedRouteFormData.deactivateOriginals) { + updatedRoutes = updatedRoutes.map((r) => + selectedRouteIdsForMerge.includes(r.id) ? { ...r, status: 'STOPPED' } : r + ); + } + + setRoutes(updatedRoutes); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes)); + showToast('success', `Merged route ${mergedRouteFormData.number} created! Source routes set to STOPPED status.`); + setIsMergeModalOpen(false); + + fetch(getBackendUrl('/api/admin/transport/routes'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newRoute), + }).catch(() => {}); + }; + + // ───────────────────────────────────────────────────────────── + // BUS STATUS MANAGER HANDLERS + // ───────────────────────────────────────────────────────────── + const handleToggleRouteStatus = (routeId: number) => { + const updatedRoutes = routes.map((r) => { + if (r.id === routeId) { + const newStatus: 'ACTIVE' | 'STOPPED' = (r.status || 'ACTIVE') === 'ACTIVE' ? 'STOPPED' : 'ACTIVE'; + showToast('success', `Route ${r.number} status changed to ${newStatus === 'ACTIVE' ? '🟢 Active' : '🔴 Stopped'}`); + return { ...r, status: newStatus }; + } + return r; + }); + + setRoutes(updatedRoutes); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes)); + + const targetRoute = updatedRoutes.find((r) => r.id === routeId); + if (targetRoute) { + fetch(getBackendUrl(`/api/admin/transport/routes/${routeId}`), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(targetRoute), + }).catch(() => {}); + } + }; + + const handleSetAllRoutesStatus = (status: 'ACTIVE' | 'STOPPED') => { + const updatedRoutes = routes.map((r) => ({ ...r, status })); + setRoutes(updatedRoutes); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes)); + showToast('success', `All routes set to ${status === 'ACTIVE' ? '🟢 Active' : '🔴 Stopped'}`); + }; + // ───────────────────────────────────────────────────────────── // TELEGRAM BOT & SENIOR HELPERS HANDLERS // ───────────────────────────────────────────────────────────── @@ -887,18 +1066,18 @@ export default function AdminDashboard() { // ───────────────────────────────────────────────────────────── if (!isLoggedIn) { return ( -
-
-
-
-
- +
+
+
+
+
+
-

+

RIT System Admin Console

-

+

Closed-loop control panel for Transport fleet, Telegram Q&A helpers, and campus services.

@@ -906,19 +1085,19 @@ export default function AdminDashboard() { {loginError && ( -
- +
+ {loginError}
)}
-
-
@@ -949,7 +1128,7 @@ export default function AdminDashboard() {
@@ -998,43 +1181,43 @@ export default function AdminDashboard() { const isTransportOnly = userRole === 'ROLE_TRANSPORT'; return ( -
+
{/* Toast Notification */} {toastMessage && (
- {toastMessage.type === 'success' ? : } + {toastMessage.type === 'success' ? : } {toastMessage.text}
)} -
+
{/* Top Header Bar */} -
-
-
-
- {isTransportOnly ? : } +
+
+
+
+ {isTransportOnly ? : }
-

+

{isTransportOnly ? 'Transport Fleet Administration' : 'RIT Closed-Loop Admin Console'}

- {isTransportOnly ? 'Transport Manager' : 'Super Admin'}
-

+

{isTransportOnly ? 'Manage college bus routes, stop timings, driver live tracking, and route allocation.' : 'Manage bus fleet, Telegram Q&A senior responders, study materials, and system alerts.'} @@ -1042,33 +1225,90 @@ export default function AdminDashboard() {

-
+
← Main Site
- {/* Tab Navigation */} + {/* Quick KPI Stats Overview Bar */} {!isTransportOnly && ( -
+
+
+
+ +
+
+

Bus Routes

+

{routes.length}

+
+
+ +
+
+ +
+
+

Telegram Seniors

+

+ {(telegramConfig?.seniorHelpers?.length || telegramConfig?.helper_chat_ids?.length || 0)} +

+
+
+ +
+
+ +
+
+

Q&A Queue

+

{questions.length}

+
+
+ +
+
+ +
+
+

Faculty

+

{facultyList.length}

+
+
+ +
+
+ +
+
+

Clubs & CoEs

+

{clubsList.length}

+
+
+
+ )} + + {/* Tab Navigation: Clean Static Grid (No Scrollbar Bar) */} + {!isTransportOnly && ( +
{[ { id: 'transport', label: 'Bus Fleet & Routes', icon: Bus }, { id: 'telegram', label: 'Telegram Q&A Seniors', icon: Bot }, - { id: 'community', label: 'Community Q&A Moderation', icon: MessageCircle }, + { id: 'community', label: 'Community Q&A', icon: MessageCircle }, { id: 'faculty', label: 'Faculty Directory', icon: UserCheck }, { id: 'clubs', label: 'Clubs & Centers', icon: Building2 }, - { id: 'curriculum', label: 'GPA Curriculum & Credits', icon: Calculator }, + { id: 'curriculum', label: 'GPA Curriculum', icon: Calculator }, ].map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; @@ -1076,14 +1316,14 @@ export default function AdminDashboard() { ); })} @@ -1094,29 +1334,44 @@ export default function AdminDashboard() { {/* TAB 1: TRANSPORT & BUS FLEET MANAGEMENT */} {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'transport' && ( -
-
+
+
-

- - College Bus Routes ({routes.length} Active) +

+ + College Bus Routes ({routes.filter(r => (r.status || 'ACTIVE') === 'ACTIVE').length} Active, {routes.filter(r => r.status === 'STOPPED').length} Stopped)

-

- Edit departure times, intermediate stops, colors, or create and delete routes instantly. +

+ Manage active routes, merge low-occupancy buses, or toggle bus status (Active / Stopped).

-
+
+ +
- {/* Route Search Filter */} -
- setBusSearchQuery(e.target.value)} - placeholder="Search routes by number (e.g. R01, RIT-01), name, or origin..." - className="w-full bg-slate-900/80 border border-white/10 rounded-2xl px-4 py-3 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors" - /> + {/* Route Status Filter Tabs & Search */} +
+
+ Show: + + + +
+ +
+ + setBusSearchQuery(e.target.value)} + placeholder="Search routes by number, name, or stop..." + className="w-full bg-slate-50 border border-slate-300 rounded-xl pl-9 pr-3 py-1.5 text-xs text-slate-900 placeholder-slate-400 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" + /> +
{loadingRoutes ? ( -
Loading bus routes...
+
Loading bus routes...
) : routes.length === 0 ? ( -
+
No routes configured yet. Click "Add New Route" to create your first bus route!
) : ( -
+
{routes - .filter( - (r) => + .filter((r) => { + const statusMatch = + gridStatusFilter === 'ALL' || + (gridStatusFilter === 'ACTIVE' && (r.status || 'ACTIVE') === 'ACTIVE') || + (gridStatusFilter === 'STOPPED' && r.status === 'STOPPED'); + + const queryMatch = !busSearchQuery.trim() || r.name.toLowerCase().includes(busSearchQuery.toLowerCase()) || r.number.toLowerCase().includes(busSearchQuery.toLowerCase()) || - r.from.toLowerCase().includes(busSearchQuery.toLowerCase()) - ) + r.from.toLowerCase().includes(busSearchQuery.toLowerCase()); + + return statusMatch && queryMatch; + }) .map((route) => (
{/* Card Header */} -
+
{route.number} - {route.name} + {route.name}
-
+
+ {/* Toggle Status Pill */} + +
{/* Route Path */} -
-
- Origin: - {route.from} ({route.departureTime}) +
+
+ Origin: + {route.from} ({route.departureTime})
-
- Destination: - {route.to} ({route.arrivalTime}) +
+ Destination: + {route.to} ({route.arrivalTime})
{/* Stops Preview */} -
- +
+ Intermediate Stops ({route.stops?.length || 0}) -
+
{route.stops && route.stops.length > 0 ? ( route.stops.map((stop, i) => ( -
- {stop.name} - {stop.time} +
+ {stop.name} + {stop.time}
)) ) : ( -

No intermediate stops configured

+

No intermediate stops configured

)}
-
- Driver PIN: RITDRIVER - - - Live Sync +
+ Driver PIN: RITDRIVER + + + {(route.status || 'ACTIVE') === 'ACTIVE' ? 'Live Sync' : 'Suspended'}
@@ -1239,31 +1562,31 @@ export default function AdminDashboard() { {/* TAB 2: TELEGRAM Q&A SENIORS & BOT CONFIGURATION */} {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'telegram' && ( -
-
+
+
{/* Left 7 Cols: Active Senior Responders */} -
+
-

- +

+ Telegram Senior Responders (Q&A Network)

-

+

When a fresher posts a question on the portal, these Telegram users receive instant alert messages and can answer directly from Telegram.

{/* Add Senior Form */} -
- Add New Senior Responder -
+ + Add New Senior Responder +
setNewHelperChatId(e.target.value)} placeholder="Telegram Chat ID (e.g. 971749136)" - className="bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-500 font-mono outline-none focus:border-orange-500" + className="bg-white border border-slate-300 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 font-mono outline-none focus:border-[#FF6B00] focus:ring-2 focus:ring-orange-500/20" required /> setNewHelperName(e.target.value)} placeholder="Senior Name / Note (Optional)" - className="bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500" + className="bg-white border border-slate-300 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 outline-none focus:border-[#FF6B00] focus:ring-2 focus:ring-orange-500/20 font-medium" />
@@ -1290,8 +1613,8 @@ export default function AdminDashboard() { : (telegramConfig?.helper_chat_ids || []).map(id => ({ chatId: id, name: '' })); return ( -
- +
+ Active Senior Responders ({helpersList.length}) @@ -1300,31 +1623,31 @@ export default function AdminDashboard() { helpersList.map((helper) => (
-
+
TG
-

- Chat ID: {helper.chatId} {helper.name ? ({helper.name}) : ''} +

+ Chat ID: {helper.chatId} {helper.name ? ({helper.name}) : ''}

-

Receives questions from freshers in real-time

+

Receives questions from freshers in real-time

)) ) : ( -

No senior helpers configured yet. Enter a Chat ID above to add!

+

No senior helpers configured yet. Enter a Chat ID above to add!

)}
@@ -1333,14 +1656,14 @@ export default function AdminDashboard() {
{/* Right 5 Cols: How-To Guide & Bot Tokens */} -
+
{/* Guide Card */} -
-

- +
+

+ How Seniors Get Their Chat ID

-
    +
    1. Open Telegram and search for @userinfobot.
    2. Click Start or send any message.
    3. Copy the numeric Id (e.g. 971749136).
    4. @@ -1350,36 +1673,36 @@ export default function AdminDashboard() { {/* Live Config Editor */} {telegramConfig && ( -
      -

      - + +

      + Live Telegram Bot Config (VPS)

      - + setTelegramConfig({ ...telegramConfig, community_bot_token: e.target.value })} - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]" />
      - + setTelegramConfig({ ...telegramConfig, spring_backend_url: e.target.value })} - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]" />
      @@ -1391,34 +1714,32 @@ export default function AdminDashboard() {

)} - - {/* ───────────────────────────────────────────────────────────── */} - {/* TAB 4: COMMUNITY QUESTIONS MODERATION */} + {/* TAB 3: COMMUNITY QUESTIONS MODERATION */} {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'community' && ( -
+
-

- +

+ Community Q&A Questions ({questions.length})

-

Moderate and delete inappropriate or spam questions.

+

Moderate and delete inappropriate or spam questions.

{loadingQuestions ? ( -
Loading community questions...
+
Loading community questions...
) : questions.length === 0 ? ( -
+
No community questions found in database.
) : ( @@ -1426,24 +1747,24 @@ export default function AdminDashboard() { {questions.map((q) => (
-

{q.title}

-

{q.body}

-
- Asked by: {q.authorName} +

{q.title}

+

{q.body}

+
+ Asked by: {q.authorName} • - Votes: {q.votes} + Votes: {q.votes}
))} @@ -1452,25 +1773,23 @@ export default function AdminDashboard() {
)} - - {/* ───────────────────────────────────────────────────────────── */} - {/* TAB 6: FACULTY DIRECTORY MANAGEMENT */} + {/* TAB 4: FACULTY DIRECTORY MANAGEMENT */} {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'faculty' && ( -
-
+
+
-

- +

+ Faculty Directory Management ({facultyList.length})

-

+

Add, update, or remove faculty profiles, cabin office hours, designations, and department assignments.

-
+
setFacultySearch(e.target.value)} placeholder="Search faculty..." - className="bg-slate-950 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500" + className="bg-slate-50 border border-slate-300 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-900 placeholder-slate-400 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
@@ -1502,35 +1821,35 @@ export default function AdminDashboard() { .map((fac) => (
-

{fac.name}

- {fac.designation} +

{fac.name}

+ {fac.designation}
-
-

🎓 {fac.department}

- {fac.email &&

✉️ {fac.email}

} - {(fac.office || fac.cabin) &&

📍 Cabin: {fac.office || fac.cabin}

} - {fac.specialization &&

⚡ {fac.specialization}

} +
+

🎓 {fac.department}

+ {fac.email &&

✉️ {fac.email}

} + {(fac.office || fac.cabin) &&

📍 Cabin: {fac.office || fac.cabin}

} + {fac.specialization &&

⚡ {fac.specialization}

}
@@ -1540,22 +1859,22 @@ export default function AdminDashboard() { )} {/* ───────────────────────────────────────────────────────────── */} - {/* TAB 7: CLUBS & CENTERS OF EXCELLENCE MANAGEMENT */} + {/* TAB 5: CLUBS & CENTERS OF EXCELLENCE MANAGEMENT */} {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'clubs' && ( -
-
+
+
-

- +

+ Clubs & Centers of Excellence ({clubsList.length})

-

+

Manage student clubs, 15 Future Tech Centers of Excellence (CoEs), faculty lead contacts, and descriptions.

-
+
setClubsSearch(e.target.value)} placeholder="Search clubs & centers..." - className="bg-slate-950 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500" + className="bg-slate-50 border border-slate-300 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-900 placeholder-slate-400 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
@@ -1587,44 +1906,44 @@ export default function AdminDashboard() { .map((club) => (
-
- + {club.type || (club.category === 'Center of Excellence' ? 'Center' : 'Club')}
-

{club.name}

+

{club.name}

-

{club.description}

+

{club.description}

-
- {club.coordinatorName &&

👤 Lead: {club.coordinatorName}

} - {club.category &&

🏷️ Category: {club.category}

} +
+ {club.coordinatorName &&

👤 Lead: {club.coordinatorName}

} + {club.category &&

🏷️ Category: {club.category}

}
))} @@ -1633,39 +1952,39 @@ export default function AdminDashboard() { )} {/* ───────────────────────────────────────────────────────────── */} - {/* TAB 8: GPA CALCULATOR CURRICULUM MANAGEMENT */} + {/* TAB 6: GPA CALCULATOR CURRICULUM MANAGEMENT */} {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'curriculum' && ( -
-
+
+
-

- +

+ Department Courses & Credits Management (GPA Calculator)

-

+

Assign and customize course codes, titles, and credit weightages for individual departments & semesters used in GPA calculations.

{/* Department & Semester Selectors */} -
+
- + setSelectedGpaSem(Number(e.target.value))} - className="w-full bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white font-semibold outline-none focus:border-orange-500" + className="w-full bg-white border border-slate-300 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-bold outline-none focus:border-[#FF6B00]" > {[1, 2, 3, 4, 5, 6, 7, 8].map((sem) => ( @@ -1688,13 +2007,13 @@ export default function AdminDashboard() {
{/* Courses Table for Selected Department & Semester */} -
+
{(() => { const currentSemCourses = (curriculumMap[selectedGpaDept] && curriculumMap[selectedGpaDept][selectedGpaSem]) || (DEPARTMENT_CURRICULUM[selectedGpaDept] && DEPARTMENT_CURRICULUM[selectedGpaDept][selectedGpaSem]) || []; return ( <> - + Configured Courses for {selectedGpaDept} - Semester {selectedGpaSem} ({currentSemCourses.length}) @@ -1703,35 +2022,35 @@ export default function AdminDashboard() { currentSemCourses.map((course: any, idx: number) => (
-
+
{course.credits} Cr
-

+

{course.name} {course.isElective && ( - + Elective Slot )}

-

Credit Weightage: {course.credits} Units

+

Credit Weightage: {course.credits} Units

-
+
@@ -1739,7 +2058,7 @@ export default function AdminDashboard() {
)) ) : ( -

+

No custom courses configured for {selectedGpaDept} Semester {selectedGpaSem} yet. Click "Add Course" above!

)} @@ -1759,74 +2078,74 @@ export default function AdminDashboard() { {/* ───────────────────────────────────────────────────────────── */} {isRouteModalOpen && ( -
+
-
-

- +
+

+ {editingRoute ? `Edit Route ${editingRoute.number}` : 'Create New Bus Route'}

-
+
- + setRouteFormData({ ...routeFormData, number: e.target.value })} placeholder="e.g. RIT-05" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-bold" required />
- + setRouteFormData({ ...routeFormData, name: e.target.value })} placeholder="e.g. Tambaram Super Express" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" required />
-
+
- + setRouteFormData({ ...routeFormData, from: e.target.value })} placeholder="e.g. Tambaram" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" required />
- + setRouteFormData({ ...routeFormData, to: e.target.value })} placeholder="e.g. RIT Campus" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" required />
@@ -1834,47 +2153,47 @@ export default function AdminDashboard() {
- + setRouteFormData({ ...routeFormData, departureTime: e.target.value })} placeholder="07:00 AM" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white font-mono outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-mono outline-none focus:bg-white focus:border-[#FF6B00]" />
- + setRouteFormData({ ...routeFormData, arrivalTime: e.target.value })} placeholder="08:30 AM" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white font-mono outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-mono outline-none focus:bg-white focus:border-[#FF6B00]" />
- + setRouteFormData({ ...routeFormData, color: e.target.value })} placeholder="#FF6B00" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2.5 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]" />
{/* Stops Builder */} -
+
- + @@ -1919,17 +2238,17 @@ export default function AdminDashboard() {
-
+
@@ -1945,97 +2264,97 @@ export default function AdminDashboard() { {/* ───────────────────────────────────────────────────────────── */} {isFacultyModalOpen && ( -
+
-
-

- +
+

+ {editingFaculty ? `Edit Faculty: ${editingFaculty.name}` : 'Add New Faculty Member'}

- +
- +
- + setFacultyFormData({ ...facultyFormData, name: e.target.value })} placeholder="e.g. Dr. ARTHI A." - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" required />
-
+
- + setFacultyFormData({ ...facultyFormData, designation: e.target.value })} placeholder="e.g. Associate Professor & HOD" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
- + setFacultyFormData({ ...facultyFormData, department: e.target.value })} placeholder="Computer Science & Engineering" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" required />
-
+
- + setFacultyFormData({ ...facultyFormData, email: e.target.value })} placeholder="arthi.a@ritchennai.edu.in" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
- + setFacultyFormData({ ...facultyFormData, office: e.target.value })} placeholder="CSE HOD Cabin, 2nd Floor" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
- + setFacultyFormData({ ...facultyFormData, specialization: e.target.value })} placeholder="Machine Learning, Deep Learning, Cloud Computing" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
-
- - +
+ +
@@ -2048,41 +2367,41 @@ export default function AdminDashboard() { {/* ───────────────────────────────────────────────────────────── */} {isClubModalOpen && ( -
+
-
-

- +
+

+ {editingClub ? `Edit: ${editingClub.name}` : 'Add New Club / Center of Excellence'}

- +
-
+
- + setClubFormData({ ...clubFormData, name: e.target.value })} placeholder="e.g. Center for Artificial Intelligence & Deep Learning" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" required />
-
+
- + setClubFormData({ ...clubFormData, category: e.target.value })} placeholder="Center of Excellence / Technical / Cultural" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500" + className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-sm text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00] font-medium" />
- +