forked from CSI-KJSCE/Travel-policy-
code base
This commit is contained in:
145
frontend/src/pages/About/About.jsx
Normal file
145
frontend/src/pages/About/About.jsx
Normal 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;
|
||||
204
frontend/src/pages/ApplicationForm/Form.jsx
Normal file
204
frontend/src/pages/ApplicationForm/Form.jsx
Normal 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;
|
||||
1107
frontend/src/pages/ApplicationForm/FormFeilds.jsx
Normal file
1107
frontend/src/pages/ApplicationForm/FormFeilds.jsx
Normal file
File diff suppressed because it is too large
Load Diff
379
frontend/src/pages/ApplicationForm/Input.jsx
Normal file
379
frontend/src/pages/ApplicationForm/Input.jsx
Normal 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;
|
||||
243
frontend/src/pages/ApplicationForm/components/ExpenseForm.jsx
Normal file
243
frontend/src/pages/ApplicationForm/components/ExpenseForm.jsx
Normal 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;
|
||||
277
frontend/src/pages/ApplicationForm/components/ExpenseTable.jsx
Normal file
277
frontend/src/pages/ApplicationForm/components/ExpenseTable.jsx
Normal 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;
|
||||
127
frontend/src/pages/ApplicationView/AcceptChoice.jsx
Normal file
127
frontend/src/pages/ApplicationView/AcceptChoice.jsx
Normal 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;
|
||||
246
frontend/src/pages/ApplicationView/ApplicationView.jsx
Normal file
246
frontend/src/pages/ApplicationView/ApplicationView.jsx
Normal 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;
|
||||
81
frontend/src/pages/ApplicationView/PdfActions.jsx
Normal file
81
frontend/src/pages/ApplicationView/PdfActions.jsx
Normal 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;
|
||||
58
frontend/src/pages/ApplicationView/RejectionFeedback.jsx
Normal file
58
frontend/src/pages/ApplicationView/RejectionFeedback.jsx
Normal 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;
|
||||
60
frontend/src/pages/ApplicationView/ValidationStatus.jsx
Normal file
60
frontend/src/pages/ApplicationView/ValidationStatus.jsx
Normal 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;
|
||||
112
frontend/src/pages/Applications/Applications.jsx
Normal file
112
frontend/src/pages/Applications/Applications.jsx
Normal 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;
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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;
|
||||
59
frontend/src/pages/Applications/components/Search.jsx
Normal file
59
frontend/src/pages/Applications/components/Search.jsx
Normal 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;
|
||||
124
frontend/src/pages/ContactUs/ContactUs.jsx
Normal file
124
frontend/src/pages/ContactUs/ContactUs.jsx
Normal 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">We’re 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;
|
||||
167
frontend/src/pages/Dashboard/Dashboard.jsx
Normal file
167
frontend/src/pages/Dashboard/Dashboard.jsx
Normal 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"
|
||||
>
|
||||
↓ 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;
|
||||
33
frontend/src/pages/Login/Login.css
Normal file
33
frontend/src/pages/Login/Login.css
Normal 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;
|
||||
}
|
||||
37
frontend/src/pages/Login/Login.jsx
Normal file
37
frontend/src/pages/Login/Login.jsx
Normal 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;
|
||||
138
frontend/src/pages/Login/components/ApplicantLogin.jsx
Normal file
138
frontend/src/pages/Login/components/ApplicantLogin.jsx
Normal 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 Validator’s 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;
|
||||
143
frontend/src/pages/Login/components/LoginAnimation.css
Normal file
143
frontend/src/pages/Login/components/LoginAnimation.css
Normal 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;
|
||||
}
|
||||
143
frontend/src/pages/Login/components/ValidatorLogin.jsx
Normal file
143
frontend/src/pages/Login/components/ValidatorLogin.jsx
Normal 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 Applicant’s 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;
|
||||
0
frontend/src/pages/Policy/Policy.css
Normal file
0
frontend/src/pages/Policy/Policy.css
Normal file
11
frontend/src/pages/Policy/Policy.jsx
Normal file
11
frontend/src/pages/Policy/Policy.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
function Policy() {
|
||||
return (
|
||||
<div>
|
||||
Policy
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Policy
|
||||
30
frontend/src/pages/Report/Data.js
Normal file
30
frontend/src/pages/Report/Data.js
Normal 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],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
22
frontend/src/pages/Report/Report.jsx
Normal file
22
frontend/src/pages/Report/Report.jsx
Normal 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;
|
||||
152
frontend/src/pages/Report/components/FilterDataForm.jsx
Normal file
152
frontend/src/pages/Report/components/FilterDataForm.jsx
Normal 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;
|
||||
@@ -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 };
|
||||
90
frontend/src/pages/Report/components/OverTheYearsLine.jsx
Normal file
90
frontend/src/pages/Report/components/OverTheYearsLine.jsx
Normal 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;
|
||||
65
frontend/src/pages/Report/components/OverTheYearsPie.jsx
Normal file
65
frontend/src/pages/Report/components/OverTheYearsPie.jsx
Normal 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;
|
||||
166
frontend/src/pages/Report/components/ReportPDF.jsx
Normal file
166
frontend/src/pages/Report/components/ReportPDF.jsx
Normal 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;
|
||||
37
frontend/src/pages/Report/components/Table.jsx
Normal file
37
frontend/src/pages/Report/components/Table.jsx
Normal 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;
|
||||
146
frontend/src/pages/Report/components/approved.jsx
Normal file
146
frontend/src/pages/Report/components/approved.jsx
Normal 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;
|
||||
143
frontend/src/pages/Report/components/card.jsx
Normal file
143
frontend/src/pages/Report/components/card.jsx
Normal 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;
|
||||
63
frontend/src/pages/Report/components/cards.css
Normal file
63
frontend/src/pages/Report/components/cards.css
Normal 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;
|
||||
|
||||
}
|
||||
28
frontend/src/pages/Report/components/cards.jsx
Normal file
28
frontend/src/pages/Report/components/cards.jsx
Normal 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
|
||||
440
frontend/src/pages/Report/components/charts.jsx
Normal file
440
frontend/src/pages/Report/components/charts.jsx
Normal 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;
|
||||
92
frontend/src/pages/Report/components/map.jsx
Normal file
92
frontend/src/pages/Report/components/map.jsx
Normal 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;
|
||||
8
frontend/src/pages/index.js
Normal file
8
frontend/src/pages/index.js
Normal 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'
|
||||
Reference in New Issue
Block a user