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 ( +
+
+ +

{error}

+
+
+ ); + } + + 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