diff --git a/jportal/src/App.jsx b/jportal/src/App.jsx
index 1eb4fab..04f7e1c 100644
--- a/jportal/src/App.jsx
+++ b/jportal/src/App.jsx
@@ -8,6 +8,7 @@ import Grades from "./components/Grades";
import Exams from "./components/Exams";
import Subjects from "./components/Subjects";
import Profile from "./components/Profile";
+import Fees from "./components/Fees";
import Cloudflare from "@/components/Cloudflare";
import { ThemeProvider } from "./components/theme-provider";
import { ThemeScript } from "./components/theme-script";
@@ -15,7 +16,7 @@ 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.20/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";
@@ -99,6 +100,9 @@ function AuthenticatedApp({ w, setIsAuthenticated, setIsDemoMode }) {
const [isAttendanceMetaLoading, setIsAttendanceMetaLoading] = useState(true);
const [isAttendanceDataLoading, setIsAttendanceDataLoading] = useState(true);
+ // Add state for fees data
+ const [feesData, setFeesData] = useState(null);
+
return (
@@ -212,6 +216,10 @@ function AuthenticatedApp({ w, setIsAuthenticated, setIsDemoMode }) {
}
/>
} />
+ }
+ />
@@ -297,6 +305,37 @@ function App() {
setIsDemoMode(true);
};
+ // After authentication (including auto-login), fetch and print fee summary and pending fines
+ useEffect(() => {
+ if (!isAuthenticated) return;
+
+ const portal = isDemoMode ? mockPortal : realPortal;
+
+ const fetchAndLogPayments = async () => {
+ try {
+ const feeSummary = await portal.get_fee_summary();
+ console.log("[Portal] Fee summary:", feeSummary);
+ } catch (err) {
+ console.error("[Portal] Failed to fetch fee summary:", err);
+ }
+
+ try {
+ const fines = await portal.get_fines_msc_charges();
+ console.log("[Portal] Pending miscellaneous charges / fines:", fines);
+ } catch (err) {
+ // The API may return Failure with message "NO APPROVED REQUEST FOUND" when there are no fines
+ if (err && err.message && err.message.includes("NO APPROVED REQUEST FOUND")) {
+ console.info("[Portal] No pending fines found (NO APPROVED REQUEST FOUND).");
+ } else {
+ console.error("[Portal] Failed to fetch pending fines:", err);
+ }
+ }
+ };
+
+ // Do not block UI - fire and forget
+ fetchAndLogPayments();
+ }, [isAuthenticated, isDemoMode]);
+
if (isLoading) {
return (
<>
diff --git a/jportal/src/components/Fees.jsx b/jportal/src/components/Fees.jsx
new file mode 100644
index 0000000..9b29790
--- /dev/null
+++ b/jportal/src/components/Fees.jsx
@@ -0,0 +1,402 @@
+import React, { useState, useEffect } from "react";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/tabs";
+import { useThemeStore } from "@/stores/theme-store";
+import { AlertCircle, CheckCircle, DollarSign, TrendingUp } from "lucide-react";
+
+export default function Fees({ w, feesData, setFeesData, guest = false }) {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [activeTab, setActiveTab] = useState("fines");
+
+ // Subscribe to theme store to ensure re-renders on theme changes
+ const themeState = useThemeStore((state) => state.themeState);
+
+ // Force re-render when theme changes
+ const [, setForceUpdate] = useState({});
+ useEffect(() => {
+ setForceUpdate({});
+ }, [themeState]);
+ // Theme re-render helper (swipe removed)
+
+ useEffect(() => {
+ const fetchFeesData = async () => {
+ // Return early if data is already cached
+ if (feesData) {
+ setLoading(false);
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ try {
+ const [finesData, summaryData] = await Promise.all([
+ w.get_fines_msc_charges().catch((err) => {
+ // If the API returns "NO APPROVED REQUEST FOUND", treat it as empty array
+ if (err.message?.includes("NO APPROVED REQUEST FOUND")) {
+ return [];
+ }
+ throw err;
+ }),
+ w.get_fee_summary(),
+ ]);
+
+ setFeesData({
+ fines: Array.isArray(finesData) ? finesData : [],
+ summary: summaryData || {},
+ });
+
+ // Debug logs
+ try {
+ console.groupCollapsed("Fees: API responses");
+ console.log("finesData:", Array.isArray(finesData) ? finesData : []);
+ console.log("summaryData (raw):", summaryData);
+ console.groupEnd();
+ } catch (e) {
+ console.warn("Failed to log fee data for debug", e);
+ }
+ } catch (error) {
+ console.error("Failed to fetch fees data:", error);
+ setError(error.message || "Failed to load fees data");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchFeesData();
+ }, [w, feesData, setFeesData]);
+
+ if (loading) {
+ return
Loading fees data...
;
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ const finesArray = feesData?.fines || [];
+ const summaryData = feesData?.summary || {};
+ const feeHeads = summaryData.feeHeads || [];
+ const studentInfo = summaryData.studentInfo?.[0] || {};
+ const advanceAmount = summaryData.advanceamount?.[0]?.amount || 0;
+
+ // Helper function to format currency
+ 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,
+ })}`;
+ };
+
+ // Calculate total fines
+ const totalFines = finesArray.reduce((sum, fine) => {
+ return sum + (parseFloat(fine.charge) || parseFloat(fine.feeamounttobepaid) || 0);
+ }, 0);
+
+ // Calculate summary totals from feeHeads
+ 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);
+
+ return (
+
+ {guest && (
+
+ Guest Demo: Viewing Sample Data
+
+ )}
+
+
+
+
+
+ Pending Fines
+
+
+ Fee Summary
+
+
+
+ {/* Pending Fines Tab */}
+
+
+ {finesArray.length > 0 ? (
+ <>
+ {/* Total Fines Summary Card */}
+
+
+
+
Total Fines Pending
+
+ {formatCurrency(totalFines)}
+
+
+
+
+
+
+ {/* Individual Fines */}
+ {finesArray.map((fine, index) => (
+
+
+
+
+ {fine.servicename || "Miscellaneous Charge"}
+
+
+ {fine.remarksbyauthority || "No remarks"}
+
+
+
+ {formatCurrency(fine.charge || fine.feeamounttobepaid)}
+
+
+
+
+ {fine.servicecode && (
+
+
Service
+
+ {fine.servicecode}
+
+
+ )}
+ {fine.requestno && (
+
+
Request No
+
+ {fine.requestno}
+
+
+ )}
+ {fine.quantity && (
+
+
Quantity
+
+ {fine.quantity}
+
+
+ )}
+
+
+ {fine.remarksbystudents && (
+
+
+ Student Remarks
+
+
+ {fine.remarksbystudents}
+
+
+ )}
+
+ ))}
+ >
+ ) : (
+
+
+
+ No Pending Fines
+
+
+ You don't have any pending fines or miscellaneous charges.
+
+
+ )}
+
+
+
+ {/* Fee Summary Tab */}
+
+
+ {/* Overall Summary Cards */}
+
+ {/* Total Fee Card */}
+
+
+
+
Total Fee
+
+ {formatCurrency(totalFeeAmount)}
+
+
+
+
+
+
+ {/* Total Received Card */}
+
+
+
+
Total Received
+
+ {formatCurrency(totalReceived)}
+
+
+
+
+
+
+ {/* Total Due Card */}
+
+
+
+
Total Due
+
+ {formatCurrency(totalDue)}
+
+
+
+
+
+
+ {/* Advance/Refund Card */}
+ {(advanceAmount > 0 || totalRefund > 0) && (
+
+
+
+
+ {advanceAmount > 0 ? "Advance" : "Refund"}
+
+
+ {formatCurrency(advanceAmount > 0 ? advanceAmount : totalRefund)}
+
+
+
+
+
+ )}
+
+
+ {/* Fee Heads by Semester/Event */}
+ {feeHeads.length > 0 && (
+
+
+ Fee Breakdown by Semester
+
+
+
+ {feeHeads.map((head, idx) => {
+ const academicYear = head.academicyear || "N/A";
+ const semester = head.stynumber ? `Semester ${head.stynumber}` : "N/A";
+ const feeAmount = parseFloat(head.feeamount) || 0;
+ const received = parseFloat(head.receiveamount) || 0;
+ const due = parseFloat(head.dueamount) || 0;
+ const refund = parseFloat(head.refundamount) || 0;
+
+ return (
+
+
+
+
+ {semester} ({academicYear})
+
+
+ Event: {head.eventid}
+
+
+
+ {head.stytypedesc || "Regular"}
+
+
+
+
+
+
Fee Amount
+
+ {formatCurrency(feeAmount)}
+
+
+
+
Received
+
+ {formatCurrency(received)}
+
+
+
+
Due
+
+ {formatCurrency(due)}
+
+
+ {refund > 0 && (
+
+
Refund
+
+ {formatCurrency(refund)}
+
+
+ )}
+
+
+ {/* Waiver, Transfer details if present */}
+ {(parseFloat(head.waiveramount) > 0 ||
+ parseFloat(head.transferinamount) > 0 ||
+ parseFloat(head.transferoutamount) > 0) && (
+
+ {parseFloat(head.waiveramount) > 0 && (
+
+
Waived
+
+ {formatCurrency(head.waiveramount)}
+
+
+ )}
+ {parseFloat(head.transferinamount) > 0 && (
+
+
Transfer In
+
+ {formatCurrency(head.transferinamount)}
+
+
+ )}
+ {parseFloat(head.transferoutamount) > 0 && (
+
+
Transfer Out
+
+ {formatCurrency(head.transferoutamount)}
+
+
+ )}
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/jportal/src/components/Login.jsx b/jportal/src/components/Login.jsx
index 3c39c1d..0e6e6e7 100644
--- a/jportal/src/components/Login.jsx
+++ b/jportal/src/components/Login.jsx
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
-import { LoginError } from "https://cdn.jsdelivr.net/npm/jsjiit@0.0.20/dist/jsjiit.esm.js";
+import { LoginError } from "https://cdn.jsdelivr.net/npm/jsjiit@0.0.22/dist/jsjiit.esm.js";
import PublicHeader from "./PublicHeader";
// Define the form schema