diff --git a/jportal/src/App.jsx b/jportal/src/App.jsx index 28b26b9..a532fec 100644 --- a/jportal/src/App.jsx +++ b/jportal/src/App.jsx @@ -1,5 +1,11 @@ import { useState, useEffect, useRef } from "react"; -import { HashRouter as Router, Routes, Route, Navigate, useNavigate } from "react-router-dom"; +import { + HashRouter as Router, + Routes, + Route, + Navigate, + useNavigate, +} from "react-router-dom"; import Header from "./components/Header"; import Navbar from "./components/Navbar"; import Login from "./components/Login"; @@ -15,7 +21,10 @@ import { DynamicFontLoader } from "./components/DynamicFontLoader"; import { Toaster } from "./components/ui/sonner"; import "./App.css"; -import { WebPortal, LoginError } from "https://cdn.jsdelivr.net/npm/jsjiit@0.0.23/dist/jsjiit.esm.js"; +import { + WebPortal, + LoginError, +} from "https://cdn.jsdelivr.net/npm/jsjiit@0.0.23/dist/jsjiit.esm.js"; import MockWebPortal from "./components/MockWebPortal"; import { TriangleAlert } from "lucide-react"; @@ -30,6 +39,7 @@ const mockPortal = new MockWebPortal(); // Create a wrapper component to use the useNavigate hook function AuthenticatedApp({ w, setIsAuthenticated, setIsDemoMode }) { + const headerRef = useRef(null); const [activeAttendanceTab, setActiveAttendanceTab] = useState("overview"); const [attendanceData, setAttendanceData] = useState({}); const [attendanceSemestersData, setAttendanceSemestersData] = useState(null); @@ -50,9 +60,11 @@ function AuthenticatedApp({ w, setIsAuthenticated, setIsDemoMode }) { const [selectedSubjectsSem, setSelectedSubjectsSem] = useState(null); const [attendanceDailyDate, setAttendanceDailyDate] = useState(new Date()); - const [isAttendanceCalendarOpen, setIsAttendanceCalendarOpen] = useState(false); + const [isAttendanceCalendarOpen, setIsAttendanceCalendarOpen] = + useState(false); const [isAttendanceTrackerOpen, setIsAttendanceTrackerOpen] = useState(false); - const [attendanceSubjectCacheStatus, setAttendanceSubjectCacheStatus] = useState({}); + const [attendanceSubjectCacheStatus, setAttendanceSubjectCacheStatus] = + useState({}); // Add attendance goal state const [attendanceGoal, setAttendanceGoal] = useState(() => { @@ -104,29 +116,16 @@ function AuthenticatedApp({ w, setIsAuthenticated, setIsDemoMode }) { const [isAttendanceMetaLoading, setIsAttendanceMetaLoading] = useState(true); const [isAttendanceDataLoading, setIsAttendanceDataLoading] = useState(true); - // Ref to measure header height - const headerRef = useRef(null); - - // Measure header height and set CSS variable - useEffect(() => { - const updateHeaderHeight = () => { - if (headerRef.current) { - const height = headerRef.current.offsetHeight; - document.documentElement.style.setProperty('--header-height', `${height}px`); - } - }; - - // Update on mount and when window resizes - updateHeaderHeight(); - window.addEventListener('resize', updateHeaderHeight); - - return () => window.removeEventListener('resize', updateHeaderHeight); - }, []); + // Add new state for fees + const [feesData, setFeesData] = useState(null); return (
-
+
} /> @@ -245,7 +244,16 @@ function AuthenticatedApp({ w, setIsAuthenticated, setIsDemoMode }) { /> } /> - } /> + + } + />
@@ -271,7 +279,13 @@ function LoginWrapper({ onLoginSuccess, onDemoLogin, w }) { }, 100); }; - return ; + return ( + + ); } function App() { @@ -299,10 +313,17 @@ function App() { } catch (error) { if ( error instanceof LoginError && - error.message.includes("JIIT Web Portal server is temporarily unavailable") + error.message.includes( + "JIIT Web Portal server is temporarily unavailable" + ) + ) { + setError( + "JIIT Web Portal server is temporarily unavailable. Please try again later." + ); + } else if ( + error instanceof LoginError && + error.message.includes("Failed to fetch") ) { - setError("JIIT Web Portal server is temporarily unavailable. Please try again later."); - } else if (error instanceof LoginError && error.message.includes("Failed to fetch")) { setError( "Please check your internet connection. If connected, JIIT Web Portal server is temporarily unavailable." ); @@ -377,7 +398,11 @@ function App() { path="*" element={ <> - {error &&
{error}
} + {error && ( +
+ {error} +
+ )} { - onDemoLogin(); - }; + // Handle demo login + const handleDemoLogin = () => { + onDemoLogin(); + }; - // Handle side effects in useEffect - useEffect(() => { - if (!loginStatus.credentials) return; + // Handle side effects in useEffect + useEffect(() => { + if (!loginStatus.credentials) return; - const performLogin = async () => { - try { - await w.student_login(loginStatus.credentials.enrollmentNumber, loginStatus.credentials.password); + const performLogin = async () => { + try { + await w.student_login( + loginStatus.credentials.enrollmentNumber, + loginStatus.credentials.password + ); - // Store credentials in localStorage - localStorage.setItem("username", loginStatus.credentials.enrollmentNumber); - localStorage.setItem("password", loginStatus.credentials.password); + // Store credentials in localStorage + localStorage.setItem( + "username", + loginStatus.credentials.enrollmentNumber + ); + localStorage.setItem("password", loginStatus.credentials.password); - console.log("Login successful"); - setLoginStatus((prev) => ({ - ...prev, - isLoading: false, - credentials: null, - })); - onLoginSuccess(); - } catch (error) { - if ( - error instanceof LoginError && - error.message.includes("JIIT Web Portal server is temporarily unavailable") - ) { - console.error("JIIT Web Portal server is temporarily unavailable"); - toast.error("JIIT Web Portal server is temporarily unavailable. Please try again later."); - } else if (error instanceof LoginError && error.message.includes("Failed to fetch")) { - toast.error("Please check your internet connection. If connected, JIIT Web Portal server is unavailable."); - } else { - console.error("Login failed:", error); - toast.error("Login failed. Please check your credentials."); - } - setLoginStatus((prev) => ({ - ...prev, - isLoading: false, - credentials: null, - })); - } - }; + console.log("Login successful"); + setLoginStatus((prev) => ({ + ...prev, + isLoading: false, + credentials: null, + })); + onLoginSuccess(); + } catch (error) { + if ( + error instanceof LoginError && + error.message.includes( + "JIIT Web Portal server is temporarily unavailable" + ) + ) { + console.error("JIIT Web Portal server is temporarily unavailable"); + toast.error( + "JIIT Web Portal server is temporarily unavailable. Please try again later." + ); + } else if ( + error instanceof LoginError && + error.message.includes("Failed to fetch") + ) { + toast.error( + "Please check your internet connection. If connected, JIIT Web Portal server is unavailable." + ); + } else { + console.error("Login failed:", error); + toast.error("Login failed. Please check your credentials."); + } + setLoginStatus((prev) => ({ + ...prev, + isLoading: false, + credentials: null, + })); + } + }; - setLoginStatus((prev) => ({ ...prev, isLoading: true })); - performLogin(); - }, [loginStatus.credentials, onLoginSuccess, w]); + setLoginStatus((prev) => ({ ...prev, isLoading: true })); + performLogin(); + }, [loginStatus.credentials, onLoginSuccess, w]); - // Clean form submission - function onSubmit(values) { - setLoginStatus((prev) => ({ - ...prev, - credentials: values, - })); - } + // Clean form submission + function onSubmit(values) { + setLoginStatus((prev) => ({ + ...prev, + credentials: values, + })); + } - return ( -
- {/* Header with theme toggle */} -
- -
+ return ( +
+ {/* Header with theme toggle */} +
+ +
- - - Login - Enter your credentials to sign in - - -
- - ( - - Enrollment Number - - - - - - )} - /> - ( - - Password - -
- - -
-
- -
- )} - /> - -
-
- -
-
- Or -
-
- - - -
-
-
- ); + + + Login + Enter your credentials to sign in + + +
+ + ( + + Enrollment Number + + + + + + )} + /> + ( + + Password + +
+ + +
+
+ +
+ )} + /> + +
+
+ +
+
+ Or +
+
+ + + +
+
+
+ ); } diff --git a/jportal/src/components/Profile.jsx b/jportal/src/components/Profile.jsx index 5aaa136..73bdf40 100644 --- a/jportal/src/components/Profile.jsx +++ b/jportal/src/components/Profile.jsx @@ -1,289 +1,584 @@ import React, { useState, useEffect } from "react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { Image, UserRound, GraduationCap, IdCard, Mail, BookOpen, Users, Calendar } from "lucide-react"; - +import { + Image, + UserRound, + GraduationCap, + IdCard, + Mail, + BookOpen, + Users, + Calendar, + CheckCircle, +} from "lucide-react"; export default function Profile({ w, profileData, setProfileData }) { - const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState("personal"); - const [showProfilePhoto, setShowProfilePhoto] = useState(false); - - useEffect(() => { - const fetchProfileData = async () => { - // Return early if data is already cached - if (profileData) { - setLoading(false); - return; - } - - setLoading(true); - try { - const data = await w.get_personal_info(); - setProfileData(data); - } catch (error) { - console.error("Failed to fetch profile data:", error); - } finally { - setLoading(false); - } - }; - - fetchProfileData(); - }, [w, profileData, setProfileData]); - - if (loading) { - return ( -
- Loading profile... -
- ); - } - - const info = profileData?.generalinformation || {}; - const qualifications = profileData?.qualification || []; - - // Prepare profile/avatar image (mobile header) - const photoData = profileData?.["photo&signature"]?.photo; - const hasProfilePhoto = Boolean(photoData); - const profileImg = hasProfilePhoto - ? `data:image/jpeg;base64,${photoData}` - : null; - - // Helper function to get initials from name - const getInitials = (name) => { - if (!name) return "U"; - const nameParts = name.trim().split(/\s+/); - if (nameParts.length === 1) { - // Single name - take first 2 letters - return nameParts[0].substring(0, 2).toUpperCase(); - } - // Multiple names - take first letter of first and last name - const firstName = nameParts[0]; - const lastName = nameParts[nameParts.length - 1]; - return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase(); - }; - - const initials = getInitials(info.studentname); - - return ( -
- {/* Profile Header Card */} -
- {hasProfilePhoto && ( - - )} - -
- {/* Avatar */} -
- {showProfilePhoto && hasProfilePhoto ? ( - Profile - ) : ( -
- {initials} -
- )} -
- - {/* Info */} -
-

- {info.studentname || "N/A"} -

- -
-
- - {info.programcode || "N/A"} - - {info.registrationno || "N/A"} -
- -
- - {info.studentemailid || "N/A"} -
-
-
-
- - {/* Bottom Row: Semester, Section, Batch */} -
-
- - Sem: - {info.semester || "N/A"} -
- -
- - Sec: - {info.sectioncode || "N/A"} -
- -
- - Batch: - {info.batch || "N/A"} -
-
-
- - {/* Tabs Bar (mobile) */} - - - - Personal - - - Academic - - - Contact - - - Education - - - - {/* Personal Information */} - -
-

Personal Information

-
- - - - - -
-
-
- - {/* Academic Information */} - -
-

Academic Information

-
- - - - - - - - -
-
-
- - {/* Contact + Family + Address */} - -
-

Contact Information

-
- - - - -
-
- -
-

Family Information

-
- - - - - -
-
- -
-

Current Address

-
- - - - - -
-
- -
-

Permanent Address

-
- - - - - -
-
-
- - {/* Educational Qualifications */} - -
-

Educational Qualifications

- {qualifications.map((qual, index) => ( -
- - - - - - - {qual.grade && } -
- ))} -
-
-
- -
- Made with Big 🍆 Energy by{" "} - - Yash Malik - -
-
- ); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState("personal"); + const [showProfilePhoto, setShowProfilePhoto] = useState(false); + + // Fees and Fines state + const [feesData, setFeesData] = useState({ fines: [], summary: {} }); + const [feesLoading, setFeesLoading] = useState(true); + + useEffect(() => { + const fetchProfileData = async () => { + // Return early if data is already cached + if (profileData) { + setLoading(false); + return; + } + + setLoading(true); + try { + const data = await w.get_personal_info(); + setProfileData(data); + } catch (error) { + console.error("Failed to fetch profile data:", error); + } finally { + setLoading(false); + } + }; + + fetchProfileData(); + }, [w, profileData, setProfileData]); + + // Fetch fees data separately + useEffect(() => { + const fetchFeesData = async () => { + setFeesLoading(true); + try { + const [finesData, summaryData] = await Promise.all([ + w.get_fines_msc_charges().catch((err) => { + if (err.message?.includes("NO APPROVED REQUEST FOUND")) { + return []; + } + throw err; + }), + w.get_fee_summary(), + ]); + + setFeesData({ + fines: Array.isArray(finesData) ? finesData : [], + summary: summaryData || {}, + }); + } catch (error) { + console.error("Failed to fetch fees data for profile:", error); + } finally { + setFeesLoading(false); + } + }; + + fetchFeesData(); + }, [w]); + + if (loading) { + return ( +
+ Loading profile... +
+ ); + } + + const info = profileData?.generalinformation || {}; + const qualifications = profileData?.qualification || []; + + // Prepare profile/avatar image (mobile header) + const photoData = profileData?.["photo&signature"]?.photo; + const hasProfilePhoto = Boolean(photoData); + const profileImg = hasProfilePhoto + ? `data:image/jpeg;base64,${photoData}` + : null; + + // Helper function to get initials from name + const getInitials = (name) => { + if (!name) return "U"; + const nameParts = name.trim().split(/\s+/); + if (nameParts.length === 1) { + // Single name - take first 2 letters + return nameParts[0].substring(0, 2).toUpperCase(); + } + // Multiple names - take first letter of first and last name + const firstName = nameParts[0]; + const lastName = nameParts[nameParts.length - 1]; + return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase(); + }; + + const initials = getInitials(info.studentname); + + // Fees Helpers + const formatCurrency = (amount) => { + if (amount === null || amount === undefined) return "N/A"; + const num = parseFloat(amount); + if (isNaN(num)) return "N/A"; + return `₹${num.toLocaleString("en-IN", { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + })}`; + }; + + const finesArray = feesData.fines || []; + const summaryData = feesData.summary || {}; + const feeHeads = summaryData.feeHeads || []; + + const totalFines = finesArray.reduce((sum, fine) => { + return ( + sum + (parseFloat(fine.charge) || parseFloat(fine.feeamounttobepaid) || 0) + ); + }, 0); + + const totalFeeAmount = feeHeads.reduce( + (sum, head) => sum + (parseFloat(head.feeamount) || 0), + 0 + ); + const totalReceived = feeHeads.reduce( + (sum, head) => sum + (parseFloat(head.receiveamount) || 0), + 0 + ); + const totalDue = feeHeads.reduce( + (sum, head) => sum + (parseFloat(head.dueamount) || 0), + 0 + ); + const totalRefund = feeHeads.reduce( + (sum, head) => sum + (parseFloat(head.refundamount) || 0), + 0 + ); + const advanceAmount = summaryData.advanceamount?.[0]?.amount || 0; + + return ( +
+ {/* Profile Header Card */} +
+ {hasProfilePhoto && ( + + )} + +
+ {/* Avatar */} +
+ {showProfilePhoto && hasProfilePhoto ? ( + Profile + ) : ( +
+ {initials} +
+ )} +
+ + {/* Info */} +
+

+ {info.studentname || "N/A"} +

+ +
+
+ + {info.programcode || "N/A"} + + + {info.registrationno || "N/A"} + +
+ +
+ + + {info.studentemailid || "N/A"} + +
+
+
+
+ + {/* Bottom Row: Semester, Section, Batch */} +
+
+ + Sem: + + {info.semester || "N/A"} + +
+ +
+ + Sec: + + {info.sectioncode || "N/A"} + +
+ +
+ + Batch: + + {info.batch || "N/A"} + +
+
+
+ + {/* Tabs Bar (mobile) */} + + + + Personal + + + Academic + + + Contact + + + Education + + + Fines + + + Fees + + + + {/* Personal Information */} + +
+

Personal Information

+
+ + + + + +
+
+
+ + {/* Academic Information */} + +
+

Academic Information

+
+ + + + + + + + +
+
+
+ + {/* Contact + Family + Address */} + +
+

Contact Information

+
+ + + + +
+
+ +
+

Family Information

+
+ + + + + +
+
+ +
+

Current Address

+
+ + + + + +
+
+ +
+

Permanent Address

+
+ + + + + +
+
+
+ + {/* Educational Qualifications */} + +
+

+ Educational Qualifications +

+ {qualifications.map((qual, index) => ( +
+ + + + + + + {qual.grade && } +
+ ))} +
+
+ + {/* Pending Fines */} + + {feesLoading ? ( +
Loading fines data...
+ ) : finesArray.length > 0 ? ( + <> +
+
+ Total Pending + + {formatCurrency(totalFines)} + +
+
+ {finesArray.map((fine, index) => ( +
+

+ {fine.servicename || "Charge"} +

+
+ + + {fine.quantity && ( + + )} + {fine.remarksbystudents && ( + + )} +
+
+ ))} + + ) : ( +
+ +

No Pending Fines

+

+ You are all caught up! +

+
+ )} +
+ + {/* Fees Summary */} + + {feesLoading ? ( +
Loading fees data...
+ ) : ( + <> +
+
+
+ Total Fee +
+
+ {formatCurrency(totalFeeAmount)} +
+
+
+
+ Total Received +
+
+ {formatCurrency(totalReceived)} +
+
+
+
+ Total Due +
+
+ {formatCurrency(totalDue)} +
+
+ {(advanceAmount > 0 || totalRefund > 0) && ( +
+
+ {advanceAmount > 0 ? "Advance" : "Refund"} +
+
+ {formatCurrency( + advanceAmount > 0 ? advanceAmount : totalRefund + )} +
+
+ )} +
+ + {feeHeads.length > 0 && ( +
+

+ Fee Breakdown +

+
+ {feeHeads.map((head, idx) => ( +
+
+
+
+ {head.stynumber + ? `Semester ${head.stynumber}` + : "N/A"} +
+
+ {head.academicyear} +
+
+
+ {head.stytypedesc || "Regular"} +
+
+
+ + + + {parseFloat(head.refundamount) > 0 && ( + + )} +
+
+ ))} +
+
+ )} + + )} +
+
+ +
+ Made with Big 🍆 Energy by{" "} + + Yash Malik + +
+
+ ); } // Helper component for consistent info display function InfoRow({ label, value }) { - return ( -
- {label}: - {value || "N/A"} -
- ); -} \ No newline at end of file + return ( +
+ + {label}: + + + {value || "N/A"} + +
+ ); +}