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

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'