code base
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user