setGpaStep(1)}
+ onClick={() => {
+ if (calculationType === 'internal') {
+ setCalculationType(null);
+ setGpaStep(1);
+ } else {
+ setGpaStep(1);
+ }
+ }}
className="inline-flex items-center gap-1.5 text-xs font-bold text-[#FF6B00] hover:text-[#EA580C] transition-colors mb-2 cursor-pointer"
>
- Back to Selection
+ {calculationType === 'internal' ? 'Back to Options' : 'Back to Department & Semester'}
- {calculationType === 'gpa' ? 'GPA Calculation' : 'Internal Marks Calculation'}
+ {calculationType === 'gpa' ? 'GPA Calculation' : 'Internal Mark Calculator'}
-
- {gpaDepartment} — {gpaSemester}
-
+ {calculationType === 'gpa' && (
+
+ {gpaDepartment} — {gpaSemester}
+
+ )}
- {(() => {
- 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}
-
-
{
- const newCode = e.target.value;
- const found = PROFESSIONAL_ELECTIVES_LIST.find((el) => el.code === newCode);
- const updated = [...subjects];
- updated[idx].electiveCode = newCode;
- updated[idx].name = found ? `${found.code} - ${found.name}` : newCode;
- setSubjects(updated);
- setCalculatedScore(null);
- }}
- className="w-full px-3 py-2 rounded-xl bg-white border border-[#CBD5E1] focus:border-[#FF6B00] text-xs font-bold text-[#1E293B] outline-none cursor-pointer"
- style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
- >
- {PROFESSIONAL_ELECTIVES_LIST.filter((opt) => opt.code === sub.electiveCode || !selectedElectiveCodes.has(opt.code)).map((opt) => (
-
- {opt.code} - {opt.name}
-
- ))}
-
-
- ) : (
-
- {sub.name}
-
- )}
-
- {sub.credits} {sub.credits === 1 ? 'Credit' : 'Credits'}
-
-
-
- {
- const updated = [...subjects];
- updated[idx].grade = e.target.value;
- setSubjects(updated);
- setCalculatedScore(null);
- }}
- className="w-full px-3.5 py-2.5 rounded-xl bg-white border border-[#CBD5E1] focus:border-[#FF6B00] focus:ring-2 focus:ring-[#FF6B00]/20 text-xs font-bold text-[#1E293B] outline-none cursor-pointer shadow-2xs"
- style={{ fontFamily: 'Inter, sans-serif' }}
- >
- O
- A+
- A
- B+
- B
- C
- UA
-
-
+ {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].name = e.target.value;
+ updated[idx].grade = e.target.value;
setSubjects(updated);
setCalculatedScore(null);
+ setInternalError(null);
}}
- placeholder={`Subject ${idx + 1} Name`}
- className="flex-1 px-3 py-2 rounded-xl bg-white border border-[#CBD5E1] text-xs font-bold text-[#1E293B] focus:border-[#FF6B00] outline-none"
- />
- {subjects.length > 1 && (
- {
- setSubjects(subjects.filter((_, i) => i !== idx));
- setCalculatedScore(null);
- }}
- className="p-2 rounded-xl text-[#EF4444] hover:bg-[#FEF2F2] transition-colors cursor-pointer"
- title="Remove subject"
- >
-
-
- )}
-
-
-
-
- CAT 1 (/50)
- {
- 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"
- />
-
-
- CAT 2 (/50)
- {
- 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"
- />
-
-
- Assignment (/10)
- {
- 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"
- />
-
-
- Attendance %
- {
- 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' }}
+ >
+
Select Grade
+
O
+
A+
+
A
+
B+
+
B
+
C
+
UA
+
))}
+
+ ) : (
+ /* ── 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);
-
{
- setSubjects([
- ...subjects,
- { name: `Subject ${subjects.length + 1}`, credits: 3, grade: 'A', cat1: '40', cat2: '40', assignment: '8', attendance: '90' }
- ]);
- setCalculatedScore(null);
- }}
- className="w-full py-2.5 rounded-2xl border-2 border-dashed border-[#CBD5E1] hover:border-[#FF6B00] text-xs font-bold text-[#64748B] hover:text-[#FF6B00] transition-colors flex items-center justify-center gap-1.5 cursor-pointer"
- >
- Add Subject
-
+ 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
+
+ ) : (
+
+ {
+ if (cat1Input !== '' && !isCat1Err) {
+ setShowWeightages((prev) => ({ ...prev, cat1: true }));
+ }
+ }}
+ className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
+ >
+ Calculate Weightage
+
+ {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
+
+ ) : (
+
+ {
+ if (cat2Input !== '' && !isCat2Err) {
+ setShowWeightages((prev) => ({ ...prev, cat2: true }));
+ }
+ }}
+ className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
+ >
+ Calculate Weightage
+
+ {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
+
+ ) : (
+
+ {
+ if (cat3Input !== '' && !isCat3Err) {
+ setShowWeightages((prev) => ({ ...prev, cat3: true }));
+ }
+ }}
+ className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
+ >
+ Calculate Weightage
+
+ {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
+
+ ) : (
+
+ {
+ if (assignmentInput !== '' && !isAssErr) {
+ setShowWeightages((prev) => ({ ...prev, assignment: true }));
+ }
+ }}
+ className="px-3 py-1.5 rounded-xl bg-[#7C3AED]/10 hover:bg-[#7C3AED]/20 text-[#7C3AED] text-[11px] font-extrabold flex items-center gap-1.5 transition-colors cursor-pointer"
+ >
+ Calculate Weightage
+
+ {showWeightages['assignment'] && !isNaN(nAss) && nAss >= 0 && nAss <= 50 && (
+
+ Obtained: {((nAss / 50) * 11.43).toFixed(2)} / 11.43
+
+ )}
+
+ )}
+
+
+
+ );
+ })()
+ )}
+
+ {internalError && (
+
)}
@@ -1277,26 +1434,52 @@ export default function Toolkit() {
{
if (calculationType === 'gpa') {
+ const unselected = subjects.filter(s => s.credits > 0 && !s.grade);
+ if (unselected.length > 0) {
+ setInternalError('Please select a grade for all subjects before calculating GPA.');
+ setCalculatedScore(null);
+ return;
+ }
+ setInternalError(null);
const GRADE_MAP: Record = { 'O': 10, 'A+': 9, 'A': 8, 'B+': 7, 'B': 6, 'C': 5, 'UA': 0, 'RA': 0 };
let totalPts = 0;
let totalCreds = 0;
subjects.forEach(s => {
- const pts = GRADE_MAP[s.grade] ?? 0;
- const cred = Number(s.credits) || 0;
- totalPts += pts * cred;
- totalCreds += cred;
+ if (s.credits > 0) {
+ const pts = GRADE_MAP[s.grade] ?? 0;
+ const cred = Number(s.credits) || 0;
+ totalPts += pts * cred;
+ totalCreds += cred;
+ }
});
setCalculatedScore(totalCreds > 0 ? (totalPts / totalCreds).toFixed(2) : '0.00');
+ setTimeout(() => {
+ resultCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }, 100);
} else {
- let total = 0;
- subjects.forEach(s => {
- const c1 = (Number(s.cat1) || 0) / 50 * 15;
- const c2 = (Number(s.cat2) || 0) / 50 * 15;
- const ass = (Number(s.assignment) || 0) / 10 * 5;
- const att = (Number(s.attendance) || 0) >= 80 ? 5 : 3;
- total += (c1 + c2 + ass + att);
- });
- setCalculatedScore(subjects.length > 0 ? (total / subjects.length).toFixed(1) : '0.0');
+ const c1 = Number(cat1Input);
+ const c2 = Number(cat2Input);
+ const c3 = Number(cat3Input);
+ const ass = Number(assignmentInput);
+
+ if (
+ cat1Input === '' || isNaN(c1) || c1 < 0 || c1 > 50 ||
+ cat2Input === '' || isNaN(c2) || c2 < 0 || c2 > 25 ||
+ cat3Input === '' || isNaN(c3) || c3 < 0 || c3 > 50 ||
+ assignmentInput === '' || isNaN(ass) || ass < 0 || ass > 50
+ ) {
+ setInternalError('Please enter a valid mark for all fields within their maximum limits.');
+ setCalculatedScore(null);
+ return;
+ }
+
+ setInternalError(null);
+ const totalRaw = c1 + c2 + c3 + ass; // max 175
+ const internal40 = ((totalRaw / 175) * 40).toFixed(2);
+ setCalculatedScore(internal40);
+ setTimeout(() => {
+ resultCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }, 100);
}
}}
className={`w-full py-4 rounded-2xl text-sm font-extrabold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-md ${
@@ -1307,38 +1490,97 @@ export default function Toolkit() {
style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}
>
- {calculationType === 'gpa' ? 'Calculate Final GPA' : 'Calculate Total Internal Marks'}
+ {calculationType === 'gpa' ? 'Calculate Final GPA' : 'Calculate Total Internal Mark'}
{/* 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
+
+
+
+ );
+ })()}
)}