code base

This commit is contained in:
ANUJ7MADKE
2025-07-13 22:49:55 +05:30
parent d4f21c9a99
commit cd43f0e98e
96 changed files with 17779 additions and 0 deletions

21
frontend/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,21 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

27
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# environment variables
.env

8
frontend/README.md Normal file
View File

@@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

24
frontend/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<style>
html,body,#root{
height:100vh;
width:100vw;
margin:0;
padding:0;
display: flex;
flex-direction: column;
/* overflow: hidden; */
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

7144
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
frontend/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@react-pdf/renderer": "^4.1.6",
"autoprefixer": "^10.4.20",
"axios": "^1.7.5",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.7",
"chartjs-plugin-datalabels": "^2.2.0",
"formik": "^2.4.6",
"framer-motion": "^11.15.0",
"frontend": "file:",
"hamburger-react": "^2.5.1",
"pdfjs-dist": "^4.7.76",
"postcss": "^8.4.40",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
"react-pdf": "^9.1.1",
"react-router-dom": "^6.25.1",
"react-search-box": "^2.3.0",
"react-table": "^7.8.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^3.4.7",
"yup": "^1.4.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^1.3.2",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"vite": "^6.2.1"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

25
frontend/src/App.css Normal file
View File

@@ -0,0 +1,25 @@
#root {
margin: 0 auto;
padding: 0 auto;
text-align: center;
}
*::-webkit-scrollbar {
width: 10px;
}
*::-webkit-scrollbar-track {
background: #f1f1f1;
}
*::-webkit-scrollbar-thumb {
background: #888;
}
*::-webkit-scrollbar-thumb:hover {
background: #555;
}
.topLevelFormContainer::-webkit-scrollbar {
width: 0px;
}

76
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,76 @@
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import React, { Suspense } from "react";
import "./App.css";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
// Lazy loading the pages and components
const Login = React.lazy(() => import("./pages/Login/Login"));
const Root = React.lazy(() => import("./components/DashboardRoot/Root"));
const Dashboard = React.lazy(() => import("./pages/Dashboard/Dashboard"));
const Form = React.lazy(() => import("./pages/ApplicationForm/Form"));
const About = React.lazy(() => import("./pages/About/About"));
const Policy = React.lazy(() => import("./pages/Policy/Policy"));
const Applications = React.lazy(() => import("./pages/Applications/Applications"));
const Report = React.lazy(() => import("./pages/Report/Report"));
const LoginRoot = React.lazy(() => import("./components/LoginRoot/LoginRoot"));
const ContactUs = React.lazy(() => import("./pages/ContactUs/ContactUs"));
const ApplicationView = React.lazy(() => import("./pages/ApplicationView/ApplicationView"));
import userDataLoader from "./services/userDataLoader";
import { upsertApplicationAction } from "./services/upsertApplicationAction";
import { applicationStatusAction } from "./services/applicationStatusAction";
import Loading from "./components/Loading";
// Define the router with lazy-loaded components
const router = createBrowserRouter([
{
path: "/",
element: <LoginRoot />,
children: [
{ index: true, element: <Login /> },
{ path: "about", element: <About /> },
{ path: "policy", element: <Policy /> },
],
},
{
path: "/applicant",
element: <Root />,
id: "Applicant-Root",
loader: userDataLoader,
children: [
{ path: "dashboard", element: <Dashboard /> },
{ path: "dashboard/:status", element: <Applications /> },
{ path: "dashboard/:status/:applicationId", element: <ApplicationView />, action: upsertApplicationAction },
{ path: "form", element: <Form />, action: upsertApplicationAction },
{ path: "contact-us", element: <ContactUs /> },
{ path: "policy", element: <Policy /> },
],
},
{
path: "/validator",
element: <Root />,
id: "Validator-Root",
loader: userDataLoader,
children: [
{ path: "dashboard", element: <Dashboard /> },
{ path: "dashboard/:status", element: <Applications /> },
{ path: "dashboard/:status/:applicationId", element: <ApplicationView />, action: applicationStatusAction },
{ path: "report", element: <Report /> },
{ path: "policy", element: <Policy /> },
],
},
]);
function App() {
return (
<>
<ToastContainer position="top-center" />
<Suspense fallback={<Loading/>}>
<RouterProvider router={router} />
</Suspense>
</>
);
}
export default App;

View File

View File

@@ -0,0 +1,141 @@
const institutes = [
{ label: "K J Somaiya Institute of Dharma Studies", value: "KJSIDS" },
{ label: "S K Somaiya College", value: "SKSC" },
{ label: "K J Somaiya College of Engineering", value: "KJSCE" },
{ label: "Somaiya Institute for Research and Consultancy", value: "SIRC" },
{ label: "K J Somaiya Institute of Management", value: "KJSIM" },
{ label: "Somaiya Sports Academy", value: "SSA" },
{ label: "K J Somaiya College of Education", value: "KJSCEd" },
{ label: "Department of Library and Information Science", value: "DLIS" },
{
label: "Maya Somaiya School of Music and Performing Arts",
value: "MSSMPA",
},
];
const instituteDepartmentMapping = {
KJSIDS: [
{ label: "Academics", value: "Academics" },
{
label: "Bharatiya Sanskriti Peetham",
value: "Bharatiya Sanskriti Peetham",
},
{
label: "Center for Studies in Jainism",
value: "Center for Studies in Jainism",
},
{
label: "Department of Ancient Indian History Culture and Archaeology",
value: "Department of Ancient Indian History Culture and Archaeology",
},
{
label: "Centre For Buddhist Studies",
value: "Centre For Buddhist Studies",
},
],
SKSC: [
{
label: "Information Technology & Computer Science",
value: "Information Technology & Computer Science",
},
{ label: "Mathematics & Statistics", value: "Mathematics & Statistics" },
{ label: "Mass Communication", value: "Mass Communication" },
{ label: "Life Science", value: "Life Science" },
{ label: "Business Studies", value: "Business Studies" },
{ label: "Polymer Science", value: "Polymer Science" },
{
label: "Commerce & Business Studies",
value: "Commerce & Business Studies",
},
{ label: "Accounting & Finance", value: "Accounting & Finance" },
{ label: "Commerce", value: "Commerce" },
{ label: "Economics", value: "Economics" },
{ label: "ENVIRONMENTAL SCIENCES", value: "ENVIRONMENTAL SCIENCES" },
{ label: "Language & Literature", value: "Language & Literature" },
{ label: "Computer Science & IT", value: "Computer Science & IT" },
{ label: "SciSER", value: "SciSER" },
{ label: "STATISTICS", value: "STATISTICS" },
{ label: "International Studies", value: "International Studies" },
{ label: "Banking & Finance", value: "Banking & Finance" },
{ label: "Psychology", value: "Psychology" },
{ label: "Financial Market", value: "Financial Market" },
{ label: "NEUTRACEUTICALS", value: "NEUTRACEUTICALS" },
{ label: "Faculty of Science - SVU", value: "Faculty of Science - SVU" },
],
KJSCE: [
{ label: "Mechanical", value: "Mechanical" },
{ label: "Electronics", value: "Electronics" },
{ label: "CBE", value: "CBE" },
{
label: "Electronics & Telecommunication",
value: "Electronics & Telecommunication",
},
{ label: "Computer", value: "Computer" },
{ label: "Information Technology", value: "Information Technology" },
{ label: "Science & Humanities", value: "Science & Humanities" },
{ label: "Admin", value: "Admin" },
{ label: "Library", value: "Library" },
],
SIRC: [
{
label: "Somaiya Institute for Research & Consultancy",
value: "Somaiya Institute for Research & Consultancy",
},
],
KJSIM: [
{
label: "Marketing and International Business",
value: "Marketing and International Business",
},
{
label:
"General Management (Entrepreneurship, Business Communication, Strategy)",
value:
"General Management (Entrepreneurship, Business Communication, Strategy)",
},
{ label: "IT", value: "IT" },
{
label: "Data Science and Technology",
value: "Data Science and Technology",
},
{
label: "HUMAN RESOURCES MANAGEMENT",
value: "HUMAN RESOURCES MANAGEMENT",
},
{ label: "MBA-Sports Management", value: "MBA-Sports Management" },
{ label: "HCM", value: "HCM" },
{ label: "FINANCE AND LAW", value: "FINANCE AND LAW" },
{ label: "Business Analytics", value: "Business Analytics" },
{
label: "PR, Social Media & Data Mining",
value: "PR, Social Media & Data Mining",
},
{ label: "Economics", value: "Economics" },
{
label: "Operations and Supply Chain Management",
value: "Operations and Supply Chain Management",
},
{ label: "Community Medicine", value: "Community Medicine" },
{ label: "Accreditation", value: "Accreditation" },
{ label: "Accounts & Finance", value: "Accounts & Finance" },
{ label: "GENERAL ADMINISTRATION", value: "GENERAL ADMINISTRATION" },
{ label: "Human Resource", value: "Human Resource" },
],
SSA: [{ label: "Sports", value: "Sports" }],
KJSCEd: [{ label: "Education", value: "Education" }],
DLIS: [
{
label: "Department of Library & Information Science",
value: "Department of Library & Information Science",
},
],
MSSMPA: [
{
label: "Maya Somaiya School of Music & Performing Art",
value: "Maya Somaiya School of Music & Performing Art",
},
],
"": [],
};
export { institutes, instituteDepartmentMapping };

View File

@@ -0,0 +1,156 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import logo from "/images/logo.jpeg";
import { IoNotifications, IoPerson } from "react-icons/io5";
import Hamburger from "hamburger-react";
import { FaSignOutAlt } from "react-icons/fa";
const Navbar = ({ userData, sidebarIsVisible, setSidebarIsVisible }) => {
// Mouse cursor tracking for the pull-down effect
const [showNavbar, setShowNavbar] = useState(false);
const [isSmallScreen, setIsSmallScreen] = useState(false);
const handleLogout = async () => {
let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, {
method: "GET",
credentials: "include",
});
return res;
};
const userDesignation = userData.designation;
const userName = userData.userName;
const [profileData] = useState({
name: userName,
university: "Somaiya Vidyavihar University",
role: userDesignation,
});
const links = [];
const handleResize = () => {
if (window.innerWidth < 768) {
setShowNavbar(true);
setIsSmallScreen(true);
} else {
setShowNavbar(false);
setIsSmallScreen(false);
}
};
useEffect(() => {
// Set initial visibility based on screen width
handleResize();
// Add event listener to handle resize
window.addEventListener("resize", handleResize);
// Cleanup the event listener on component unmount
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const handleMouseMove = (e) => {
if (e.clientY < 60 && !isSmallScreen) {
setShowNavbar(true);
} else {
setShowNavbar(false);
}
};
useEffect(() => {
// Add event listener for mousemove only for large screens
if (!isSmallScreen) {
window.addEventListener("mousemove", handleMouseMove);
}
// Clean up the event listener
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [isSmallScreen]);
return (
<>
<header>
{/* Navbar with the pull-down effect */}
<nav
className={`bg-white shadow-md border-b-4 border-gray-200 w-full px-2 z-50 transition-all duration-300 ease-in-out transform ${
isSmallScreen
? ""
: `fixed top-0 left-0 ${showNavbar ? "translate-y-0" : "-translate-y-full"}`
}`}
>
<div className="w-full flex items-center justify-between px-4 py-3">
<div className="flex items-center justify-between w-full">
{/* Hamburger Menu for Mobile */}
<div className="md:hidden">
<Hamburger
toggled={sidebarIsVisible}
toggle={setSidebarIsVisible}
/>
</div>
{/* Logo for Desktop */}
<Link to="/" className="hidden md:flex items-center">
<img src={logo} alt="Somaiya" className="object-contain w-48" />
</Link>
</div>
<div className="flex items-center space-x-4 text-lg font-medium">
{/* Navbar Links */}
<div className="hidden sm:flex items-center space-x-2">
{links?.map((link, index) => (
<div key={index} className="flex items-center space-x-2">
<Link
to={link.path}
className="text-gray-700 hover:bg-red-700 hover:text-white px-4 py-2 rounded-md transition-all duration-200"
>
{link.label}
</Link>
<span>|</span>
</div>
))}
</div>
{/* Logout Button */}
<div className="flex items-center space-x-2">
<Link
to="/"
onClick={handleLogout}
className="flex items-center text-gray-700 hover:bg-red-700 hover:text-white px-1 sm:px-4 py-2 rounded-md transition-all duration-200"
>
{/* Logout icon */}
<FaSignOutAlt className="w-4 h-4 sm:mr-2" />
{/* Text hidden on small screens */}
<span className="hidden sm:block">Logout</span>
</Link>
<span>|</span>
</div>
{/* User Profile */}
{profileData.name && profileData.role && (
<div className="flex items-center space-x-2 bg-red-100 p-2 rounded-md">
<IoPerson className="text-red-700 text-xl" />
<div className="hidden sm:block">
<div className="text-gray-700 font-semibold">
{profileData.name}
</div>
<div className="text-xs text-gray-500">
{`${userData.department} ${profileData.role} at ${userData.institute}`}
</div>
</div>
</div>
)}
</div>
</div>
</nav>
</header>
</>
);
};
export default Navbar;

View File

@@ -0,0 +1,46 @@
import React, { useEffect, useState } from "react";
import { Outlet, useLoaderData } from "react-router-dom";
import Navbar from "./Navbar";
import Sidebar from "./Sidebar";
const Root = () => {
const { user, role } = useLoaderData()?.data;
const [sidebarIsVisible, setSidebarIsVisible] = useState(true)
const urlPath = window.location.pathname;
const handleResize = () => {
if (window.innerWidth < 768) {
setSidebarIsVisible(false); // Hide sidebar on small screens
} else {
setSidebarIsVisible(true); // Show sidebar on larger screens
}
};
useEffect(() => {
// Set initial visibility based on screen width
setSidebarIsVisible(window.innerWidth >= 768);
// Add event listener to handle resize
window.addEventListener("resize", handleResize);
// Cleanup the event listener on component unmount
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return (
<div className="h-full">
<Navbar userData={user} role={role} setSidebarIsVisible={setSidebarIsVisible} sidebarIsVisible={sidebarIsVisible} />
<div className= "flex h-full bg-gray-100 overflow-auto">
{sidebarIsVisible && !(urlPath.split("/").at(-1).includes("dashboard")) && <Sidebar role={role} />}
<div className="w-full min-h-full h-screen overflow-y-scroll">
<Outlet />
</div>
</div>
</div>
);
};
export default Root;

View File

@@ -0,0 +1,258 @@
import React from "react";
import { FaSignOutAlt } from "react-icons/fa";
import { Link, NavLink } from "react-router-dom";
export const handleLogout = async () => {
let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, {
method: "GET",
credentials: "include",
});
return res;
};
const Sidebar = ({ role }) => (
<div className="w-72 h-screen bg-white p-6 shadow-lg z-10 flex flex-col overflow-y-auto">
<div className="mb-8 text-center border-b-2 border-gray-200 pb-6">
<div className="bg-white shadow-lg rounded-lg px-4 py-4 border border-gray-300">
<h2 className="text-xl font-semibold text-red-700 tracking-tight">
{`${role} Portal`}
</h2>
<p className="text-gray-700 text-sm font-medium py-1">
Travel Policy SVU
</p>
</div>
</div>
<nav className="flex-grow">
<ul className="space-y-4 text-sm">
<li className="border-b border-gray-200 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard`}
end
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 13l4 4L10 13m5-5h6a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6m-4 6l4 4 4-4"
></path>
</svg>{" "}
Dashboard
</NavLink>
</li>
<li className="text-gray-700 border-b border-gray-200 pb-2">
<span className="flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded cursor-pointer">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
></path>
</svg>{" "}
Application Status
</span>
<ul className="pl-4 mt-2 border-l border-gray-200 ml-2">
<li className="border-b border-gray-200 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/pending`}
className={({ isActive }) =>
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 3v18m9-9H3"
></path>
</svg>{" "}
Pending
</NavLink>
</li>
<li className="border-b border-gray-200 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/accepted`}
className={({ isActive }) =>
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
></path>
</svg>{" "}
Accepted
</NavLink>
</li>
<li className="pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/rejected`}
className={({ isActive }) =>
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>{" "}
Rejected
</NavLink>
</li>
</ul>
</li>
{role === "Applicant" ? (
<>
<li className="border-b border-gray-200 pb-2">
<NavLink
to="/applicant/policy"
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
></path>
</svg>{" "}
Policy
</NavLink>
</li>
<li>
<NavLink
to="/applicant/contact-us"
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 10h5l2 6h6l2-6h5"
></path>
</svg>{" "}
Contact Us
</NavLink>
</li>
</>
) : (
//role = "Validator"
<>
<li>
<NavLink
to="/validator/report"
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 10h5l2 6h6l2-6h5"
></path>
</svg>{" "}
Report
</NavLink>
</li>
</>
)}
</ul>
</nav>
{/* Spacer to push logout to the bottom */}
<div className="mt-14">
<Link
to="/"
onClick={handleLogout}
className="flex items-center text-gray-700 hover:bg-red-700 hover:text-white px-4 py-2 rounded-md transition-all duration-200"
>
<FaSignOutAlt className="w-4 h-4 mr-2" />
Logout
</Link>
</div>
</div>
);
export default Sidebar;

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useRouteError } from 'react-router-dom';
// Error component to display error messages
const ErrorComponent = () => {
const error = useRouteError();
// Extracting status and message from the error
const status = error.status || 500;
const message = error.data.message || 'Something went wrong.';
return (
<div style={{ padding: '1em', border: '1px solid red', borderRadius: '5px', backgroundColor: '#fdd', color: '#d00' }}>
<h2>Error {status}</h2>
<p>{message}</p>
</div>
);
};
export default ErrorComponent;

View File

@@ -0,0 +1,13 @@
import React from "react";
import { TbLoader3 } from "react-icons/tb";
function Loading() {
return (
<div className="flex flex-col justify-center items-center h-full animate-pulse">
<TbLoader3 className="animate-spin text-xl size-24 text-red-700" />
<p className="mt-2">Loading...</p>
</div>
);
}
export default Loading;

View File

@@ -0,0 +1,94 @@
/* login styles for the Navbar */
.login-navbar {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #f8f9fa;
padding: 10px 20px;
width: 100vw;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
.login-navbar-brand .login-logo {
height: 45px;
}
@media (max-width: 768px) {
.login-navbar-brand .login-logo {
height: 25px;
}
}
.login-navbar-toggler {
display: none;
cursor: pointer;
}
.login-navbar-toggler-icon {
width: 30px;
height: 30px;
background-color: rgba(0, 0, 0, 0.5);
}
.login-navbar-collapse {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.login-navbar-nav {
list-style: none;
display: flex;
margin: 0;
padding: 0;
justify-content: space-evenly;
align-items: center;
}
.login-nav-item {
margin-left: 20px;
}
.login-nav-link {
text-decoration: none;
color: #000;
font-size: 16px;
}
.login-nav-link.active {
font-weight: bold;
text-decoration: underline;
}
.login-trust-logo {
height: 40px;
}
@media (max-width: 768px) {
.login-navbar-toggler {
display: block;
}
.login-navbar-collapse {
display: none;
}
.login-navbar-collapse.show {
display: flex;
flex-direction: column;
width: 100%;
}
.login-navbar-nav {
flex-direction: column;
width: 100%;
}
.login-nav-item {
margin-left: 0;
margin-bottom: 10px;
}
}

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Outlet,NavLink } from 'react-router-dom';
import './LoginRoot.css';
//Navlinks to be used later & will be parent route for policy,services and login
const LoginRoot = () => {
return (
<>
<header>
<nav className="login-navbar">
<a className="login-navbar-brand" href="#">
<img src="/images/KJSCE-Logo.svg" alt="Somaiya Vidyavihar University" className="login-logo" />
</a>
<div className="login-navbar-toggler">
<span className="login-navbar-toggler-icon"></span>
</div>
<div className="login-navbar-collapse" id="loginNavbarNav">
<ul className="login-navbar-nav">
<li className="login-nav-item">
<NavLink className="login-nav-link" to="policy" end>Policy</NavLink>
</li>
<li className="login-nav-item">
<NavLink className="login-nav-link" to="about" end>About</NavLink>
</li>
<li className="login-nav-item">
<NavLink className="login-nav-link" to="" end>Login</NavLink>
</li>
<li className="login-nav-item">
<a className="login-nav-link" href="#">
<img src="/images/Trust.jpg" alt="Somaiya Trust" className="login-trust-logo" />
</a>
</li>
</ul>
</div>
</nav>
</header>
<Outlet/>
<footer className="flex items-center justify-center h-6 w-full bg-gray-100 text-gray-800 fixed bottom-0 left-0 z-50">
<div className="text-center text-sm">
©2024 KJSCE, All Rights Reserved.
</div>
</footer>
</>
);
}
export default LoginRoot;

View File

@@ -0,0 +1,33 @@
import React from 'react';
const Modal = ({ onClose, children }) => {
return (
<div
className="fixed top-0 inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={onClose}
>
<div
className="bg-white p-6 rounded-lg relative w-11/12 md:w-3/5 lg:w-2/5 max-h-[85%] h-min overflow-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-start p-2">
<button
type='button'
className="absolute top-3 right-3 text-xl font-bold text-gray-700 hover:text-gray-900"
onClick={onClose}
>
X
</button>
</div>
<div className="h-full overflow-y-auto">
{children}
</div>
</div>
</div>
);
};
export default Modal;

View File

@@ -0,0 +1,105 @@
import React from 'react';
function Pagination({ numOfItems, itemsPerPage = 10, currentPage, onPageChange }) {
const totalPages = Math.ceil(numOfItems / itemsPerPage);
const pages = Array.from({ length: totalPages }, (_, index) => index + 1);
const maxPageButtons = 5; // Maximum number of page buttons to display
const handlePrevious = () => {
if (currentPage > 1) onPageChange(currentPage - 1);
};
const handleNext = () => {
if (currentPage < totalPages) onPageChange(currentPage + 1);
};
const getPageButtons = () => {
if (totalPages <= maxPageButtons) {
return pages;
}
const half = Math.floor(maxPageButtons / 2);
let start = Math.max(1, currentPage - half);
let end = Math.min(totalPages, currentPage + half);
if (currentPage <= half) {
end = maxPageButtons;
} else if (currentPage + half >= totalPages) {
start = totalPages - maxPageButtons + 1;
}
return Array.from({ length: end - start + 1 }, (_, index) => start + index);
};
return (
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div className="flex flex-1 justify-between sm:hidden">
<button
type='button'
onClick={handlePrevious}
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
disabled={currentPage === 1}
>
Previous
</button>
<button
type='button'
onClick={handleNext}
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
disabled={currentPage === totalPages}
>
Next
</button>
</div>
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{(currentPage - 1) * itemsPerPage + 1}</span> to <span className="font-medium">{Math.min(currentPage * itemsPerPage, numOfItems)}</span> of <span className="font-medium">{numOfItems}</span> applications
</p>
</div>
<div>
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<button
type='button'
onClick={handlePrevious}
className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
disabled={currentPage === 1}
>
<span className="sr-only">Previous</span>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clipRule="evenodd" />
</svg>
</button>
{getPageButtons().map(page => (
<button
type='button'
key={page}
onClick={() => onPageChange(page)}
aria-current={currentPage === page ? 'page' : undefined}
className={`relative inline-flex items-center px-4 py-2 text-sm font-semibold ${currentPage === page ? 'bg-red-700 text-white' : 'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50'} focus:z-20 focus:outline-offset-0`}
>
{page}
</button>
))}
<button
type='button'
onClick={handleNext}
className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
disabled={currentPage === totalPages}
>
<span className="sr-only">Next</span>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clipRule="evenodd" />
</svg>
</button>
</nav>
</div>
</div>
</div>
);
}
export default Pagination;

View File

@@ -0,0 +1,25 @@
// PdfViewer.js
import React from "react";
import Modal from "./Modal/Modal";
function PdfViewer({ fileUrl, setIsModalOpen }) {
if (!fileUrl) {
return <p>Loading PDF...</p>;
}
return (
<Modal onClose={() => setIsModalOpen(false)}>
<object data={fileUrl} type="application/pdf" width="100%" height="600px">
<p>
PDF preview failed. Please{" "}
<a href={fileUrl} target="_blank" rel="noopener noreferrer">
open the PDF
</a>{" "}
in a new tab.
</p>
</object>
</Modal>
);
}
export default PdfViewer;

0
frontend/src/hooks/.keep Normal file
View File

3
frontend/src/index.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

10
frontend/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,145 @@
import React from "react";
const About = () => {
return (
<div className="bg-white text-gray-800">
{/* Hero Section */}
<div className="relative bg-red-600 text-white h-[60vh] flex items-center justify-center">
<div className="absolute inset-0 bg-red-800 bg-opacity-50"></div>
<div className="relative z-10 text-center px-6">
<h1 className="text-4xl md:text-6xl font-bold mb-4">
Welcome to Our Travel Policy
</h1>
<p className="text-lg md:text-xl">
Structured, efficient, and research-focused travel planning.
</p>
<button type='button' className="mt-6 px-6 py-3 bg-white text-red-600 font-semibold rounded-lg shadow-md hover:bg-red-700 hover:text-white transition duration-300">
Learn More
</button>
</div>
</div>
{/* Mission & Vision */}
<div className="flex flex-col md:flex-row items-center py-12 px-6 md:px-12 gap-12">
<div className="md:w-1/2">
<h2 className="text-3xl font-bold mb-4">Our Approach</h2>
<p className="text-lg leading-relaxed">
Our travel policy for research students and associates ensures a
structured process for approvals and financial support, fostering
efficiency and alignment with academic and research objectives.
</p>
</div>
<div className="md:w-1/2">
<img
src="https://via.placeholder.com/600x400"
alt="Travel Policy"
className="rounded-lg shadow-lg transform transition duration-300 hover:scale-105"
/>
</div>
</div>
{/* Achievements & History */}
<div className="bg-gray-100 py-12 px-6 md:px-12">
<h2 className="text-3xl font-bold text-center mb-8">Policy Highlights</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[
{
title: "Travel Request Form",
description:
"Submit detailed forms including purpose, destination, budget, and supporting documentation.",
icon: "📄",
},
{
title: "Approval Process",
description:
"Supervisor, department head, and Office of Research must approve for major travels.",
icon: "✔️",
},
{
title: "Financial Support",
description:
"Eligibility depends on travel relevance, available funds, and research alignment.",
icon: "💰",
},
{
title: "Documentation",
description:
"Attach conference invitations or research collaboration letters for approval.",
icon: "📜",
},
{
title: "International Travel",
description:
"Managed through the Office of Research for additional oversight and funding.",
icon: "✈️",
},
{
title: "Funding Sources",
description:
"Includes department funds, institutional grants, or scholarships.",
icon: "🏛️",
},
].map((item, index) => (
<div
key={index}
className="flex flex-col items-center p-6 bg-white shadow-md rounded-lg transition transform hover:scale-105"
>
<div className="text-4xl mb-4">{item.icon}</div>
<h3 className="text-xl font-semibold mb-2">{item.title}</h3>
<p className="text-gray-700 text-center">{item.description}</p>
</div>
))}
</div>
</div>
{/* Why Choose Us */}
<div className="py-12 px-6 md:px-12">
<h2 className="text-3xl font-bold text-center mb-8">Why Our Policy Stands Out</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[
{
title: "Efficiency",
description: "Streamlined processes for quicker approvals.",
},
{
title: "Clarity",
description: "Clear guidelines for both students and staff.",
},
{
title: "Supportive",
description:
"Ensures financial and logistical aid for research-related travel.",
},
{
title: "Global Outlook",
description: "Facilitates international collaborations and exchanges.",
},
{
title: "Comprehensive",
description: "Covers all aspects of academic travel management.",
},
{
title: "Transparent",
description: "Approval criteria and funding sources clearly defined.",
},
].map((item, index) => (
<div
key={index}
className="flex flex-col items-start p-6 bg-white shadow-md rounded-lg transition transform hover:scale-105"
>
<h3 className="text-xl font-semibold mb-2">{item.title}</h3>
<p className="text-gray-700">{item.description}</p>
</div>
))}
</div>
</div>
{/* Back to Top */}
<div className="text-center py-6">
</div>
</div>
);
};
export default About;

View File

@@ -0,0 +1,204 @@
import React, { useEffect, useState } from "react";
import { Formik } from "formik";
import Input from "./Input";
import {
useSubmit,
useRouteLoaderData,
useNavigation,
useParams,
} from "react-router-dom";
import { studentFormFeilds, facultyFormFeilds } from "./FormFeilds";
import * as yup from "yup";
function Form({
prefilledData,
applicantDesignation,
resubmission = false,
onValuesChange,
}) {
const { role, user } =
useRouteLoaderData("Applicant-Root")?.data ||
useRouteLoaderData("Validator-Root")?.data;
const applicationId = useParams().applicationId || "";
const submit = useSubmit("upsertApplicationAction");
const navigation = useNavigation();
const isSubmittingNav = navigation.state === "submitting";
let formFeilds = [];
let toBeFormFeilds = [];
let designation;
const applicant = useRouteLoaderData("Applicant-Root");
if (applicantDesignation) {
designation = applicantDesignation;
} else {
designation = applicant?.data?.user?.designation; //Faculty or Student
}
if (designation === "STUDENT") {
toBeFormFeilds = studentFormFeilds;
} else {
toBeFormFeilds = facultyFormFeilds;
}
if (prefilledData) {
formFeilds = toBeFormFeilds?.map((section) => {
return {
...section,
fields: section?.fields?.map((field) => ({
...field,
disabled:
role === "Validator"
? true
: resubmission && field?.name == "expenses"
? false
: true,
})),
};
});
} else {
formFeilds = toBeFormFeilds;
}
const createIntialValuesScheme = (formFields) => {
const schema = {};
formFields?.forEach((section) => {
section?.fields?.forEach((field) => {
if (prefilledData) {
if (field.type === "miniForm") {
schema[field.name] = JSON.parse(prefilledData[field.name]);
} else if (field.type === "checkbox") {
schema[field.name] = JSON.parse(
prefilledData[field.name] || "false"
);
} else if (field.type === "number") {
schema[field.name] = parseInt(prefilledData[field.name]);
} else {
schema[field.name] = prefilledData[field.name];
}
} else if (field.type === "checkbox") {
schema[field.name] = false;
} else if (field.type === "miniForm") {
schema[field.name] = [];
} else {
schema[field.name] = "";
}
});
});
return schema;
};
const intialValuesSchema = createIntialValuesScheme(formFeilds);
const createValidationSchema = (formFields) => {
const schema = {};
formFields?.forEach((section) => {
section.fields?.forEach((field) => {
if (field.validation) {
schema[field.name] = field.validation;
}
});
});
return yup.object().shape(schema);
};
const validationSchema = createValidationSchema(formFeilds);
const handleSubmit = async (values, { setSubmitting }) => {
const formDataToSend = new FormData();
for (const key in values) {
if (key === "expenses") {
// Serialize the expenses array as a JSON string and append
const expenses = JSON.stringify(values[key]);
formDataToSend.append("expenses", expenses);
// Append expenseProof files separately (as file objects)
values[key].forEach((expense, index) => {
if (expense.expenseProof) {
formDataToSend.append(
`expenses[${index}].expenseProof`,
expense.expenseProof
);
}
});
} else {
// For other fields, just append normally
formDataToSend.append(key, values[key]);
}
}
formDataToSend.append("resubmission", resubmission);
formDataToSend.append("applicationId", applicationId);
try {
submit(formDataToSend, {
method: "POST",
encType: "multipart/form-data", // Specify the encoding type
});
} catch (error) {
console.error("Error uploading form:", error.message);
} finally {
setSubmitting(false); // Reset the submitting state after request is done
}
};
return (
<Formik
initialValues={intialValuesSchema}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue, // Use setFieldValue for file handling
isSubmitting,
}) => {
// Notify parent about values change
useEffect(() => {
if (onValuesChange) {
onValuesChange(values);
}
}, [values, onValuesChange]);
return (
<form
onSubmit={handleSubmit}
className="p-2 my-4 overflow-y-auto bg-transparent"
>
<Input
values={values}
errors={errors}
touched={touched}
handleChange={handleChange}
handleBlur={handleBlur}
setFieldValue={setFieldValue} // Pass setFieldValue for file handling
formFeilds={formFeilds}
/>
{(resubmission || !prefilledData) && role != "Validator" && (
<button
type="submit"
disabled={isSubmitting || isSubmittingNav}
className="w-full flex items-center justify-center bg-gradient-to-r from-red-600 to-red-800 hover:from-red-800 hover:to-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transform transition duration-300 ease-in-out disabled:bg-gray-400"
>
{isSubmitting || isSubmittingNav ? "Submitting" : "Submit"}
</button>
)}
</form>
);
}}
</Formik>
);
}
export default Form;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,379 @@
import React, { useEffect, useState } from "react";
import { studentFormFeilds, facultyFormFeilds } from "./FormFeilds";
import { useParams, useRouteLoaderData } from "react-router-dom";
import ExpenseForm from "./components/ExpenseForm";
import ExpenseTable from "./components/ExpenseTable";
import PdfViewer from "../../components/PdfViewer";
import PdfActions from "../ApplicationView/PdfActions";
function Input({
values,
errors,
touched,
handleChange,
handleBlur,
setFieldValue,
formFeilds,
}) {
const applicationId = useParams().applicationId;
const [showMiniFrom, setShowMiniForm] = useState(false);
const [expensesEditValues, setExpensesEditValues] = useState(null);
const [pdfIsVisible, setPdfIsVisible] = useState(false);
const [fileUrl, setFileUrl] = useState(null);
return formFeilds.map((section, sectionIndex) => {
if (
section?.parent?.name &&
!section?.parent?.values?.includes(values[section?.parent?.name])
) {
section.fields.forEach((formFeild) => {
if (typeof values[formFeild?.name] === "boolean") {
values[formFeild.name] = false;
} else {
values[formFeild.name] = "";
}
});
return null;
}
return (
<div
key={sectionIndex}
className="space-y-4 bg-white p-6 rounded-lg shadow-md min-w-fit border-t-4 border-red-700 mb-4"
>
<h3 className="text-xl font-semibold mt-2 mb-4">{section.label}</h3>
<div
className={`${
section.label === "Expense Details"
? "grid grid-cols-1" // Apply single column grid when the label is "Expense Details"
: section.label === "Travel Polciy Report"
? "grid grid-cols-2" // Apply two-column grid when the label is "Travel Polciy Report"
: "grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3" // Apply multi-column grid for other labels
} gap-4`}
>
{section.fields.map((formFeild) => {
if (formFeild?.parent?.name) {
if (values[formFeild?.parent?.name] === false) {
typeof values[formFeild?.name] === "boolean"
? (values[formFeild?.name] = false)
: (values[formFeild?.name] = "");
return null;
} else if (
typeof values[formFeild?.parent?.name] === "string" &&
!formFeild?.parent?.values.includes(
values[formFeild?.parent?.name]
)
) {
typeof values[formFeild?.name] === "boolean"
? (values[formFeild?.name] = false)
: (values[formFeild?.name] = "");
return null;
}
}
switch (formFeild.type) {
case "dropdown":
return (
<div
key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md"
>
<label
htmlFor={formFeild.name}
className="block font-medium"
>
{formFeild.label}
</label>
<select
name={formFeild.name}
id={formFeild.name}
onChange={handleChange}
onBlur={handleBlur}
value={values[formFeild.name] || ""}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out"
disabled={formFeild?.disabled}
>
<option value="" label="Select option" />
{formFeild.options[values[formFeild.depend] || ""].map(
(option) => (
<option
key={option.value}
value={option.value}
label={option.label}
className="text-black"
>
{option.label}
</option>
)
)}
</select>
<p className="text-red-500 text-sm">
{errors[formFeild.name] &&
touched[formFeild.name] &&
errors[formFeild.name]}
</p>
</div>
);
case "checkbox":
return (
<div
key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md"
>
<label
htmlFor={formFeild.name}
className="inline-flex items-center space-x-2"
>
<input
type="checkbox"
name={formFeild.name}
id={formFeild.name}
onChange={handleChange}
onBlur={handleBlur}
checked={values[formFeild.name] || false}
className="h-4 w-4 border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-red-700"
disabled={formFeild?.disabled}
/>
<span className="text-sm">{formFeild.label}</span>
</label>
<p className="text-red-500 text-sm">
{errors[formFeild.name] &&
touched[formFeild.name] &&
errors[formFeild.name]}
</p>
</div>
);
case "textarea":
return (
<div
key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md"
>
<label
htmlFor={formFeild.name}
className="block font-medium"
>
{formFeild.label}
</label>
<textarea
name={formFeild.name}
id={formFeild.name}
onChange={handleChange}
onBlur={handleBlur}
value={values[formFeild.name] || ""}
className="w-full px-3 py-2 border border-gray-300 rounded-md max-h-32 min-h-20 focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out"
disabled={formFeild?.disabled}
/>
<p className="text-red-500 text-sm">
{errors[formFeild.name] &&
touched[formFeild.name] &&
errors[formFeild.name]}
</p>
</div>
);
case "file":
return (
<div
key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md"
>
<label
htmlFor={formFeild.name}
className="block font-medium"
>
{formFeild.label}
</label>
{formFeild?.disabled ? (
values[formFeild.name] === "" ? (
<p className="pt-2">No File Submitted</p>
) : (
<PdfActions
applicationId={applicationId}
fileName={formFeild.name}
/>
)
) : (
<>
<input
type="file"
name={formFeild.name}
id={formFeild.name}
onChange={(e) => {
setFieldValue(formFeild.name, e.target.files[0]);
}}
onBlur={handleBlur}
className="w-full bg-white px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out"
/>
<p className="text-red-500 text-sm">
{errors[formFeild.name] &&
touched[formFeild.name] &&
errors[formFeild.name]}
</p>
</>
)}
</div>
);
case "miniForm":
return (
<div
key={formFeild.name}
className="space-y-4 bg-slate-50 p-6 rounded-md w-full"
>
{pdfIsVisible && (
<PdfViewer
fileUrl={fileUrl}
setIsModalOpen={setPdfIsVisible}
/>
)}
{/* Label and Add Expense Button */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4">
<label
htmlFor={formFeild.name}
className="block text-lg font-medium text-gray-800 mb-3 sm:mb-0 sm:w-1/2"
>
{`${formFeild.label}: ₹${values[formFeild.name]
?.reduce(
(total, rec) =>
total + parseFloat(rec.expenseAmount || 0),
0
)
.toFixed(2)}`}
{/* {values[formFeild.name]
?.reduce(
(total, rec) =>
total + parseFloat(rec.expenseAmount || 0),
0
)
.toFixed(2) > 10000 && (
<p className="text-red-600">Warning: Limit Exceded</p>
)} */}
</label>
{!formFeild?.disabled &&
(values[formFeild.name]?.length < 10 ? (
<div className="flex-shrink-0 mt-4 sm:mt-0 sm:w-auto">
<button
className="bg-red-700 text-white font-semibold py-3 px-8 rounded-lg shadow-md transform transition duration-300 hover:bg-red-800 hover:scale-105 active:scale-95"
type="button"
onClick={() => {setShowMiniForm(true); setExpensesEditValues(null)}}
>
Add Expense
</button>
</div>
) : (
<h3 className="block text-lg font-medium text-gray-800 mb-3 sm:mb-0 sm:w-1/2">
Cannot add more than 10 expenses
</h3>
))}
</div>
{/* Expense Form */}
{showMiniFrom && !formFeild?.disabled && (
<ExpenseForm
onClose={() => setShowMiniForm(false)}
setExpenses={(newExpenses) =>
setFieldValue(formFeild.name, [
...values[formFeild.name],
newExpenses,
])
}
editExpense={(editedExpense) => {
setFieldValue(
formFeild.name,
values[formFeild.name].map((expense) =>
expense === expensesEditValues
? editedExpense
: expense
)
);
}}
expenses={expensesEditValues}
/>
)}
{/* Error Message */}
<p className="text-red-500 text-sm mt-2">
{errors[formFeild.name] &&
touched[formFeild.name] &&
errors[formFeild.name]}
</p>
{/* Display Expense Table */}
{values[formFeild.name]?.length > 0 && (
<div className="mt-6 w-full overflow-x-auto">
<ExpenseTable
expenses={values[formFeild.name]}
setPdfIsVisible={setPdfIsVisible}
setFileUrl={setFileUrl}
deleteExpense={(expense) =>
setFieldValue(
formFeild.name,
values[formFeild.name]?.filter(
(toDel) => toDel !== expense
)
)
}
editStatus={(expense, status) =>{
setFieldValue(
formFeild.name,
values[formFeild.name]?.map((toEdit) =>
toEdit === expense
? { ...toEdit, proofStatus: status }
: toEdit
)
)}
}
editExpense={(expenseValues) => {setShowMiniForm(true); setExpensesEditValues(expenseValues)}}
disabled={formFeild?.disabled}
/>
</div>
)}
</div>
);
default:
return (
<div
key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md"
>
<label
htmlFor={formFeild.name}
className="block font-medium"
>
{formFeild.label}
</label>
<input
type={formFeild.type}
name={formFeild.name}
id={formFeild.name}
onChange={handleChange}
onBlur={handleBlur}
value={values[formFeild.name] || ""}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out"
disabled={formFeild?.disabled}
max={formFeild?.max}
min={formFeild?.min}
/>
<p className="text-red-500 text-sm">
{errors[formFeild.name] &&
touched[formFeild.name] &&
errors[formFeild.name]}
</p>
</div>
);
}
})}
</div>
</div>
);
});
}
export default Input;

View File

@@ -0,0 +1,243 @@
import React, { useEffect, useRef, useState } from "react";
import Modal from "../../../components/Modal/Modal"; // Ensure your Modal is correctly imported
// Validation function (manually written)
const validateForm = (values) => {
const errors = {};
// Validate Expense Category
if (!values.expenseName) {
errors.expenseName = "Expense Category is required";
}
// Validate Expense Name
if (!values.expenseDetails) {
errors.expenseDetails = "Expense Name is required";
}
// Validate Expense Amount
if (!values.expenseAmount) {
errors.expenseAmount = "Expense Amount is required";
} else if (values.expenseAmount <= 0) {
errors.expenseAmount = "Amount must be positive";
} else if (values.expenseAmount < 1) {
errors.expenseAmount = "Amount must be at least 1";
}
// Validate Expense Proof
if (!values.expenseProof) {
errors.expenseProof = "Expense Proof is required";
} else {
// Validate file size and type
if (values.expenseProof.size > 1024 * 1024) {
errors.expenseProof = "File size too large (max 1MB)";
} else if (
!["image/jpeg", "image/png", "application/pdf"].includes(values.expenseProof.type)
) {
errors.expenseProof = "Invalid file type. Only JPEG, PNG, and PDF are allowed.";
}
}
return errors;
};
const ExpenseForm = ({ onClose, setExpenses, editExpense, expenses = null }) => {
const fileInputRef = useRef(null);
const [values, setValues] = useState({
expenseName: "",
expenseDetails: "",
expenseAmount: "",
expenseProof: null,
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({
expenseName: false,
expenseDetails: false,
expenseAmount: false,
expenseProof: false,
});
useEffect(() => {
if (expenses) {
// If the expenses object contains a File, set it to the input
if (expenses.expenseProof && expenses.expenseProof instanceof File) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(expenses.expenseProof); // Add the file from expenses to DataTransfer
fileInputRef.current.files = dataTransfer.files; // Set files to the input
}
setValues(expenses); // Set the rest of the form data
}
}, [expenses]);
// Handle form input changes with error checking on each keystroke
const handleChange = (e) => {
const { name, value } = e.target;
setValues((prevValues) => {
const updatedValues = { ...prevValues, [name]: value };
// Validate the field on every change
const validationErrors = validateForm(updatedValues);
setErrors(validationErrors);
return updatedValues;
});
};
// Handle file input change
const handleFileChange = (e) => {
const file = e.target.files[0];
setValues((prevValues) => {
const updatedValues = { ...prevValues, expenseProof: file };
// Validate the file field on change
const validationErrors = validateForm(updatedValues);
setErrors(validationErrors);
return updatedValues;
});
};
// Handle blur (mark field as touched)
const handleBlur = (e) => {
const { name } = e.target;
setTouched((prevTouched) => ({
...prevTouched,
[name]: true,
}));
};
// Handle form submission
const handleSubmit = () => {
// Validate form before submission
const validationErrors = validateForm(values);
setErrors(validationErrors);
// If no errors, proceed with submission
if (Object.keys(validationErrors).length === 0) {
if (expenses) {
editExpense({
expenseId: expenses.expenseId,
expenseName: values.expenseName,
expenseDetails: values.expenseDetails,
expenseAmount: values.expenseAmount,
expenseProof: values.expenseProof,
})
} else {
setExpenses({
expenseId: crypto.randomUUID(),
expenseName: values.expenseName,
expenseDetails: values.expenseDetails,
expenseAmount: values.expenseAmount,
expenseProof: values.expenseProof,
});
}
onClose(); // Close the modal after submission
}
};
return (
<Modal onClose={onClose}>
<div className="space-y-4">
{/* Expense Category */}
<div className="space-y-1 bg-slate-50 p-3 rounded-md">
<label htmlFor="expenseName" className="block font-medium">
Expense Name
</label>
<select
name="expenseName"
id="expenseName"
value={values.expenseName}
onChange={handleChange}
onBlur={handleBlur}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="">Select Category</option>
<option value="TRAVEL">Travel</option>
<option value="LODGING">Lodging</option>
<option value="BOARDING">Boarding</option>
<option value="LOCAL_CONVEYANCE">Local Conveyance</option>
<option value="TRANSPORTATION">Transportation</option>
<option value="REGISTRATION">Registration</option>
<option value="MISCELLANEOUS">Miscellaneous</option>
</select>
{errors.expenseName && (
<p className="text-red-500 text-sm">{errors.expenseName}</p>
)}
</div>
{/* Expense Name */}
<div className="space-y-1 bg-slate-50 p-3 rounded-md">
<label htmlFor="expenseDetails" className="block font-medium">
Expense Details
</label>
<input
type="text"
name="expenseDetails"
id="expenseDetails"
value={values.expenseDetails}
onChange={handleChange}
onBlur={handleBlur}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
{errors.expenseDetails && (
<p className="text-red-500 text-sm">{errors.expenseDetails}</p>
)}
</div>
{/* Expense Amount */}
<div className="space-y-1 bg-slate-50 p-3 rounded-md">
<label htmlFor="expenseAmount" className="block font-medium">
Expense Amount
</label>
<input
type="number"
name="expenseAmount"
id="expenseAmount"
value={values.expenseAmount}
onChange={handleChange}
onBlur={handleBlur}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
{errors.expenseAmount && (
<p className="text-red-500 text-sm">{errors.expenseAmount}</p>
)}
</div>
{/* Expense Proof (File Upload) */}
<div className="space-y-1 bg-slate-50 p-3 rounded-md">
<label htmlFor="expenseProof" className="block font-medium">
Expense Proof (Upload File)
</label>
<input
type="file"
ref={fileInputRef}
name="expenseProof"
id="expenseProof"
accept="application/pdf"
onChange={handleFileChange}
onBlur={handleBlur}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
{errors.expenseProof && (
<p className="text-red-500 text-sm">{errors.expenseProof}</p>
)}
</div>
{/* Submit Button */}
<div className="flex justify-center mt-4">
<button
type="button"
onClick={handleSubmit} // Call handleSubmit manually
className="bg-blue-600 text-white py-2 px-6 rounded-lg hover:bg-blue-700"
>
{expenses ? "Update" : "Add"} Expense
</button>
</div>
</div>
</Modal>
);
};
export default ExpenseForm;

View File

@@ -0,0 +1,277 @@
import React, { useState } from "react";
import { useTable } from "react-table";
import axios from "axios";
import {
MdDangerous,
MdDeleteOutline,
MdEdit,
MdVerified
} from "react-icons/md";
import PdfActions from "../../ApplicationView/PdfActions";
import { useParams, useRouteLoaderData } from "react-router-dom";
const ExpenseTable = ({
expenses,
setPdfIsVisible,
setFileUrl,
deleteExpense,
editExpense,
editStatus,
disabled,
}) => {
const applicationId = useParams().applicationId;
const applicationStatus = useParams().status;
const { role } =
useRouteLoaderData("Applicant-Root")?.data ||
useRouteLoaderData("Validator-Root")?.data;
// const handleExpenseAction = async (expense, action) => {
// try {
// await axios.put(
// `${import.meta.env.VITE_APP_API_URL}/validator/expenseAction`,
// { expense, action, applicationId },
// { withCredentials: true }
// );
// alert(`Proof ${action}`);
// window.location.reload();
// } catch (error) {
// console.error("Error performing expense action:", error);
// }
// };
const columns = React.useMemo(() => {
// Common columns
const baseColumns = [
{
Header: "Expense Name",
accessor: "expenseName",
},
{
Header: "Expense Details",
accessor: "expenseDetails",
},
{
Header: "Amount",
accessor: "expenseAmount",
Cell: ({ value }) => `${value}`,
},
{
Header: "Expense Proof",
accessor: "expenseProof",
Cell: ({ value, row }) => {
if (applicationId) {
return (
<div className="max-w-72 m-auto">
<PdfActions
fileName={"expenseProof" + row.id}
applicationId={applicationId}
/>
</div>
);
} else if (value && value.name) {
return (
<button
type="button"
onClick={() => {
setPdfIsVisible(true);
setFileUrl(URL.createObjectURL(value));
}}
className="text-blue-600 hover:text-blue-700 focus:outline-none"
>
View Document
</button>
);
}
return "No Document";
},
},
];
// Add the "Delete" column only if 'disabled' is false
if (role === "Applicant" && !disabled) {
baseColumns.push({
Header: "Actions",
accessor: "actions",
Cell: ({ row }) => (
row?.original?.proofStatus != "verified" ?(
<div className="flex justify-center space-x-7">
<div className="text-center">
<button
type="button"
onClick={() => deleteExpense(row.original)}
className="bg-red-600 text-white py-2 px-3 rounded-lg hover:bg-red-700 transition-colors focus:outline-none"
>
<MdDeleteOutline />
</button>
</div>
<div className="text-center">
<button
type="button"
onClick={() => editExpense(row.original)}
className="bg-blue-600 text-white py-2 px-3 rounded-lg hover:bg-blue-700 transition-colors focus:outline-none"
>
<MdEdit />
</button>
</div>
</div>) : <h1 className="text-green-600">Approved</h1>
),
});
}
if (role === "Validator") {
baseColumns.push({
Header: "Approval",
accessor: "approval",
Cell: ({ row }) => {
const isVerified = row?.original?.proofStatus === "verified";
const isRejected = row?.original?.proofStatus === "rejected";
const status = isVerified ? "verified" : isRejected ? "rejected" : "pending";
const [hoverSide, setHoverSide] = useState(null);
return (
<div className="flex flex-col items-center justify-center py-2">
<div className="relative flex items-center w-36 sm:w-48 cursor-pointer my-5 group">
{/* Status indicator text */}
<div className="absolute -top-5 left-0 w-full flex justify-between text-xs font-medium">
<span className={`${status === "verified" ? "text-green-600 font-bold" : "text-gray-500"}`}>Approved</span>
<span className={`${status === "pending" ? "text-gray-600 font-bold" : "text-gray-500"}`}>Pending</span>
<span className={`${status === "rejected" ? "text-red-600 font-bold" : "text-gray-500"}`}>Rejected</span>
</div>
{/* Track background with hover effect */}
<div className={`w-full h-8 sm:h-10 rounded-full bg-gradient-to-r from-green-600 via-gray-300 to-red-600 p-1 group-hover:shadow-md transition-all duration-300 relative overflow-hidden`}>
{/* Hover indicators */}
{hoverSide === 'left' && (
<div className="absolute inset-y-0 left-0 w-1/2 bg-green-300 bg-opacity-30 rounded-l-full pointer-events-none z-0"></div>
)}
{hoverSide === 'right' && (
<div className="absolute inset-y-0 right-0 w-1/2 bg-red-300 bg-opacity-30 rounded-r-full pointer-events-none z-0"></div>
)}
{/* Sliding knob */}
<div
className={`absolute h-6 sm:h-8 w-10 sm:w-12 bg-white rounded-full shadow-lg transition-all duration-300 flex items-center justify-center transform ${
status === "verified" ? "left-1" :
status === "rejected" ? "right-1" :
"left-1/2 -translate-x-1/2"
} group-hover:shadow-xl z-10`}
>
{status === "verified" && <MdVerified className="text-green-600" size={18} />}
{status === "rejected" && <MdDangerous className="text-red-600" size={18} />}
{status === "pending" && <span className="text-sm text-gray-600 font-medium">?</span>}
</div>
</div>
{/* Clickable buttons overlaid on top */}
<button
type="button"
onClick={() => editStatus(row.original, "verified")}
onMouseEnter={() => setHoverSide('left')}
onMouseLeave={() => setHoverSide(null)}
className="absolute left-0 w-1/2 h-full opacity-0 z-20"
aria-label="Approve"
disabled={applicationStatus != "pending"}
/>
<button
type="button"
onClick={() => editStatus(row.original, "rejected")}
onMouseEnter={() => setHoverSide('right')}
onMouseLeave={() => setHoverSide(null)}
className="absolute right-0 w-1/2 h-full opacity-0 z-20"
aria-label="Reject"
disabled={applicationStatus != "pending"}
/>
</div>
{/* Status text */}
<p className={`text-xs ${
status === "verified" ? "text-green-600 font-medium" :
status === "rejected" ? "text-red-600 font-medium" :
"text-gray-500"
}`}>
{status === "verified" ? "Expense Approved" :
status === "rejected" ? "Expense Rejected" :
"Click to change status"}
</p>
</div>
);
},
});
}
return baseColumns;
}, [deleteExpense, setPdfIsVisible, setFileUrl, disabled]);
// Using the useTable hook to create the table instance
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({
columns,
data: expenses || [], // Data passed to the table
});
return (
<div className="w-full mx-auto bg-white p-6 rounded-lg shadow-md hover:shadow-lg">
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Expense Breakdown
</h2>
{/* Wrapping the table inside a scrollable div for responsiveness */}
<div className="overflow-x-auto">
<table
{...getTableProps()}
className="min-w-full bg-white border border-gray-300 table-auto"
>
{/* Table header */}
<thead className="bg-gray-100 sticky top-0 z-10">
{headerGroups.map((headerGroup) => {
const { key, ...restHeaderProps } =
headerGroup.getHeaderGroupProps(); // Destructure `key` here
return (
<tr key={key} {...restHeaderProps}>
{headerGroup.headers.map((column) => {
const { key, ...restColumnProps } = column.getHeaderProps(); // Destructure `key` here
return (
<th
key={column.id} // Explicitly add key for th
{...restColumnProps}
className="px-3 py-2 text-center text-sm font-bold text-gray-600 border-b border-gray-300"
>
{column.render("Header")}
</th>
);
})}
</tr>
);
})}
</thead>
{/* Table body */}
<tbody {...getTableBodyProps()} className="text-sm text-gray-700">
{rows.map((row) => {
prepareRow(row);
const { key, ...restRowProps } = row.getRowProps(); // Destructure `key` here
return (
<tr key={key} {...restRowProps} className="hover:bg-gray-50">
{row.cells.map((cell) => {
const { key, ...restCellProps } = cell.getCellProps(); // Destructure `key` here
return (
<td
key={cell.column.id} // Explicitly add key for td
{...restCellProps}
className="px-3 py-2 border-t border-b border-gray-300"
>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
export default ExpenseTable;

View File

@@ -0,0 +1,127 @@
import React from "react";
import Modal from "../../components/Modal/Modal";
function AcceptChoice({
onClose,
onSubmit,
designation,
applicantDesignation,
}) {
const handleSubmit = (toVC = false) => {
onSubmit(toVC);
onClose();
};
return (
<Modal onClose={onClose}>
<div className="bg-white rounded-lg p-6 shadow-lg mx-auto">
<h2 className="text-2xl font-semibold text-red-700 mb-4">
Confirm Application Approval
</h2>
<p className="text-gray-600 mb-6">
{(() => {
switch (designation) {
case "FACULTY":
return "By approving, you will forward this application to the Head of Department (HOD).";
case "HOD":
return "By approving, you will forward this application to the Head of Institute (HOI).";
case "HOI":
if (applicantDesignation === "STUDENT") {
return "By approving, you will forward this application to Accounts.";
} else {
return "By approving, you can forward this application to either the Vice Chancellor (VC) or Accounts.";
}
case "VC":
return "By approving, you will forward this application to Accounts.";
case "ACCOUNTS":
return "By approving, you confirm that the given expenses will be paid by the institute.";
default:
return "";
}
})()}
</p>
<div className="flex flex-col gap-4 justify-center">
{/* Cancel Button */}
<button
type="button"
onClick={onClose}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 focus:outline-none"
>
Cancel
</button>
{(() => {
switch (designation) {
case "FACULTY":
return (
<button
onClick={handleSubmit}
type="button"
className="px-4 py-2 bg-green-700 text-white rounded-lg hover:bg-green-800 focus:outline-none"
>
Approve
</button>
);
case "HOD":
return (
<button
onClick={handleSubmit}
type="button"
className="px-4 py-2 bg-green-700 text-white rounded-lg hover:bg-green-800 focus:outline-none"
>
Approve
</button>
);
case "HOI":
return (
<div className="flex flex-col gap-2">
{applicantDesignation !== "STUDENT" && (
<button
onClick={() => handleSubmit(true)}
type="button"
className="px-4 py-2 bg-blue-700 text-white rounded-lg hover:bg-blue-800 focus:outline-none"
>
Forward to VC
</button>
)}
<button
onClick={() => handleSubmit(false)}
type="button"
className="px-4 py-2 bg-green-700 text-white rounded-lg hover:bg-green-800 focus:outline-none"
>
Forward to Accounts
</button>
</div>
);
case "VC":
return (
<button
onClick={handleSubmit}
type="button"
className="px-4 py-2 bg-green-700 text-white rounded-lg hover:bg-green-800 focus:outline-none"
>
Forward to Accounts
</button>
);
case "ACCOUNTS":
return (
<button
onClick={handleSubmit}
type="button"
className="px-4 py-2 bg-green-700 text-white rounded-lg hover:bg-green-700 focus:outline-none"
>
Approve
</button>
);
default:
return null;
}
})()}
</div>
</div>
</Modal>
);
}
export default AcceptChoice;

View File

@@ -0,0 +1,246 @@
import React, { useEffect, useState } from "react";
import {
useNavigate,
useParams,
useRouteLoaderData,
useSubmit,
} from "react-router-dom";
import ValidationStatus from "./ValidationStatus";
import Form from "../ApplicationForm/Form";
import RejectionFeedback from "./RejectionFeedback";
import { TbLoader3 } from "react-icons/tb";
import AcceptChoice from "./AcceptChoice";
import { FaCopy } from "react-icons/fa";
function ApplicationView() {
const { role, user } =
useRouteLoaderData("Applicant-Root")?.data ||
useRouteLoaderData("Validator-Root")?.data;
const submit = useSubmit();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [applicationDisplay, setApplicationDisplay] = useState(null);
const [rejectionFeedbackPopUp, setRejectionFeedbackPopUp] = useState(false);
const [acceptChoicePopUp, setAcceptChoicePopUp] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
const [formValues, setFormValues] = useState({});
const applicationId = useParams().applicationId;
const statusParam = useParams().status;
const getFullApplication = async (applicationId) => {
try {
setLoading(true);
const response = await fetch(
`${
import.meta.env.VITE_APP_API_URL
}/general/getApplicationData/${applicationId}`,
{
method: "GET",
credentials: "include",
}
);
if (!response.ok) {
throw new Error(
`Failed to fetch application data: ${response.status} ${response.statusText}`
);
}
const fullApplication = await response.json();
setApplicationDisplay(fullApplication?.data);
} catch (error) {
console.error("Error fetching application data:", error);
} finally {
setLoading(false);
}
};
const handleSubmit = (
applicationId,
action,
rejectionFeedback = "",
toVC = false,
resubmission = false
) => {
try {
setLoading(true);
const formData = new FormData();
formData.append("applicationId", applicationId);
formData.append("action", action);
formData.append("rejectionFeedback", rejectionFeedback);
formData.append("toVC", toVC);
formData.append("resubmission", resubmission);
if (formValues && formValues?.expenses) {
formData.append("expenses", JSON.stringify(formValues.expenses));
}
submit(formData, {
method: "PUT",
encType: "multipart/form-data", // Specify the encoding type
});
} catch (error) {
console.error("Error during submit:", error);
} finally {
setLoading(false);
}
};
// Navigation for status change
let currentStatus = applicationDisplay?.currentStatus?.toLowerCase();
useEffect(() => {
getFullApplication(applicationId);
}, [applicationId]);
useEffect(() => {
if (
(statusParam !== currentStatus && currentStatus) ||
(applicationId !== applicationDisplay?.applicationId &&
applicationDisplay?.applicationId)
) {
const location = window.location.pathname;
const newPath = location.split("/").slice(0, -2).join("/");
navigate(
`${newPath}/${currentStatus}/${applicationDisplay?.applicationId}`
);
}
}, [statusParam, currentStatus, applicationDisplay]);
if (loading) {
return (
<div className="flex flex-col justify-center items-center h-full animate-pulse pb-[10%]">
<TbLoader3 className="animate-spin text-xl size-24 text-red-700" />
<p className="mt-2">Loading...</p>
</div>
);
}
let title = applicationDisplay?.formData?.eventName;
if (!applicationDisplay) return null;
const copyToClipboard = () => {
navigator.clipboard.writeText(applicationId)
.then(() => {
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
})
.catch(err => {
console.error("Failed to copy application ID: ", err);
alert("Failed to copy application ID. Please try again.");
});
};
// Check if this is a Travel Intimation Form
const isTravelIntimationForm = applicationDisplay?.formData?.formName === "Travel Intimation Form";
return (
<div className="min-w-min bg-white shadow rounded-lg p-2 sm:p-4 md:p-6 m-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start mb-6 gap-3">
<div>
<h1 className="text-3xl font-extrabold text-gray-800">{title}</h1>
</div>
{isTravelIntimationForm && (
<button
onClick={copyToClipboard}
className={`flex items-center ${copySuccess ? 'bg-green-600' : 'bg-red-700 hover:bg-red-800'} text-white font-medium py-2 px-4 rounded-lg shadow-md transition duration-300 transform ${copySuccess ? '' : 'hover:scale-110'} self-start sm:self-auto`}
title="Copy Application ID"
>
{copySuccess ? (
<>
<span className="mr-2"></span>
<span className="hidden sm:inline">Copied!</span>
</>
) : (
<>
<FaCopy className="mr-2" />
<span className="hidden sm:inline">Copy ID</span>
</>
)}
</button>
)}
</div>
<ValidationStatus
validations={{
facultyValidation: applicationDisplay?.facultyValidation,
hodValidation: applicationDisplay?.hodValidation,
hoiValidation: applicationDisplay?.hoiValidation,
vcValidation: applicationDisplay?.vcValidation,
accountsValidation: applicationDisplay?.accountsValidation,
}}
rejectionFeedback={applicationDisplay?.rejectionFeedback}
/>
<Form
prefilledData={applicationDisplay?.formData}
applicantDesignation={applicationDisplay?.applicant?.designation}
resubmission={applicationDisplay?.resubmission || false}
onValuesChange={(values) => setFormValues(values)}
/>
{rejectionFeedbackPopUp && (
<RejectionFeedback
onClose={() => setRejectionFeedbackPopUp(false)}
onSubmit={(rejectionFeedback, resubmission) =>
handleSubmit(
applicationDisplay?.applicationId,
"rejected",
rejectionFeedback,
false,
resubmission
)
}
/>
)}
{acceptChoicePopUp && (
<AcceptChoice
onClose={() => setAcceptChoicePopUp(false)}
onSubmit={(toVC) =>
handleSubmit(applicationDisplay?.applicationId, "accepted", "", toVC, false)
}
designation={user.designation}
applicantDesignation={applicationDisplay?.applicant?.designation}
/>
)}
<div className="flex justify-between items-center my-4 gap-2 mx-2">
{role === "Validator" && currentStatus === "pending" && (
<div className="flex space-x-2">
<button
type="button"
onClick={() => setAcceptChoicePopUp(true)}
className="bg-green-700 text-white font-semibold text-sm sm:text-sm md:text-lg px-4 py-2 rounded-md hover:bg-green-800 focus:outline-double transition duration-200 hover:scale-110 hover:animate-spin"
>
Accept
</button>
<button
type="button"
onClick={() => setRejectionFeedbackPopUp(true)}
className="bg-red-700 text-white font-semibold text-sm sm:text-sm md:text-lg px-4 py-2 rounded-md hover:bg-red-800 focus:outline-double transition duration-200 hover:scale-110 hover:animate-spin"
>
Reject
</button>
</div>
)}
<button
type="button"
onClick={() => {
const location = window.location.pathname;
const newPath = location.split("/").slice(0, -1).join("/");
navigate(newPath);
}}
className="bg-blue-700 text-white font-semibold text-sm sm:text-sm md:text-lg px-4 py-2 rounded-md hover:bg-blue-800 focus:outline-double transition duration-200 hover:scale-110"
>
Close
</button>
</div>
</div>
);
}
export default ApplicationView;

View File

@@ -0,0 +1,81 @@
// PdfActions.js
import React, { useState } from "react";
import axios from "axios";
import Modal from "../../components/Modal/Modal.jsx";
import PdfViewer from "../../components/PdfViewer.jsx";
import { FaFileDownload } from "react-icons/fa";
function PdfActions({ fileName, applicationId }) {
const [fileUrl, setFileUrl] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const fetchFileBlob = async () => {
try {
const response = await axios.get(
`${
import.meta.env.VITE_APP_API_URL
}/general/getFile/${applicationId}/${fileName}`,
{
responseType: "blob",
withCredentials: true,
}
);
if (response.data.type !== "application/pdf") {
throw new Error("Invalid file format received.");
}
const blob = new Blob([response.data], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
setFileUrl(url);
return () => URL.revokeObjectURL(url); // Clean up URL when component unmounts
} catch (error) {
console.error("Error fetching PDF:", error);
}
};
const handleView = async () => {
if (!fileUrl) await fetchFileBlob(); // Only fetch if fileUrl is not set
setIsModalOpen(true);
};
const handleDownload = async () => {
if (!fileUrl) await fetchFileBlob(); // Only fetch if fileUrl is not set
const link = document.createElement("a");
link.href = fileUrl;
link.setAttribute("download", `${fileName}.pdf`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="flex flex-wrap gap-3 justify-center lg:justify-between">
<button
type="button"
className="hidden bg-gray-500 hover:bg-gray-700 text-white text-sm font-bold py-2 px-3 rounded transition-all duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-gray-300 sm:block md:block"
onClick={handleView}
>
View PDF
</button>
<button
type="button"
className="bg-gray-500 hover:bg-gray-700 text-white text-sm font-bold py-2 px-3 rounded transition-all duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-gray-300 flex gap-2 items-center"
onClick={handleDownload}
>
<FaFileDownload className="block sm:hidden md:hidden"/>
<span className="hidden sm:block md:block">Download PDF</span>
</button>
{/* Modal to view PDF */}
{isModalOpen && fileUrl && (
<PdfViewer fileUrl={fileUrl} setIsModalOpen={setIsModalOpen} />
)}
</div>
);
}
export default PdfActions;

View File

@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import Modal from '../../components/Modal/Modal';
import { MdOutlineSettingsInputHdmi } from 'react-icons/md';
function RejectionFeedback({ onClose, onSubmit }) {
const [reason, setReason] = useState('');
const handleChange = (e) => {
setReason(e.target.value);
};
const handleSubmit = (e, resubmission = false) => {
e.preventDefault();
onSubmit(reason, resubmission);
onClose();
};
return (
<Modal onClose={onClose}>
<div className="bg-white rounded-lg p-1 shadow-lg">
<h2 className="text-2xl font-semibold text-red-700 mb-4">Confirm Application Rejection</h2>
<p className="text-gray-600 mb-6">
Please provide a reason for rejecting the application.<br/> This will help the applicant to improve future applications.
</p>
<form className="space-y-4">
<textarea
className="w-full p-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-700"
placeholder="Enter the reason for rejection"
rows="4"
value={reason}
onChange={handleChange}
required
/>
<div className="flex justify-end space-x-4">
<button
onClick={(e) => handleSubmit(e, false)}
type="button"
className="px-4 py-2 bg-red-700 text-white rounded-lg hover:bg-red-800 focus:outline-none"
>
Reject
</button>
<button
onClick={(e) => handleSubmit(e, true)}
type="button"
className="px-4 py-2 bg-green-700 text-white rounded-lg hover:bg-green-800 focus:outline-none"
>
Allow Resubmission
</button>
</div>
</form>
</div>
</Modal>
);
}
export default RejectionFeedback;

View File

@@ -0,0 +1,60 @@
import React from "react";
import { MdWarning } from "react-icons/md";
function ValidationStatus({ validations, rejectionFeedback }) {
const roles = [
{ name: "FACULTY", status: validations.facultyValidation },
{ name: "HOD", status: validations.hodValidation },
{ name: "HOI", status: validations.hoiValidation },
{ name: "VC", status: validations.vcValidation },
{ name: "ACCOUNTS", status: validations.accountsValidation },
];
const getStatusColor = (status) => {
switch (status) {
case "ACCEPTED":
return "bg-green-300";
case "REJECTED":
return "bg-red-300";
default:
return "bg-yellow-300";
}
};
return (
<div className="m-3">
<div className="flex flex-row justify-evenly">
{roles
.filter((role) => role.status !== null) // Exclude roles with null status
.map((role, index) => (
<div
key={index}
className="flex flex-col gap-1 justify-center items-center"
>
<div
className={`rounded-full w-10 h-10 ${getStatusColor(
role.status
)}`}
></div>
<p>{role.name}</p>
</div>
))}
</div>
{rejectionFeedback && (
<div
className="mt-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 rounded-lg shadow-md w-fit min-w-[30%]"
>
<div className="flex justify-start items-center gap-2">
<MdWarning className="w-6 h-6 text-red-500" />
<p className="font-semibold">Rejection Reason:</p>
</div>
<p>{rejectionFeedback}</p>
</div>
)}
</div>
);
}
export default ValidationStatus;

View File

@@ -0,0 +1,112 @@
import React, { useState, useEffect, useCallback } from "react";
import { useParams, useRouteLoaderData } from "react-router-dom";
import ApplicationTable from "../Applications/components/ApplicationTable";
import Pagination from "../../components/Pagination";
import axios from "axios";
import ApplicationView from "../ApplicationView/ApplicationView";
import ApplicationsStatusDescription from "./components/ApplicationsStatusDescription";
import Search from "./components/Search";
import Modal from "../../components/Modal/Modal";
import Root from "../../components/DashboardRoot/Root";
import { TbLoader3 } from "react-icons/tb";
const Applications = () => {
const { role } =
useRouteLoaderData("Applicant-Root")?.data ||
useRouteLoaderData("Validator-Root")?.data;
const [numOfApplications, setNumOfApplications] = useState(0);
const [applications, setApplications] = useState([]);
const [applicantName, setApplicantName] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [applicationDisplay, setApplicationDisplay] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 4;
const { status } = useParams();
// Reset currentPage when status changes
useEffect(() => {
setCurrentPage(1);
}, [status]);
// Fetch applications based on status, pagination, and search criteria
useEffect(() => {
const fetchApplications = async () => {
setLoading(true);
try {
const skip = (currentPage - 1) * itemsPerPage;
const res = await axios.get(
`${
import.meta.env.VITE_APP_API_URL
}/general/getApplications/${status}?take=${itemsPerPage}&skip=${skip}${
applicantName
? `&sortBy=applicantName&sortValue=${applicantName}`
: ""
}`,
{ withCredentials: true }
);
setNumOfApplications(res.data.totalApplications);
setApplications(res.data.applications);
} catch (err) {
console.error(err);
setError(err.message);
} finally {
setLoading(false);
}
};
fetchApplications();
}, [status, currentPage, applicantName]);
const handlePageChange = (page) => {
setCurrentPage(page);
};
const handleSelect = useCallback((selection) => {
setApplicantName(selection); // Update search criteria only when selection is finalized
}, []);
const renderTable = () =>
applications.length > 0 ? (
<ApplicationTable
title={`${
status.charAt(0).toUpperCase() + status.slice(1).toLowerCase()
} Applications`}
applications={applications}
/>
) : (
<p className="text-gray-600">
No {status.toLowerCase()} applications found.
</p>
);
if (loading) {
return (
<div className="flex flex-col justify-center items-center h-full animate-pulse pb-[10%]">
<TbLoader3 className="animate-spin text-xl size-24 text-red-700" />
<p className="mt-2">Loading...</p>
</div>
);
}
if (error) return <div>Error: {error}</div>;
return (
<main className="flex flex-col p-6">
<div className="min-w-min bg-white shadow rounded-lg p-6 mb-20">
<ApplicationsStatusDescription />
{role === "Validator" && (
<Search value={applicantName} setValue={handleSelect} />
)}
{renderTable()}
<Pagination
numOfItems={numOfApplications}
itemsPerPage={itemsPerPage}
currentPage={currentPage}
onPageChange={handlePageChange}
/>
</div>
</main>
);
};
export default Applications;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
const ApplicationTable = ({ title, applications,}) => {
const navigate = useNavigate();
return (
<div className="mb-6">
{/* <h2 className="text-xl font-bold mb-2">{title}</h2> */}
<table className="w-full text-left border-collapse">
<thead>
<tr>
<th className="border-b p-4 text-gray-700">Topic</th>
<th className="border-b p-4 text-gray-700">Name</th>
<th className="border-b p-4 text-gray-700">Submitted</th>
<th className="border-b p-4 text-gray-700">Branch</th>
<th className="border-b p-4 text-gray-700">Status</th>
</tr>
</thead>
<tbody>
{applications.map((app, index) => (
<tr
key={index}
// onClick={() => onRowClick({ ...app, currentStatus: title.split(" ")[0] })}
onClick={() => {
const location = window.location.pathname;
const newPath = location.split('/').slice(0, -1).join('/');
navigate(`${newPath}/${title.split(" ")[0]?.toLowerCase()}/${app.applicationId}`);
}}
className="odd:bg-gray-50 even:bg-white hover:bg-gray-200 cursor-pointer"
style={{ height: '50px' }}
>
<td className="p-4">{app.formData.eventName}</td>
<td className="p-4">{app.applicantName}</td>
<td className="p-4">{formatDateToDDMMYYYY(app.createdAt)}</td>
<td className="p-4">{app.formData.applicantDepartment}</td>
<td className="p-4 text-green-500">{title.split(" ")[0]}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default ApplicationTable;
function formatDateToDDMMYYYY(dateString) {
// Convert the ISO string to a Date object
const date = new Date(dateString);
// Extract the day, month, and year
const day = String(date.getDate()).padStart(2, '0'); // Ensures two-digit format
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-based, so add 1
const year = date.getFullYear();
// Format the date as dd/mm/yyyy
return `${day}/${month}/${year}`;
}

View File

@@ -0,0 +1,51 @@
import React from "react";
import { useNavigate, useParams, useRouteLoaderData } from "react-router-dom";
function ApplicationsStatusDescription() {
const { role } =
useRouteLoaderData("Applicant-Root")?.data ||
useRouteLoaderData("Validator-Root")?.data;
const navigate = useNavigate();
const { status } = useParams();
return (
<div className="bg-slate-50 shadow-md rounded-lg p-6 mb-8 border border-slate-400">
<div className="flex justify-between items-center mb-6 gap-5">
<h1 className="text-3xl font-semibold text-gray-800">
{`${status.toUpperCase()} APPLICATIONS`}
</h1>
{role === "Applicant" && (
<button
type='button'
onClick={() => navigate("../form")}
className="flex items-center bg-gradient-to-r from-red-600 to-red-800 hover:from-red-800 hover:to-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transform transition duration-300 ease-in-out hover:scale-105"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 inline-block mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
<span className="text-sm">Create New Application</span>
</button>
)}
</div>
<p className="text-gray-600 text-lg leading-relaxed sm:block hidden">
Easily track the details and statuses of all your submitted applications
in one place.
<br />
Stay updated and manage your applications with ease.
</p>
</div>
);
}
export default ApplicationsStatusDescription;

View File

@@ -0,0 +1,59 @@
import React, { useEffect, useState } from "react";
import ReactSearchBox from "react-search-box";
import axios from "axios";
let applicantNamesCache = null;
const Search = ({ value, setValue }) => {
const [applicantNames, setApplicantNames] = useState([
{ key: "", value: "" },
]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function getApplicants() {
setIsLoading(true);
if (!applicantNamesCache) {
try {
const res = await axios.get(
`${import.meta.env.VITE_APP_API_URL}/validator/getApplicantNames`,
{ withCredentials: true }
);
if (res.status === 200) {
applicantNamesCache = res.data;
}
} catch (error) {
console.error("Error fetching applicant names:", error);
}
}
setApplicantNames(applicantNamesCache);
setIsLoading(false);
}
getApplicants();
}, []);
if (isLoading) return <p>Loading search suggestions...</p>;
return (
<div className="mb-7 p-2 rounded bg-gray-200">
<div className="flex flex-row items-start justify-start">
<div className="w-[90%]">
<ReactSearchBox
placeholder={`Applicant Name`}
data={applicantNames}
onSelect={(record) => setValue(record.item.value)}
clearOnSelect={false}
inputFontSize="large"
fuseConfigs={{ ignoreLocation: true, threshold: 0.3 }}
/>
</div>
<button type='button' className="bg-red-700 hover:bg-red-800 text-white font-bold py-2 px-4 rounded ml-3" onClick={()=> setValue("")}>
Clear
</button>
</div>
{value !== "" && <p className="text-gray-600 mt-3 ml-2 text-left">{`Showing Results for ${value}`}</p>}
</div>
);
};
export default Search;

View File

@@ -0,0 +1,124 @@
import React, { useState } from "react";
function ContactUs() {
const [formSubmitted, setFormSubmitted] = useState(false);
const [formError, setFormError] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
// Simulating form submission
setTimeout(() => {
setFormSubmitted(true);
}, 1000);
};
return (
<div className="h-full bg-white text-gray-900">
{/* Hero Section */}
<section className="relative bg-red-600 text-white h-[60vh] flex items-center justify-center">
<div className="absolute inset-0 bg-red-700 opacity-60"></div>
<div className="relative z-10 text-center text-white pt-24">
<h1 className="text-3xl sm:text-4xl font-bold">Get in Touch with Us</h1>
<p className="mt-4 text-lg sm:text-xl">Were eager to hear from prospective students, parents, and alumni.</p>
<a href="#contact-form" className="mt-6 inline-block py-2 px-6 bg-blue-700 text-white rounded-full hover:bg-white hover:text-blue-700 transition-all">Contact Us</a>
</div>
</section>
{/* Contact Form */}
<section id="contact-form" className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100">
<div className="max-w-lg mx-auto">
<h2 className="text-2xl font-semibold text-center mb-6">Contact Form</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label>
<input
id="name"
type="text"
required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
<input
id="email"
type="email"
required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700"
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</label>
<input
id="subject"
type="text"
required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</label>
<textarea
id="message"
rows="4"
required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700"
/>
</div>
<button
type="submit"
className="w-full py-2 px-4 bg-red-700 text-white rounded-md hover:bg-red-800 transition-all"
>
Submit
</button>
</form>
{formSubmitted && (
<p className="mt-4 text-center text-green-500">Thank you for reaching out! We'll get back to you soon.</p>
)}
{formError && (
<p className="mt-4 text-center text-red-500">There was an error submitting your form. Please try again.</p>
)}
</div>
</section>
{/* Location Map */}
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-screen-xl mx-auto">
<h2 className="text-2xl font-semibold text-center mb-6">Our Location</h2>
<div className="relative">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3770.792765995658!2d72.89735127502728!3d19.072846982131118!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3be7c627a20bcaa9%3A0xb2fd3bcfeac0052a!2sK.%20J.%20Somaiya%20College%20of%20Engineering!5e0!3m2!1sen!2sin!4v1729907307577!5m2!1sen!2sin"
width="100%" height="300" loading="lazy" title="KJSCE Location" style={{ border: "0", borderRadius: "10px" }}
></iframe>
</div>
</div>
</section>
{/* Contact Details */}
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100">
<div className="max-w-screen-xl mx-auto flex flex-col sm:flex-row justify-between items-center">
<div className="flex flex-col items-center sm:items-start mb-6 sm:mb-0">
<h3 className="text-xl font-semibold text-gray-800">Contact Details</h3>
<p className="mt-2 text-gray-600">
<span className="font-medium">Address:</span> K. J. Somaiya College of Engineering, Vidya Nagar, Mumbai, India
</p>
<p className="mt-2 text-gray-600">
<span className="font-medium">Phone:</span> (022) 6728 8000
</p>
<p className="mt-2 text-gray-600">
<span className="font-medium">Email:</span> info@somaiya.edu
</p>
</div>
<div className="flex space-x-6 justify-center sm:justify-start">
<a href="https://facebook.com" className="hover:text-red-700 transform transition-transform duration-300 scale-110">Facebook</a>
<a href="https://twitter.com" className="hover:text-red-700 transform transition-transform duration-300 scale-110">Twitter</a>
<a href="https://linkedin.com" className="hover:text-red-700 transform transition-transform duration-300 scale-110">LinkedIn</a>
<a href="https://instagram.com" className="hover:text-red-700 transform transition-transform duration-300 scale-110">Instagram</a>
</div>
</div>
</section>
</div>
);
}
export default ContactUs;

View File

@@ -0,0 +1,167 @@
import React from "react";
import { useNavigate, useRouteLoaderData } from "react-router-dom";
function Dashboard() {
const { role, user } =
useRouteLoaderData("Applicant-Root")?.data ||
useRouteLoaderData("Validator-Root")?.data;
const navigate = useNavigate();
const { userName, designation, department, institute } = user;
// Personalized greeting message (updated for professionalism and animation)
const greetingLine1 = `Hello, ${userName}!`;
const greetingLine2 = `${designation} in ${department} Department, ${institute}`;
return (
<div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" >
{/* Hero Section */}
<section
className="relative w-full h-screen flex items-center justify-center text-white overflow-hidden bg-cover bg-center snap-start"
style={{
backgroundImage: `url('https://source.unsplash.com/1600x900/?technology,research')`,
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-red-700 via-red-600 to-red-800 opacity-80"></div>
<div className="w-full text-center px-4 sm:px-6 md:px-8 relative z-10 animate-fade-in">
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold mb-4 tracking-wide drop-shadow-lg animate-slide-in-down break-words text-center">
{greetingLine1}
</h1>
<p className="text-lg sm:text-xl md:text-2xl font-medium text-gray-200 drop-shadow-md mb-6 animate-slide-in-left">
{greetingLine2}
</p>
<p className="text-base sm:text-lg md:text-xl text-gray-300 drop-shadow-md mb-8 animate-slide-in-right">
{role === "Applicant"
? "Submit and track your funding applications with ease."
: "Review, validate, and manage applications efficiently."}
</p>
{role === "Applicant" && (
<button
onClick={() => navigate("../form")}
className="px-3 py-3 sm:px-5 sm:py-2 md:px-6 md:py-3 bg-white text-red-700 font-bold text-lg sm:text-base md:text-lg rounded-lg shadow-xl hover:bg-red-100 hover:scale-110 transition-all transform"
>
Start a New Application
</button>
)}
{role === "Validator" && (
<button
onClick={() => navigate("../dashboard/pending")}
className="px-3 py-3 sm:px-5 sm:py-2 md:px-6 md:py-3 bg-white text-red-700 font-bold text-lg sm:text-base md:text-lg rounded-lg shadow-xl hover:bg-red-100 hover:scale-110 transition-all transform"
>
Check For New Applications
</button>
)}
</div>
<div className="absolute bottom-6 sm:bottom-10 left-1/2 transform -translate-x-1/2">
<a
href="#features"
className="text-white text-base sm:text-lg lg:text-xl transition-transform transform hover:scale-110"
>
&#8595; Scroll Down
</a>
</div>
</section>
{/* Features Section */}
<section
id="features"
className="py-12 sm:py-16 px-6 sm:px-8 md:px-12 lg:px-16 bg-gradient-to-b from-white via-gray-100 to-red-50 min-h-screen snap-start"
>
<h2 className="text-3xl sm:text-4xl md:text-5xl font-semibold text-center text-red-700 mb-10 sm:mb-12">
Our Key Features
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-12">
{/* Feature 1 */}
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105">
<div className="text-5xl mb-4 text-red-700">🔍</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center">
View Your Applications
</h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
{role === "Applicant"
? "Manage and track the status of your funding applications. Review feedback and make necessary updates."
: "Review and validate applications submitted by applicants. Approve or reject based on eligibility and guidelines."}
</p>
<button
onClick={() =>
navigate("../dashboard/pending")
}
className="px-6 py-3 sm:px-5 sm:py-2 md:px-6 md:py-3 bg-red-700 text-white font-semibold text-lg sm:text-base md:text-lg rounded-lg hover:bg-red-600 transition-all transform hover:scale-110 shadow-md"
>
{role === "Applicant" ? "View Status" : "Review Application"}
</button>
</div>
{/* Feature 2 */}
{role === "Validator" && (
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105">
<div className="text-5xl mb-4 text-red-700">📊</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center">
View Insights
</h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
Analyze and gain insights about funding and related data.
</p>
<button
onClick={() => navigate("../report")}
className="px-6 py-3 sm:px-5 sm:py-2 md:px-6 md:py-3 bg-red-700 text-white font-semibold text-lg sm:text-base md:text-lg rounded-lg hover:bg-red-600 transition-all transform hover:scale-110 shadow-md"
>
View Insights
</button>
</div>
)}
{/* Feature 3 */}
{role === "Applicant" && (
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105">
<div className="text-5xl mb-4 text-red-700">📝</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center">
Create New Application
</h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
Start your application process to apply for funding and
financial assistance.
</p>
<button
onClick={() => navigate("../form")}
className="px-6 py-3 sm:px-5 sm:py-2 md:px-6 md:py-3 bg-red-700 text-white font-semibold text-lg sm:text-base md:text-lg rounded-lg hover:bg-red-600 transition-all transform hover:scale-110 shadow-md"
>
Start Application
</button>
</div>
)}
{/* Feature 4 */}
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105">
<div className="text-5xl mb-4 text-red-700">📚</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center">
Understand the Policy
</h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
Learn about the eligibility, funding process, and guidelines for
financial assistance.
</p>
<button
onClick={() => navigate("../policy")}
className="px-6 py-3 sm:px-5 sm:py-2 md:px-6 md:py-3 bg-red-700 text-white font-semibold text-lg sm:text-base md:text-lg rounded-lg hover:bg-red-600 transition-all transform hover:scale-110 shadow-md"
>
Learn More
</button>
</div>
</div>
</section>
{/* Footer Section */}
<footer className="bg-red-700 py-1 text-white text-center snap-center">
<p className="text-xs sm:text-sm md:text-base">
© {new Date().getFullYear()} Travel Policy SVU. All Rights Reserved.
</p>
</footer>
</div>
);
}
export default Dashboard;

View File

@@ -0,0 +1,33 @@
/* General styles for login page */
.login-page {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.login {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.loginPage_bg {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
object-fit:cover;
z-index: -1;
opacity: 0.6;
}
.login-container {
margin: 2rem;
}

View File

@@ -0,0 +1,37 @@
import React, { useState } from 'react';
import './Login.css';
import loginPageBg from '/images/campus_bg.jpeg';
import 'bootstrap/dist/css/bootstrap.min.css';
import ApplicantLogin from './components/ApplicantLogin';
import ValidatorLogin from './components/ValidatorLogin';
const Login = () => {
const [isApplicant, setIsApplicant] = useState(true);
const toggleRole = () => {
setIsApplicant(!isApplicant);
};
return (
<div className="login-page">
<img src={loginPageBg} className='loginPage_bg' />
<div className='login'>
<div className={`login-container`}>
{isApplicant ? (
<>
<ApplicantLogin changeRole={toggleRole} />
</>
) : (
<>
<ValidatorLogin changeRole={toggleRole} />
</>
)
}
</div>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './LoginAnimation.css';
function ApplicantLogin({ changeRole }) {
const [credentials, setCredentials] = useState({ email: 'faculty.computer.kjsce@example.com', password: 'securePassword123' });
const [animate, setAnimate] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false); // Loading state
const handleChangeRole = () => {
setAnimate(true);
setTimeout(() => {
changeRole();
}, 800); // Match this timeout duration to your animation duration
};
const handleSubmit = async (e) => {
e.preventDefault();
// Basic Validation
if (!credentials.email || !credentials.password) {
setError('Please enter both email and password.');
return;
}
if (!/\S+@\S+\.\S+/.test(credentials.email)) {
setError('Please enter a valid email address.');
return;
}
setLoading(true); // Show loading state
setError(''); // Reset previous errors
try {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/applicant-login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(credentials),
});
const result = await response.json();
if (response.ok) {
window.location.href = '/applicant/dashboard';
} else {
setError(result.message || 'Invalid login credentials.');
}
} catch (error) {
console.error('Error during login:', error);
setError('An error occurred. Please try again later.');
} finally {
setLoading(false); // Hide loading state
}
};
return (
<div className="flex flex-col md:flex-row bg-red-700 shadow-lg rounded-lg overflow-hidden max-w-4xl mx-auto">
<div className={`w-full md:w-3/4 bg-red-700 p-4 flex flex-col justify-center ${animate ? 'slide-out-right' : 'fade-in-fwd'}`}>
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">Travel Policy</h2>
<p className="text-white text-sm md:text-base mb-6 hidden md:block">
Our web application simplifies the process of requesting, approving, and managing financial support for research students and associates.
</p>
<h3 className="text-white text-lg md:text-xl font-bold">Validator?</h3>
<p className="text-white mb-3">Go to Validators Sign in</p>
<button
type='button'
className="bg-white text-red-700 text-sm md:text-base px-3 py-1.5 rounded-full font-semibold shadow-md hover:bg-gray-100 transition"
onClick={handleChangeRole}
>
Click Here
</button>
</div>
<div className={`bg-white w-full md:w-3/4 p-8 flex flex-col justify-center ${animate ? 'text-blur-out' : 'fade-in-fwd'}`}>
<h2 className="text-lg md:text-xl lg:text-2xl font-bold mb-3">Login for Applicants<span role="img" aria-label="wave">👋</span></h2>
<button
type='button'
className="bg-gray-100 text-gray-700 text-sm md:text-base px-4 py-2 rounded-full font-semibold mb-3 shadow-md flex items-center justify-center hover:bg-gray-200 transition-transform transform hover:scale-105"
onClick={handleSubmit}
>
<svg
className="w-6 h-6 mr-2"
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5354C22.436 21.7713 19.482 24 16 24C11.582 24 8.00002 20.418 8.00002 16C8.00002 11.582 11.582 8 16 8C18.0394 8 19.8947 8.76934 21.3074 10.026L25.0787 6.25467C22.6974 4.03534 19.512 2.66667 16 2.66667C8.63669 2.66667 2.66669 8.63667 2.66669 16C2.66669 23.3633 8.63669 29.3333 16 29.3333C23.3634 29.3333 29.3334 23.3633 29.3334 16C29.3334 15.106 29.2414 14.2333 29.074 13.3887Z" fill="#FFC107"/>
<path d="M4.20398 9.794L8.58465 13.0067C9.76998 10.072 12.6406 8 16 8C18.0393 8 19.8946 8.76934 21.3073 10.026L25.0786 6.25467C22.6973 4.03534 19.512 2.66667 16 2.66667C10.8786 2.66667 6.43731 5.558 4.20398 9.794Z" fill="#FF3D00"/>
<path d="M16 29.3333C19.444 29.3333 22.5733 28.0153 24.9393 25.872L20.8127 22.38C19.429 23.4323 17.7383 24.0014 16 24C12.532 24 9.58734 21.7887 8.478 18.7027L4.13 22.0527C6.33667 26.3707 10.818 29.3333 16 29.3333Z" fill="#4CAF50"/>
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5353C23.0095 20.1443 22.0622 21.4354 20.8107 22.3807L20.8127 22.3793L24.9393 25.8713C24.6473 26.1367 29.3333 22.6667 29.3333 16C29.3333 15.106 29.2413 14.2333 29.074 13.3887Z" fill="#1976D2"/>
</svg>
Login With Google
</button>
<p className="text-center text-gray-500 text-xs md:text-sm mb-3">or use email</p>
{/* Display Error Message */}
{error && <div className="text-red-600 text-sm mb-3">{error}</div>}
<form onSubmit={handleSubmit}>
<input
placeholder="Email"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
value={credentials.email}
onChange={(event) => setCredentials(prev => ({ ...prev, email: event.target.value }))}
/>
<input
type="password"
placeholder="Password"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
autoComplete='on'
value={credentials.password}
onChange={(event) => setCredentials(prev => ({ ...prev, password: event.target.value }))}
/>
<div className="flex flex-col md:flex-row items-center justify-between mb-3">
<label className="flex items-center mb-2 md:mb-0 text-sm md:text-base">
<input type="checkbox" className="mr-2" />
<span>Remember me</span>
</label>
<a href="#" className="text-red-700 text-sm md:text-base hover:underline">Forgot Password?</a>
</div>
<button
type="submit"
className={`bg-red-700 text-white text-sm md:text-base w-full py-2 rounded-lg font-semibold shadow-md hover:bg-red-800 transition ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={loading}
>
{loading ? 'Logging in...' : 'Log in'}
</button>
</form>
</div>
</div>
);
}
export default ApplicantLogin;

View File

@@ -0,0 +1,143 @@
/**
* ----------------------------------------
* animation slide-out-right
* ----------------------------------------
*/
@-webkit-keyframes slide-out-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(1000px);
transform: translateX(1000px);
opacity: 0;
}
}
@keyframes slide-out-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(1000px);
transform: translateX(1000px);
opacity: 0;
}
}
/* Apply animation to the specific class */
.slide-out-right {
-webkit-animation: slide-out-right 1s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
animation: slide-out-right 1s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
}
/**
* ----------------------------------------
* animation slide-out-left
* ----------------------------------------
*/
@-webkit-keyframes slide-out-left {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(-1000px);
transform: translateX(-1000px);
opacity: 0;
}
}
@keyframes slide-out-left {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(-1000px);
transform: translateX(-1000px);
opacity: 0;
}
}
/* Apply animation to the specific class */
.slide-out-left {
-webkit-animation: slide-out-left 1s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
animation: slide-out-left 1s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
}
/**
* ----------------------------------------
* animation text-blur-out
* ----------------------------------------
*/
@-webkit-keyframes text-blur-out {
0% {
-webkit-filter: blur(0.01);
filter: blur(0.01);
}
100% {
-webkit-filter: blur(12px) opacity(0%);
filter: blur(12px) opacity(0%);
}
}
@keyframes text-blur-out {
0% {
-webkit-filter: blur(0.01);
filter: blur(0.01);
}
100% {
-webkit-filter: blur(12px) opacity(0%);
filter: blur(12px) opacity(0%);
}
}
/* Apply animation to the specific class */
.text-blur-out {
-webkit-animation: text-blur-out 0.7s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
animation: text-blur-out 0.7s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
}
/**
* ----------------------------------------
* animation fade-in-fwd
* ----------------------------------------
*/
@-webkit-keyframes fade-in-fwd {
0% {
-webkit-transform: translateZ(-80px);
transform: translateZ(-80px);
opacity: 0;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
@keyframes fade-in-fwd {
0% {
-webkit-transform: translateZ(-80px);
transform: translateZ(-80px);
opacity: 0;
}
100% {
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1;
}
}
/* Apply animation to the specific class */
.fade-in-fwd {
-webkit-animation: fade-in-fwd 0.6s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
animation: fade-in-fwd 0.6s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
}

View File

@@ -0,0 +1,143 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './LoginAnimation.css';
function ValidatorLogin({ changeRole }) {
const [credentials, setCredentials] = useState({ email: 'hod.computer.kjsce@example.com', password: 'securePassword123' });
const [animate, setAnimate] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false); // Loading state
const handleChangeRole = () => {
setAnimate(true);
setTimeout(() => {
changeRole();
}, 800); // Ensure this matches your CSS animation duration
};
// Basic email validation
const validateEmail = (email) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
};
const handleSubmit = async (e) => {
e.preventDefault();
// Basic validation
if (!credentials.email || !credentials.password) {
setError('Please enter both email and password.');
return;
}
// Validate email format
if (!validateEmail(credentials.email)) {
setError('Please enter a valid email address.');
return;
}
setLoading(true); // Show loading state
setError(''); // Reset previous errors
try {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/validator-login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(credentials),
});
const result = await response.json();
if (response.ok) {
// Handle successful login (navigate, store tokens, etc.)
window.location.href = '/validator/dashboard';
} else {
setError(result.message || 'Invalid login credentials.');
}
} catch (error) {
console.error('Error during login:', error);
setError('An error occurred. Please try again later.');
} finally {
setLoading(false); // Hide loading state
}
};
return (
<div className="flex flex-col md:flex-row bg-red-700 shadow-lg rounded-lg overflow-hidden max-w-4xl mx-auto">
<div className={`bg-white w-full md:w-3/4 p-8 flex flex-col justify-center ${animate ? 'text-blur-out' : 'fade-in-fwd'}`}>
<h2 className="text-lg md:text-xl lg:text-2xl font-bold mb-3">Login for Validator<span role="img" aria-label="wave">👋</span></h2>
<button
type='button'
className="bg-gray-100 text-gray-700 text-sm md:text-base px-4 py-2 rounded-full font-semibold mb-3 shadow-md flex items-center justify-center hover:bg-gray-200 transition-transform transform hover:scale-105"
onClick={handleSubmit}
>
<svg
className="w-6 h-6 mr-2" // Adjust the size of the icon if needed
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5354C22.436 21.7713 19.482 24 16 24C11.582 24 8.00002 20.418 8.00002 16C8.00002 11.582 11.582 8 16 8C18.0394 8 19.8947 8.76934 21.3074 10.026L25.0787 6.25467C22.6974 4.03534 19.512 2.66667 16 2.66667C8.63669 2.66667 2.66669 8.63667 2.66669 16C2.66669 23.3633 8.63669 29.3333 16 29.3333C23.3634 29.3333 29.3334 23.3633 29.3334 16C29.3334 15.106 29.2414 14.2333 29.074 13.3887Z" fill="#FFC107"/>
<path d="M4.20398 9.794L8.58465 13.0067C9.76998 10.072 12.6406 8 16 8C18.0393 8 19.8946 8.76934 21.3073 10.026L25.0786 6.25467C22.6973 4.03534 19.512 2.66667 16 2.66667C10.8786 2.66667 6.43731 5.558 4.20398 9.794Z" fill="#FF3D00"/>
<path d="M16 29.3333C19.444 29.3333 22.5733 28.0153 24.9393 25.872L20.8127 22.38C19.429 23.4323 17.7383 24.0014 16 24C12.532 24 9.58734 21.7887 8.478 18.7027L4.13 22.0527C6.33667 26.3707 10.818 29.3333 16 29.3333Z" fill="#4CAF50"/>
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5353C23.0095 20.1443 22.0622 21.4354 20.8107 22.3807L20.8127 22.3793L24.9393 25.8713C24.6473 26.1367 29.3333 22.6667 29.3333 16C29.3333 15.106 29.2413 14.2333 29.074 13.3887Z" fill="#1976D2"/>
</svg>
Login With Google
</button>
<p className="text-center text-gray-500 text-xs md:text-sm mb-3">or use email</p>
{/* Display Error Message */}
{error && <div className="text-red-600 text-sm mb-3">{error}</div>}
<form onSubmit={handleSubmit}>
<input
placeholder="Email"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
value={credentials.email}
onChange={(event) => setCredentials(prev => ({ ...prev, email: event.target.value }))}
/>
<input
type="password"
placeholder="Password"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
value={credentials.password}
onChange={(event) => setCredentials(prev => ({ ...prev, password: event.target.value }))}
/>
<div className="flex flex-col md:flex-row items-center justify-between mb-3">
<label className="flex items-center mb-2 md:mb-0 text-sm md:text-base">
<input type="checkbox" className="mr-2" />
<span>Remember me</span>
</label>
<a href="#" className="text-red-700 text-sm md:text-base hover:underline">Forgot Password?</a>
</div>
<button
type="submit"
className={`bg-red-700 text-white text-sm md:text-base w-full py-2 rounded-lg font-semibold shadow-md hover:bg-red-800 transition ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={loading}
>
{loading ? 'Logging in...' : 'Log in'}
</button>
</form>
</div>
<div className={`w-full md:w-3/4 bg-red-700 p-4 flex flex-col justify-center ${animate ? 'slide-out-left' : 'fade-in-fwd'}`}>
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">Travel Policy</h2>
<p className="text-white text-sm md:text-base mb-6 hidden md:block">
Our web application simplifies the process of requesting, approving, and managing financial support for research students and associates.
</p>
<h3 className="text-white text-lg md:text-xl font-bold">Applicant?</h3>
<p className="text-white mb-3">Go to Applicants Sign in</p>
<button type='button' className="bg-white text-red-700 text-sm md:text-base px-3 py-1.5 rounded-full font-semibold shadow-md hover:bg-gray-100 transition" onClick={handleChangeRole}>Click Here</button>
</div>
</div>
);
}
export default ValidatorLogin;

View File

View File

@@ -0,0 +1,11 @@
import React from 'react'
function Policy() {
return (
<div>
Policy
</div>
)
}
export default Policy

View File

@@ -0,0 +1,30 @@
export const CardsData = [
{
title: "Total Funds Deployed",
color: {
backGround: "linear-gradient(#93A5CF, #E4EfE9)",
boxShadow: "0px 10px 20px 0px rgba(0, 0, 0, 0.1)",
},
value: "12,23,234",
series: [
{
name: "Funds",
data: [20134, 1200, 23532, 23543, 12564,23346,23454,54634,45364,45634,45745,564],
},
],
},
{
title: "Enrollment rate",
color: {
backGround: "linear-gradient(#93A5CF, #E4EfE9)",
boxShadow: "0px 10px 20px 0px rgba(0, 0, 0, 0.1)",
},
value: "90%",
series: [
{
name: "Enrollment Trends",
data: [200, 145, 232, 543, 564,342,635,345,346,123,543],
},
],
},
];

View File

@@ -0,0 +1,22 @@
import React, { useState } from "react";
import Loading from "../../components/Loading";
import Charts from "../Report/components/charts";
import FilterDataForm from "./components/FilterDataForm";
function Report() {
const [reportData, setReportData] = useState({
data: [],
query: {},
});
const [loading, setLoading] = useState(false);
return (
<main className="flex flex-col p-6">
<div className="bg-white shadow rounded-lg p-6 w-full">
<FilterDataForm setReportData={setReportData} setLoading={setLoading} />
{loading ? <Loading /> : <Charts reportData={reportData} />}
</div>
</main>
);
}
export default Report;

View File

@@ -0,0 +1,152 @@
import React, { useEffect } from "react";
import { Formik } from "formik";
import { useSubmit, useRouteLoaderData, useNavigation } from "react-router-dom";
import { filterDataFormFeilds } from "./FilterDataFormFeilds";
import * as yup from "yup";
import Input from "../../ApplicationForm/Input";
import axios from "axios";
function FilterDataForm({ setReportData, setLoading }) {
const { role, user } = useRouteLoaderData("Validator-Root")?.data;
const navigation = useNavigation();
const isSubmittingNav = navigation.state === "submitting";
const prefilledData =
user?.institute || user?.department
? {
institute: user?.institute,
department: user?.department,
}
: null;
const formFields = prefilledData
? filterDataFormFeilds.map((section) => ({
...section,
fields: section.fields.map((field) => ({
...field,
disabled: prefilledData[field.name],
})),
}))
: filterDataFormFeilds;
const createInitialValuesScheme = (formFields) => {
const schema = {};
formFields?.forEach((section) => {
section?.fields?.forEach((field) => {
if (prefilledData) {
if (field.type === "miniForm" || field.type === "checkbox") {
schema[field.name] = JSON.parse(prefilledData[field.name]);
} else {
schema[field.name] = prefilledData[field.name];
}
} else if (field.type === "checkbox") {
schema[field.name] = false;
} else if (field.type === "miniForm") {
schema[field.name] = [];
} else {
schema[field.name] = "";
}
});
});
return schema;
};
const initialValuesSchema = createInitialValuesScheme(formFields);
const createValidationSchema = (formFields) => {
const schema = {};
formFields?.forEach((section) => {
section.fields?.forEach((field) => {
if (field.validation) {
schema[field.name] = field.validation;
}
});
});
return yup.object().shape(schema);
};
const validationSchema = createValidationSchema(formFields);
const handleSubmit = async (values, { setSubmitting, setErrors }) => {
const { institute, department, year, applicationType } = values;
try {
setLoading(true);
const queryParams = new URLSearchParams();
if (institute) queryParams.append("institute", institute);
if (department) queryParams.append("department", department);
if (year) queryParams.append("year", year);
if (applicationType) queryParams.append("applicationType", applicationType);
const res = await axios.get(
`http://localhost:3000/validator/getReportData?${queryParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
}
);
setReportData({data: res.data, query: values});
} catch (error) {
if (error.response && error.response.data) {
setErrors({ submit: error.response.data.message });
} else {
setErrors({ submit: "An unexpected error occurred" });
}
} finally {
setLoading(false);
setSubmitting(false);
}
};
useEffect(() => {
// Trigger form submission on first render
handleSubmit(initialValuesSchema, { setSubmitting: () => {}, setErrors: () => {} });
}, []);
return (
<Formik
initialValues={initialValuesSchema}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue, // Use setFieldValue for file handling
isSubmitting,
}) => (
<form onSubmit={handleSubmit} className="bg-transparent">
<Input
values={values}
errors={errors}
touched={touched}
handleChange={handleChange}
handleBlur={handleBlur}
setFieldValue={setFieldValue} // Pass setFieldValue for file handling
formFeilds={formFields}
/>
<button
type="submit"
disabled={isSubmitting || isSubmittingNav}
className="w-full flex items-center justify-center bg-gradient-to-r from-red-600 to-red-800 hover:from-red-800 hover:to-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transform transition duration-300 ease-in-out disabled:bg-gray-400"
>
{isSubmitting || isSubmittingNav ? "Gettting Data" : "Get Data"}
</button>
</form>
)}
</Formik>
);
}
export default FilterDataForm;

View File

@@ -0,0 +1,65 @@
import * as yup from "yup";
import {
institutes,
instituteDepartmentMapping,
} from "../../../components/BaseData";
const currentYear = new Date().getFullYear();
const yearOptions = [];
for (let year = 2018; year <= currentYear; year++) {
yearOptions.push({ label: year.toString(), value: year.toString() });
}
const filterDataFormFeilds = [
{
label: "Travel Polciy Report",
fields: [
{
label: "Select Institute",
name: "institute",
type: "dropdown",
options: {
"": institutes,
},
validation: yup
.string()
.notRequired("Department selection is notRequired"),
},
{
depend: "institute",
label: "Select Department",
name: "department",
type: "dropdown",
options: instituteDepartmentMapping,
validation: yup
.string()
.notRequired("Department selection is notRequired"),
},
{
label: "Select Application Type",
name: "applicationType",
type: "dropdown",
options: {
"": [
{ label: "Student Applications", value: "STUDENT" },
{ label: "Faculty Applications", value: "FACULTY" },
],
},
validation: yup
.string()
.notRequired("Department selection is notRequired"),
},
{
label: "Select Year",
name: "year",
type: "dropdown",
options: {
"": yearOptions,
},
validation: yup.string().notRequired("Year is required"),
},
],
},
];
export { filterDataFormFeilds };

View File

@@ -0,0 +1,90 @@
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
function OverTheYearsLine() {
const options = {
responsive: true, // Make the chart responsive to window resizing
maintainAspectRatio: false, // Allow chart size to change dynamically (optional)
// Title configuration
plugins: {
title: {
display: true,
text: "Applications Over the Years", // Set a title for the chart
font: {
size: 16,
},
},
tooltip: {
// Customize tooltips
callbacks: {
label: function (context) {
// Format tooltip labels
return `${context.dataset.label}: ${context.raw} steps`;
},
},
},
legend: {
display: true,
position: "top", // Legend position: 'top', 'left', 'bottom', 'right'
},
},
// Scales configuration (e.g., setting up x and y axes)
scales: {
x: {
// X-axis configuration (labels are auto-set)
title: {
display: true,
text: "Year", // Label for the X-axis
},
},
y: {
// Y-axis configuration
title: {
display: true,
text: "Number of Applications", // Label for the Y-axis
},
// ticks: {
// // Custom tick marks
// beginAtZero: true, // Start Y-axis from 0
// stepSize: 9000, // Tick step size for Y-axis
// },
},
},
};
const data = {
labels: [2020, 2021, 2022, 2023, 2024],
datasets: [
{
label: "Steps",
data: [30, 50, 45, 90, 35],
borderColor: "rgb(75, 192, 192)",
},
],
};
return <Line options={options} data={data} />;
}
export default OverTheYearsLine;

View File

@@ -0,0 +1,65 @@
import { Pie } from "react-chartjs-2";
import {
Chart as ChartJS,
ArcElement,
Tooltip,
Legend,
} from "chart.js";
// Register chart components
ChartJS.register(ArcElement, Tooltip, Legend);
function OverTheYearsPie() {
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: "Steps Distribution Over Years",
font: {
size: 16,
},
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.label}: ${context.raw} steps`;
},
},
},
legend: {
display: true,
position: "top",
},
},
};
const data = {
labels: ["2020", "2021", "2022", "2023", "2024"], // Labels for the pie slices
datasets: [
{
data: [3000, 5000, 4500, 9000, 12000], // Data for the pie chart
backgroundColor: [
"rgba(75, 192, 192, 0.5)", // Color for the 2020 slice
"rgba(255, 99, 132, 0.5)", // Color for the 2021 slice
"rgba(54, 162, 235, 0.5)", // Color for the 2022 slice
"rgba(153, 102, 255, 0.5)", // Color for the 2023 slice
"rgba(255, 159, 64, 0.5)", // Color for the 2024 slice
],
borderColor: [
"rgb(75, 192, 192)",
"rgb(255, 99, 132)",
"rgb(54, 162, 235)",
"rgb(153, 102, 255)",
"rgb(255, 159, 64)",
],
borderWidth: 1,
},
],
};
return <Pie options={options} data={data} />;
}
export default OverTheYearsPie;

View File

@@ -0,0 +1,166 @@
import React from "react";
import {
Page,
Text,
View,
Document,
StyleSheet,
Image,
} from "@react-pdf/renderer";
// Create styles
const styles = StyleSheet.create({
page: {
flexDirection: "column",
backgroundColor: "white",
padding: 20,
},
sectionTitle: {
textAlign: "center",
fontSize: 16,
fontWeight: "bold",
marginBottom: 10,
},
section: {
margin: 10,
padding: 10,
},
viewer: {
width: "75vw", // Full width
height: "100vh", // Full height
},
cardContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginVertical: 10,
},
card: {
width: "45%",
padding: 10,
backgroundColor: "#f8e7d1",
borderRadius: 5,
textAlign: "center",
fontSize: 14,
fontWeight: "bold",
},
chartContainer: {
marginVertical: 20,
textAlign: "center",
},
table: {
display: "table",
width: "auto",
borderStyle: "solid",
borderWidth: 1,
borderColor: "#bfbfbf",
borderBottomWidth: 0,
borderRightWidth: 0,
},
tableRow: {
flexDirection: "row",
},
tableColHeader: {
width: "25%",
borderStyle: "solid",
borderColor: "#bfbfbf",
borderRightWidth: 1,
borderBottomWidth: 1,
backgroundColor: "#f2f2f2",
padding: 5,
textAlign: "center",
},
tableCol: {
width: "25%",
borderStyle: "solid",
borderColor: "#bfbfbf",
borderRightWidth: 1,
borderBottomWidth: 1,
padding: 5,
textAlign: "center",
},
tableCellHeader: {
margin: 5,
fontSize: 12,
fontWeight: "bold",
},
tableCell: {
margin: 5,
fontSize: 10,
},
image: {
width: 400,
height: 300,
},
});
// Create Document Component
const ReportPDF = ({ tableData, chartImages }) => {
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Title */}
<Text style={styles.sectionTitle}>Travel Policy Report</Text>
{/* Summary Cards */}
{/* <View style={styles.cardContainer}>
<View style={styles.card}>
<Text>Total Funds Deployed</Text>
<Text>12,23,234</Text>
</View>
<View style={styles.card}>
<Text>Enrollment Rate</Text>
<Text>90%</Text>
</View>
</View> */}
{/* Table */}
<View style={styles.table}>
<View style={styles.tableRow}>
<View style={styles.tableColHeader}>
<Text style={styles.tableCellHeader}>ID</Text>
</View>
<View style={styles.tableColHeader}>
<Text style={styles.tableCellHeader}>Stream</Text>
</View>
<View style={styles.tableColHeader}>
<Text style={styles.tableCellHeader}>Scholarship</Text>
</View>
<View style={styles.tableColHeader}>
<Text style={styles.tableCellHeader}>Funds</Text>
</View>
</View>
{tableData?.map((row) => (
<View key={row.id} style={styles.tableRow}>
<View style={styles.tableCol}>
<Text style={styles.tableCell}>{row.id}</Text>
</View>
<View style={styles.tableCol}>
<Text style={styles.tableCell}>{row.Stream}</Text>
</View>
<View style={styles.tableCol}>
<Text style={styles.tableCell}>{row.Scholarship}</Text>
</View>
<View style={styles.tableCol}>
<Text style={styles.tableCell}>{row.Funds}</Text>
</View>
</View>
))}
</View>
{/* Charts */}
{chartImages?.barChart && (
<Image src={chartImages.barChart} style={styles.image} />
)}
{chartImages?.pieChart1 && (
<Image src={chartImages.pieChart1} style={styles.image} />
)}
{chartImages?.pieChart2 && (
<Image src={chartImages.pieChart2} style={styles.image} />
)}
</Page>
</Document>
);
};
export default ReportPDF;

View File

@@ -0,0 +1,37 @@
import React from "react";
const Table = ({ tableData }) => {
return (
<div className="table-responsive">
<table
style={{
width: "100%",
borderCollapse: "collapse",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
}}
>
<thead>
<tr style={{ backgroundColor: "#f4f4f4" }}>
<th style={{ padding: "10px", border: "1px solid #ddd" }}>ID</th>
<th style={{ padding: "10px", border: "1px solid #ddd" }}>Stream</th>
<th style={{ padding: "10px", border: "1px solid #ddd" }}>Scholarship</th>
<th style={{ padding: "10px", border: "1px solid #ddd" }}>Funds</th>
</tr>
</thead>
<tbody>
{tableData?.map((row) => (
<tr key={row.id}>
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.id}</td>
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.Stream}</td>
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.Scholarship}</td>
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.Funds}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Table;

View File

@@ -0,0 +1,146 @@
import React, { useState } from "react";
import { Bar } from "react-chartjs-2";
const ChartWithDropdown = () => {
// Chart data options for faculty, students, HOI, and HOD
const chartDataOptions = {
faculty: {
approved: {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"],
datasets: [
{
label: "Approved Applications (Faculty)",
data: [100, 150, 200, 250, 300,400,900,132,920,1000,890 ,100],
backgroundColor: "rgba(75, 192, 192, 0.5)",
borderColor: "rgb(75, 192, 192)",
borderWidth: 1,
},
],
},
rejected: {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"],
datasets: [
{
label: "Rejected Applications (Faculty)",
data: [50, 60, 70, 80, 20,40,90,78,23,29,98,33],
backgroundColor: "rgba(255, 99, 132, 0.5)",
borderColor: "rgb(255, 99, 132)",
borderWidth: 1,
},
],
},
},
HOI: {
approved: {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"],
datasets: [
{
label: "Approved Applications (HOI)",
data: [1200, 1500, 1800, 2200, 2500,2000,1999,3453,2345,5633,2345,5647],
backgroundColor: "rgba(54, 162, 235, 0.5)",
borderColor: "rgb(54, 162, 235)",
borderWidth: 1,
},
],
},
rejected: {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"],
datasets: [
{
label: "Rejected Applications (HOI)",
data: [200, 300, 400, 500, 450, 350, 320, 410, 360, 430, 300, 250],
backgroundColor: "rgba(255, 159, 64, 0.5)",
borderColor: "rgb(255, 159, 64)",
borderWidth: 1,
},
],
},
},
HOD: {
approved: {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"],
datasets: [
{
label: "Approved Applications (HOD)",
data: [300, 400, 500, 450, 400, 350, 300, 250, 200, 150, 100, 50],
backgroundColor: "rgba(153, 102, 255, 0.5)",
borderColor: "rgb(153, 102, 255)",
borderWidth: 1,
},
],
},
rejected: {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"],
datasets: [
{
label: "Rejected Applications (HOD)",
data: [30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140],
backgroundColor: "rgba(255, 206, 86, 0.5)",
borderColor: "rgb(255, 206, 86)",
borderWidth: 1,
},
],
},
},
};
const chartOptions = {
responsive: true,
plugins: {
legend: { display: true },
title: { display: true, text: "Applications Over the Years" },
},
scales: {
x: { title: { display: true, text: "Year" } },
y: { title: { display: true, text: "Number of Applications" }, beginAtZero: true },
},
};
const [category, setCategory] = useState("faculty"); // Faculty, HOI, or HOD
const [applicationType, setApplicationType] = useState("approved"); // Approved or Rejected
// Fetch the data based on the selected category and application type
const data =
chartDataOptions[category]?.[applicationType] ||
chartDataOptions["faculty"]["approved"];
return (
<div style={{ width: "100%", margin: "auto", padding: "20px", flexGrow: 1 }}>
{/* Dropdown for selecting category */}
<div style={{ marginBottom: "20px" }}>
<label htmlFor="category-select" style={{ marginRight: "10px" }}>
Select Category:
</label>
<select
id="category-select"
value={category}
onChange={(e) => setCategory(e.target.value)}
style={{ padding: "5px", fontSize: "16px", marginRight: "20px", borderRadius: "15px", textAlign: "center", border: "2px solid black" }}
>
<option value="faculty">Faculty</option>
<option value="HOI">HOI</option>
<option value="HOD">HOD</option>
</select>
{/* Dropdown for selecting application type */}
<label htmlFor="type-select" style={{ marginRight: "10px" }}>
Select Application Type:
</label>
<select
id="type-select"
value={applicationType}
onChange={(e) => setApplicationType(e.target.value)}
style={{ padding: "5px", fontSize: "16px", borderRadius: "15px", textAlign: "center", border: "2px solid black" }}
>
<option value="approved">Approved Applications</option>
<option value="rejected">Rejected Applications</option>
</select>
</div>
{/* Chart */}
{data && <Bar data={data} options={chartOptions} />}
</div>
);
};
export default ChartWithDropdown;

View File

@@ -0,0 +1,143 @@
import React, { useState } from "react";
import { motion } from "framer-motion";
import "./cards.css";
import { Bar } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from "chart.js";
// Register chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
const Card = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<div className="card">
<motion.div
layout
onClick={() => setExpanded(!expanded)} // Toggle the state
className="motion_card"
>
{!expanded && ( // Render CompactCard when NOT expanded
<motion.div>
<CompactCard param={props} />
</motion.div>
)}
{expanded && ( // Render ExpandedCard when expanded
<motion.div>
<ExpandedCard param={props} />
</motion.div>
)}
</motion.div>
</div>
);
};
// Compact Card Component
function CompactCard({ param }) {
return (
<div className="CompactCard">
<div className="data">
<h1>{param.title}</h1>
</div>
<span>{param.value}</span>
</div>
);
}
// Expanded Card Component
function ExpandedCard({ param }) {
const barOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: "Number of Applications Over the Years ",
},
},
scales: {
x: {
title: {
display: true,
text: "Year",
},
},
y: {
title: {
display: true,
text: "Number of Applications",
},
ticks: {
beginAtZero: true,
},
},
},
};
const chartData = {
labels: ["Jan", "Feb", "Mar", "April", "May","June","July","Aug","Sep","Nov","Dec"], // Example years
datasets: [
{
label: param.series[0].name, // e.g., "Applications"
data: param.series[0].data, // e.g., [100, 150, 200, 250, 300]
backgroundColor: "rgba(75, 192, 192, 0.6)", // Bar color
},
],
};
return (
<motion.div
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="ExpandedCard"
style={{
position: "fixed", // Make it fixed to cover the whole screen
top: 0,
left: 0,
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.5)", // Semi-transparent background
zIndex: 999, // Ensure it's on top of other elements
}}
>
<div
className="expandedCardContent"
style={{
backgroundColor: "white",
padding: "20px",
borderRadius: "10px",
width: "70%",
height: "70%"
}}
>
<div className="data">
<h1>{param.title}</h1>
</div>
<Bar options={barOptions} data={chartData} />
<span>{param.value}</span>
</div>
</motion.div>
)
}
export default Card;

View File

@@ -0,0 +1,63 @@
.generalInfo{
display: flex;
flex-direction: column;
flex-grow: 1;
width: 30%;
padding: 10px;
}
.cards{
display: flex;
gap: 10px;
padding: 10px;
flex-direction: row;
}
.Cards{
display: flex;
flex-direction: column;
gap: 20px;
margin: 20px;
}
.CompactCard{
display: flex;
flex-direction: row;
padding: 20px;
width: 300px;
height: 200px;
align-items: center;
gap: 20px;
cursor: pointer;
background-color: antiquewhite;
border-width: 5px;
border-color: rgb(85, 85, 85);
border-radius: 5px;
filter: drop-shadow(2px 4px 6px rgb(114, 114, 114));
}
.CompactCard:hover
{
filter: drop-shadow(2px 4px 6px rgb(255, 255, 255));
}
.data>h1
{
font-size: large;
font-weight: 1000;
}
.CompactCard>span
{
display: flex;
flex-direction: row;
align-items: end;
}
.motionCard{
gap: 20px;
}
.h{
display: flex;
flex-direction: row;
margin: 10px;
gap: 10px;
}

View File

@@ -0,0 +1,28 @@
import './cards.css'
import React from 'react'
import Card from './card'
import { CardsData } from '../Data';
const Cards= () =>{
return(
<div className="Cards">
{CardsData.map((card , id)=>{
return(
<div className="parentContainer">
<Card
title={card.title}
color={card.color}
value={card.value}
series={card.series}
/>
</div>
)
})}
</div>
);
}
export default Cards

View File

@@ -0,0 +1,440 @@
import React, { useEffect, useRef, useState } from "react";
import ChartWithDropdown from "./approved";
import Cards from "./cards";
import "./cards.css";
import { Bar, Pie } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import ChartDataLabels from 'chartjs-plugin-datalabels';
import Table from "./Table";
import { PDFDownloadLink, PDFViewer } from "@react-pdf/renderer";
import ApprovalVsRejectionTrends from "./map";
import ReportPDF from "./reportPDF";
// Register chart components for all three types (Line, Bar, Pie)
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
ArcElement,
Title,
Tooltip,
Legend,
ChartDataLabels
);
function Charts({ reportData }) {
const { data, query } = reportData;
if (!data) {
return (
<div className="text-center text-xl text-red-700 py-10">
No Data Found
</div>
);
}
const { acceptedApplications, rejectedApplications, pendingApplications } = data;
const tableData = [];
const groupedData = {};
if (acceptedApplications) {
for (const item of acceptedApplications) {
const { institute, department, formData } = item;
const { totalExpense } = formData;
if (!groupedData[institute]) {
groupedData[institute] = {};
}
if (query.institute) {
if (!groupedData[institute][department]) {
groupedData[institute][department] = {
totalExpense: 0,
applications: 0,
};
}
// Aggregate the data
groupedData[institute][department].totalExpense +=
parseFloat(totalExpense); // Summing the expenses
groupedData[institute][department].applications += 1;
} else {
if (!groupedData[institute].applications) {
groupedData[institute] = {
totalExpense: 0,
applications: 0,
};
}
// Aggregate the data
groupedData[institute].totalExpense += parseFloat(totalExpense); // Summing the expenses
groupedData[institute].applications += 1;
}
}
}
// Step 2: Transform grouped data into desired table format
if (query.institute) {
for (const institute in groupedData) {
for (const department in groupedData[institute]) {
const departmentData = groupedData[institute][department];
tableData.push({
id: tableData.length + 1,
Stream: department,
Scholarship: departmentData.applications, // Assuming each application is one scholarship
Purpose_of_Travel: departmentData.purposeOfTravel,
Funds: departmentData.totalExpense.toFixed(2), // Formatting funds to 2 decimal places
});
}
}
} else {
for (const institute in groupedData) {
const instituteData = groupedData[institute];
tableData.push({
id: tableData.length + 1,
Stream: institute,
Scholarship: instituteData.applications, // Assuming each application is one scholarship
Purpose_of_Travel: instituteData.purposeOfTravel,
Funds: instituteData.totalExpense.toFixed(2), // Formatting funds to 2 decimal places
});
}
}
const [chartImages, setChartImages] = useState({
barChart: null,
pieChart1: null,
pieChart2: null,
isLoading: false,
});
// Line Chart Data and Options
const lineOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: "Number of Applications Over the Years ",
},
},
scales: {
x: {
title: {
display: true,
text: "Year",
},
},
y: {
title: {
display: true,
text: "Number of Applications",
},
ticks: {
beginAtZero: true,
},
},
},
};
const lineData = {
labels: [2020, 2021, 2022, 2023, 2024],
datasets: [
{
label: "Applications",
data: [1200, 1500, 1800, 2200, 2500], // Updated data for number of applications
borderColor: "rgb(75, 192, 192)",
fill: false,
tension: 0.1,
},
],
};
// Bar Chart Data and Options
const barOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: "Number of Applications Over the Years ",
},
},
scales: {
x: {
title: {
display: true,
text: "Month",
},
},
y: {
title: {
display: true,
text: "Number of Applications",
},
ticks: {
beginAtZero: true,
},
},
},
};
const barData = {
labels: [
"Jan",
"Feb",
"Mar",
"April",
"May",
"June",
"July",
"Aug",
"Sep",
"Nov",
"Dec",
],
datasets: [
{
label: "Applications",
data: [
1200, 1500, 1800, 2200, 200, 800, 1235, 604, 2345, 2523, 3453, 6453,
], // Updated data for number of applications
backgroundColor: "rgba(75, 192, 192, 0.5)",
borderColor: "rgb(75, 192, 192)",
borderWidth: 1,
},
],
};
// Pie Chart Data and Options
const pieOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: "Purpose of Travel",
},
},
};
const pieData = {
labels: ["Academic", "Research", "Personal", "Other"],
datasets: [
{
data: [1200, 1500, 1800, 2200], // Updated data for number of applications
backgroundColor: [
"rgba(75, 192, 192, 0.5)",
"rgba(255, 99, 132, 0.5)",
"rgba(54, 162, 235, 0.5)",
"rgba(153, 102, 255, 0.5)",
],
borderColor: [
"rgb(75, 192, 192)",
"rgb(255, 99, 132)",
"rgb(54, 162, 235)",
"rgb(153, 102, 255)",
],
borderWidth: 1,
},
],
};
const pie_Options = {
responsive: true,
plugins: {
title: {
display: true,
text: "Travel",
},
},
};
const pie_Data = {
labels: ["Domestic", "International", "Local"],
datasets: [
{
data: [1200, 1500, 1800], // Updated data for number of applications
backgroundColor: [
"rgba(79, 246, 96, 0.5)",
"rgba(255, 99, 132, 0.5)",
"rgba(54, 162, 235, 0.5)",
],
borderColor: [
"rgb(79, 246, 96)",
"rgb(255, 99, 132)",
"rgb(54, 162, 235)",
],
borderWidth: 1,
},
],
};
// const barChartRef = useRef();
// const pieChartRef1 = useRef();
// const pieChartRef2 = useRef();
// const loadChartsInPdf = () => {
// const barChartInstance = barChartRef.current;
// const pieChartInstance1 = pieChartRef1.current;
// const pieChartInstance2 = pieChartRef2.current;
// if (barChartInstance) {
// const barBase64Image = barChartInstance.toBase64Image();
// setChartImages((prevImages) => ({
// ...prevImages,
// barChart: barBase64Image,
// }));
// }
// if (pieChartInstance1) {
// const pieBase64Image = pieChartInstance1.toBase64Image();
// setChartImages((prevImages) => ({
// ...prevImages,
// pieChart1: pieBase64Image,
// }));
// }
// if (pieChartInstance2) {
// const pieBase64Image = pieChartInstance2.toBase64Image();
// setChartImages((prevImages) => ({
// ...prevImages,
// pieChart2: pieBase64Image,
// }));
// }
// };
// useEffect(() => {
// setChartImages((prevImages) => ({ ...prevImages, isLoading: true }));
// const handleRender = () => {
// loadChartsInPdf();
// setChartImages((prevImages) => ({ ...prevImages, isLoading: false }));
// };
// const barChartInstance = barChartRef.current;
// const pieChartInstance1 = pieChartRef1.current;
// const pieChartInstance2 = pieChartRef2.current;
// if (barChartInstance) {
// barChartInstance.options.animation.onComplete = handleRender;
// }
// if (pieChartInstance1) {
// pieChartInstance1.options.animation.onComplete = handleRender;
// }
// if (pieChartInstance2) {
// pieChartInstance2.options.animation.onComplete = handleRender;
// }
// return () => {
// if (barChartInstance) {
// barChartInstance.options.animation.onComplete = null;
// }
// if (pieChartInstance1) {
// pieChartInstance1.options.animation.onComplete = null;
// }
// if (pieChartInstance2) {
// pieChartInstance2.options.animation.onComplete = null;
// }
// };
// }, []);
return (
<div className="p-10">
<h1 className="text-3xl mb-6">Travel Policy Report</h1>
{/* Container for all three charts */}
{/* <div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6">
<div className="w-full">
<Bar options={barOptions} data={barData} ref={barChartRef} />
</div>
<div className="w-full">
<Pie options={pieOptions} data={pieData} ref={pieChartRef1} />
</div>
</div> */}
{/* <div className="cards">
<Cards />
<div className="generalInfo">
<div className="card2">
<ChartWithDropdown />
</div>
</div>
</div> */}
{/* <div className="hh">
<ApprovalVsRejectionTrends />
</div> */}
{/* Line Chart */}
{/* <div className="w-full">
<Line options={lineOptions} data={lineData} />*/}
<div className="flex flex-col gap-10 items-center justify-center my-10">
<div className="w-full">
<Table tableData={tableData} />
</div>
{/*
<div>
<Pie options={pie_Options} data={pie_Data} ref={pieChartRef2} />
</div> */}
</div>
{chartImages.isLoading ? (
<div className="text-center text-xl text-red-700 py-10">
Generating PDF Report...
</div>
) : (
<div className="pdfreport">
<PDFDownloadLink
document={
<ReportPDF tableData={tableData} chartImages={chartImages} />
}
fileName={`report_${query.institute || "allInstitutes"}_${
query.department || "allDepartments"
}_${query.year || "allYears"}_${
query.applicationType || "allApplications"
}.pdf`}
>
{({ blob, url, loading, error }) =>
loading ? (
<div className="text-center text-xl text-red-700 py-10">
Getting Your PDF Report Ready...
</div>
) : (
<button
disabled={loading}
className="w-full flex items-center justify-center bg-gradient-to-r from-red-600 to-red-800 hover:from-red-800 hover:to-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transform transition duration-300 ease-in-out disabled:bg-gray-400"
type="button"
>
Download PDF
</button>
)
}
</PDFDownloadLink>
<PDFViewer style={{ width: "70vw", height: "100vh" }}>
<ReportPDF tableData={tableData} chartImages={chartImages} />
</PDFViewer>
</div>
)}
</div>
);
}
export default Charts;

View File

@@ -0,0 +1,92 @@
import React, { useState } from "react";
import { Line } from "react-chartjs-2";
import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Title, Tooltip, Legend, Filler } from "chart.js";
// Register required Chart.js components
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Title, Tooltip, Legend, Filler);
const ApprovalVsRejectionTrends = () => {
// Sample data for Approved and Rejected Applications
const applicationData = {
faculty: {
approved: [100, 150, 200, 250, 300, 400, 500, 450, 600, 550, 700, 650],
rejected: [50, 60, 70, 80, 100, 90, 120, 110, 130, 100, 140, 120],
},
HOI: {
approved: [500, 600, 700, 800, 750, 700, 900, 850, 1000, 950, 1100, 1050],
rejected: [100, 120, 140, 150, 130, 110, 180, 150, 200, 170, 220, 190],
},
HOD: {
approved: [300, 400, 350, 450, 500, 480, 550, 520, 600, 580, 650, 620],
rejected: [80, 90, 100, 110, 120, 100, 140, 130, 150, 140, 160, 150],
},
};
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// State for selected category
const [category, setCategory] = useState("faculty");
// Data for the Line Chart
const lineChartData = {
labels: months,
datasets: [
{
label: "Approved Applications",
data: applicationData[category].approved,
borderColor: "rgb(75, 192, 192)",
backgroundColor: "rgba(75, 192, 192, 0.2)",
tension: 0.4, // For a smooth curve
fill: true,
},
{
label: "Rejected Applications",
data: applicationData[category].rejected,
borderColor: "rgb(255, 99, 132)",
backgroundColor: "rgba(255, 99, 132, 0.2)",
tension: 0.4,
fill: true,
},
],
};
const lineChartOptions = {
responsive: true,
plugins: {
legend: { display: true, position: "top" },
title: { display: true, text: "Approval vs. Rejection Trends" },
},
scales: {
x: { title: { display: true, text: "Months" } },
y: { title: { display: true, text: "Number of Applications" }, beginAtZero: true },
},
};
return (
<div style={{ width: "90%", margin: "auto", padding: "20px" }}>
<h2 style={{ textAlign: "center" }}>Approval vs. Rejection Trends</h2>
{/* Dropdown to select category */}
<div style={{ marginBottom: "20px", textAlign: "center" }}>
<label htmlFor="category-select" style={{ marginRight: "10px" }}>
Select Category:
</label>
<select
id="category-select"
value={category}
onChange={(e) => setCategory(e.target.value)}
style={{ padding: "5px", fontSize: "16px", borderRadius: "8px", border: "1px solid #ccc" }}
>
<option value="faculty">Faculty</option>
<option value="HOI">HOI</option>
<option value="HOD">HOD</option>
</select>
</div>
{/* Line Chart */}
<Line data={lineChartData} options={lineChartOptions} />
</div>
);
};
export default ApprovalVsRejectionTrends;

View File

@@ -0,0 +1,8 @@
export { default as Login } from './login/Login'
export { default as Dashboard } from './Dashboard/Dashboard'
export { default as Applications } from './Applications/Applications'
export { default as Form } from './ApplicationForm/Form'
export { default as ApplicationView } from './ApplicationView/ApplicationView'
export { default as About } from './about/About'
export { default as Policy } from './policy/Policy'
export { default as Report } from './Report/Report'

View File

@@ -0,0 +1,52 @@
import { json } from "react-router-dom";
import { toastSuccess, toastError, toastWarning } from "../utils/toast";
export async function applicationStatusAction({ request, params }) {
const formData = await request.formData();
const action = formData.get("action")
try {
if (action === "accepted") {
const expenses = JSON.parse(formData.get("expenses"));
const hasUnverifiedExpense = expenses.some(item => item?.proofStatus !== "verified");
if (hasUnverifiedExpense) {
toastWarning("Please verify all the proofs before approving");
return json(
{ message: "Please verify all the proofs before approving" },
{ status: 400 }
);
}
}
const res = await fetch(
`${
import.meta.env.VITE_APP_API_URL
}/validator/statusAction`,
{
method: "PUT",
credentials: "include",
body: formData,
}
);
if (res.status === 401) {
return json({ message: "Unauthorized access" }, { status: res.status });
}
if (!res.ok) {
toastError(res.statusText);
return json({ message: res.statusText }, { status: res.status });
}
toastSuccess(`Application ${action.slice(0, 1).toUpperCase() + action.slice(1).toLowerCase()} Successfully`);
window.location.reload()
return null;
} catch (error) {
console.error("Fetch error:", error);
throw json({ message: error.message }, { status: error.status || 500 });
}
}

View File

@@ -0,0 +1,49 @@
import { json, redirect } from 'react-router-dom';
import { toastSuccess, toastError, toastSecurityAlert } from '../utils/toast';
export async function upsertApplicationAction({ request }) {
const formData = await request.formData();
const resubmission = JSON.parse(formData.get('resubmission'));
formData.delete('resubmission');
try {
let res;
if (resubmission) {
res = await fetch(`${import.meta.env.VITE_APP_API_URL}/applicant/resubmit-application`, {
method: 'PUT',
credentials: 'include',
body: formData
});
} else {
res = await fetch(`${import.meta.env.VITE_APP_API_URL}/applicant/create-application`, {
method: 'POST',
credentials: 'include',
body: formData
});
}
if (res.status === 401) {
return json({ message: 'Unauthorized access' }, { status: res.status });
}
if (!res.ok) {
const errorData = await res.text();
// Check for field tampering attempt
if (errorData.includes("Forbidden: Field") && errorData.includes("Tampering detected")) {
toastSecurityAlert("SECURITY ALERT: Your submission was blocked because form tampering was detected. Disabled fields cannot be modified. This incident has been logged.");
} else {
toastError(errorData);
}
return json({ message: errorData }, { status: res.status });
}
toastSuccess("Application Submitted Successfully");
return redirect("../dashboard/pending");
} catch (error) {
console.error('Fetch error:', error);
return json({ message: error.message || 'An unexpected error occurred' }, { status: error.status || 500 });
}
}

View File

@@ -0,0 +1,58 @@
import axios from "axios";
import { json, redirect } from "react-router-dom";
import { toastError } from "../utils/toast";
async function userDataLoader({ params, request }) {
try {
const res = await axios.get(`${import.meta.env.VITE_APP_API_URL}/general/dataRoot`, {
withCredentials: true,
});
if (res.status === 401 || res.status === 403) {
toastError("Unauthorized Access. Please Login.");
return redirect("/"); // Redirect to login page
}
const userRole = res.data.role;
const url = new URL(request.url);
const userRoleInURL = url.pathname.split("/")[1];
// Role-based route protection
if (userRoleInURL === "applicant" && userRole !== "Applicant") {
toastError("Access Denied: Applicant Role Required.");
return redirect("/");
}
if (userRoleInURL === "validator" && userRole !== "Validator") {
toastError("Access Denied: Validator Role Required.");
return redirect("/");
}
return { data: res.data };
} catch (error) {
// Handle errors during the request
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
toastError(error.response?.data?.message || "Unauthorized Access.");
// Log out the user if unauthorized or forbidden
await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, {
method: 'GET',
credentials: 'include', // Include credentials (cookies) for logout
});
return redirect("/"); // Redirect to login page
}
// If the error isn't related to authorization, return a network error
throw json(
{
message: error.response?.data?.message || "Network error. Please try again later.",
status: error.response?.status || 500,
},
{ status: error.response?.status || 500 }
);
}
}
export default userDataLoader;

View File

View File

@@ -0,0 +1,46 @@
import { toast } from 'react-toastify';
// Default toast configuration
const defaultConfig = {
position: "top-center",
autoClose: 4000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true
};
// Pre-configured toast functions
export const toastSuccess = (message) => {
return toast.success(message, {
...defaultConfig,
autoClose: 3000
});
};
export const toastError = (message) => {
return toast.error(message, {
...defaultConfig,
autoClose: 5000
});
};
export const toastInfo = (message) => {
return toast.info(message, {
...defaultConfig
});
};
export const toastWarning = (message) => {
return toast.warning(message, {
...defaultConfig
});
};
export const toastSecurityAlert = (message) => {
return toast.error(message, {
...defaultConfig,
icon: "🔐",
autoClose: 7000
});
};

View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

18
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/applicant-login': process.env.VITE_APP_API_URL,
'/validator-login': process.env.VITE_APP_API_URL,
'/verify-applicant': process.env.VITE_APP_API_URL,
'/verify-validator': process.env.VITE_APP_API_URL,
'/submit': process.env.VITE_APP_API_URL,
},
host: true,
port: 5173,
},
})