Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b605279e6 |
35
backend/package-lock.json
generated
35
backend/package-lock.json
generated
@@ -15,7 +15,9 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jwa": "^2.0.1",
|
||||
"jws": "^4.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"prisma": "^5.20.0"
|
||||
@@ -196,7 +198,8 @@
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
@@ -414,6 +417,7 @@
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
@@ -789,12 +793,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
@@ -816,21 +820,23 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
@@ -1124,6 +1130,7 @@
|
||||
"integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.20.0"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jwa": "^2.0.1",
|
||||
"jws": "^4.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"prisma": "^5.20.0"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Generator to create Prisma Client
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "darwin-arm64", "linux-musl-arm64-openssl-3.0.x"]
|
||||
@@ -40,7 +39,7 @@ enum Designation {
|
||||
model User {
|
||||
profileId String @id @default(uuid())
|
||||
userName String
|
||||
email String @unique @db.Text
|
||||
email String @unique
|
||||
password String
|
||||
|
||||
institute Institute?
|
||||
@@ -54,9 +53,9 @@ model User {
|
||||
}
|
||||
|
||||
model Application {
|
||||
applicationId String @id @default(uuid())
|
||||
applicationId String @id @default(uuid())
|
||||
applicantId String
|
||||
applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId])
|
||||
applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId])
|
||||
institute Institute
|
||||
department String
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import prisma from "../src/config/prismaConfig.js";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// Common password for all users
|
||||
// Use a common password for all development users to make testing easier
|
||||
const commonPassword = "securePassword123";
|
||||
|
||||
// Applicant and Validator data
|
||||
// Secure the password with hashing even for seed data!
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(commonPassword, salt);
|
||||
|
||||
const institutes = [
|
||||
"KJSIDS",
|
||||
"SKSC",
|
||||
@@ -28,96 +34,116 @@ async function main() {
|
||||
"Library",
|
||||
];
|
||||
|
||||
// Create VC (single, no department or institute)
|
||||
console.log("Seeding VC...");
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
userName: "Validator_VC",
|
||||
email: "vc@example.com",
|
||||
password: commonPassword,
|
||||
console.log("Seeding started...");
|
||||
|
||||
// VC is usually global
|
||||
// We use 'upsert' here. It means "Update if exists, Insert if not".
|
||||
// This prevents errors if we run the seed script multiple times.
|
||||
await prisma.user.upsert({
|
||||
where: { email: "vc@somaiya.edu" },
|
||||
update: {},
|
||||
create: {
|
||||
userName: "Vice Chancellor",
|
||||
email: "vc@somaiya.edu",
|
||||
password: hashedPassword,
|
||||
institute: "KJSCE",
|
||||
department: "Management",
|
||||
designation: "VC",
|
||||
},
|
||||
});
|
||||
|
||||
for (const institute of institutes) {
|
||||
// Create HOI for each institute
|
||||
// 1. Create HOI for this institute
|
||||
console.log(`Seeding HOI for ${institute}...`);
|
||||
const hoiEmail = `hoi.${institute.toLowerCase()}@example.com`;
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
const hoiEmail = `hoi@${institute.toLowerCase()}.edu`;
|
||||
await prisma.user.upsert({
|
||||
where: { email: hoiEmail },
|
||||
update: {},
|
||||
create: {
|
||||
userName: `HOI_${institute}`,
|
||||
email: hoiEmail,
|
||||
password: commonPassword,
|
||||
password: hashedPassword,
|
||||
institute,
|
||||
department: "Administration",
|
||||
designation: "HOI",
|
||||
},
|
||||
});
|
||||
|
||||
// Create Accounts for each institute
|
||||
// 2. Create Accounts for this institute
|
||||
console.log(`Seeding Accounts for ${institute}...`);
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
const accountsEmail = `accounts.${institute.toLowerCase()}@example.com`;
|
||||
await prisma.user.upsert({
|
||||
where: { email: accountsEmail },
|
||||
update: {},
|
||||
create: {
|
||||
userName: `Validator_Accounts_${institute}`,
|
||||
email: `accounts.${institute.toLowerCase()}@example.com`,
|
||||
password: commonPassword,
|
||||
email: accountsEmail,
|
||||
password: hashedPassword,
|
||||
institute,
|
||||
designation: "ACCOUNTS",
|
||||
},
|
||||
});
|
||||
|
||||
for (const department of departments) {
|
||||
// Create HOD for each department of each institute
|
||||
// 3. Create HOD for each department of each institute
|
||||
console.log(`Seeding HOD for ${department} in ${institute}...`);
|
||||
const hodEmail = `hod.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`;
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
await prisma.user.upsert({
|
||||
where: { email: hodEmail },
|
||||
update: {},
|
||||
create: {
|
||||
userName: `HOD_${department}_${institute}`,
|
||||
email: hodEmail,
|
||||
password: commonPassword,
|
||||
password: hashedPassword,
|
||||
institute,
|
||||
department,
|
||||
designation: "HOD",
|
||||
},
|
||||
});
|
||||
|
||||
// Create Faculty for each department of each institute
|
||||
// 4. Create Faculty for each department of each institute
|
||||
console.log(`Seeding Faculty for ${department} in ${institute}...`);
|
||||
const facultyEmail = `faculty.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`;
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
await prisma.user.upsert({
|
||||
where: { email: facultyEmail },
|
||||
update: {},
|
||||
create: {
|
||||
userName: `Faculty_${department}_${institute}`,
|
||||
email: facultyEmail,
|
||||
password: commonPassword,
|
||||
password: hashedPassword,
|
||||
institute,
|
||||
department,
|
||||
designation: "FACULTY",
|
||||
},
|
||||
});
|
||||
// Create Student for each department of each institute
|
||||
|
||||
// 5. Create Student for each department of each institute
|
||||
console.log(`Seeding Student for ${department} in ${institute}...`);
|
||||
const studentEmail = `student.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`;
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
await prisma.user.upsert({
|
||||
where: { email: studentEmail },
|
||||
update: {},
|
||||
create: {
|
||||
userName: `Student_${department}_${institute}`,
|
||||
email: studentEmail,
|
||||
password: commonPassword,
|
||||
password: hashedPassword,
|
||||
institute,
|
||||
department,
|
||||
designation: "STUDENT",
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("Seeding completed!");
|
||||
}
|
||||
|
||||
console.log("Seeding completed successfully.");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}).finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -55,11 +55,16 @@ const createApplication = async (req, res) => {
|
||||
});
|
||||
|
||||
if (!intimationApplication) {
|
||||
return res.status(404).send({ message: "Intimation Application not found" });
|
||||
return res
|
||||
.status(404)
|
||||
.send({ message: "Intimation Application not found" });
|
||||
}
|
||||
|
||||
if ( intimationApplication["formName"] !== "Travel Intimation Form") {
|
||||
return res.status(400).send({ message: "Intimation Application ID is not of a Travel Intimation Form" });
|
||||
if (intimationApplication["formName"] !== "Travel Intimation Form") {
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Intimation Application ID is not of a Travel Intimation Form",
|
||||
});
|
||||
}
|
||||
|
||||
const validationFields = [
|
||||
@@ -111,7 +116,9 @@ const createApplication = async (req, res) => {
|
||||
},
|
||||
});
|
||||
if (!primarySupervisor) {
|
||||
return res.status(404).send({ message: "Faculty not found (Incorrect Primary Supervisor Email)" });
|
||||
return res.status(404).send({
|
||||
message: "Faculty not found (Incorrect Primary Supervisor Email)",
|
||||
});
|
||||
}
|
||||
anotherSupervisor = await prisma.user.findUnique({
|
||||
where: {
|
||||
@@ -161,7 +168,10 @@ const createApplication = async (req, res) => {
|
||||
if (applicant.designation === "STUDENT") {
|
||||
// Validate that primary supervisor email is provided for student applications
|
||||
if (!formData.primarySupervisorEmail) {
|
||||
return res.status(400).send({ message: "Primary supervisor email is required for student applications" });
|
||||
return res.status(400).send({
|
||||
message:
|
||||
"Primary supervisor email is required for student applications",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +225,7 @@ const createApplication = async (req, res) => {
|
||||
department,
|
||||
institute,
|
||||
totalExpense,
|
||||
formData,
|
||||
formData: JSON.stringify(formData),
|
||||
proofOfTravel: proofOfTravelBuffer,
|
||||
proofOfAccommodation: proofOfAccommodationBuffer,
|
||||
proofOfAttendance: proofOfAttendanceBuffer,
|
||||
@@ -312,7 +322,7 @@ const updateApplication = async (req, res) => {
|
||||
return res.status(404).send({ message: "Application not found" });
|
||||
}
|
||||
|
||||
const originalFormData = originalApplication.formData;
|
||||
const originalFormData = JSON.parse(originalApplication.formData);
|
||||
|
||||
// Verify that disabled fields haven't been changed
|
||||
// Only expenses can be edited, everything else should remain the same regardless of resubmission status
|
||||
@@ -400,7 +410,7 @@ const updateApplication = async (req, res) => {
|
||||
|
||||
const updatedData = {
|
||||
totalExpense,
|
||||
formData,
|
||||
formData: JSON.stringify(formData),
|
||||
proofOfTravel: proofOfTravelBuffer,
|
||||
proofOfAccommodation: proofOfAccommodationBuffer,
|
||||
proofOfAttendance: proofOfAttendanceBuffer,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import prisma from "../config/prismaConfig.js";
|
||||
import generateToken from "../services/generateToken.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const applicantLogin = async (req, res) => {
|
||||
try {
|
||||
@@ -8,7 +9,7 @@ const applicantLogin = async (req, res) => {
|
||||
// Check if the applicant profile exists
|
||||
const validProfile = await prisma.user.findUnique({
|
||||
where: {
|
||||
email
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,14 +20,51 @@ const applicantLogin = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the password is correct
|
||||
if (validProfile.password !== password) {
|
||||
let isPasswordCorrect = false;
|
||||
let needsMigration = false;
|
||||
|
||||
// Step 1: Try checking if it's a hashed password
|
||||
try {
|
||||
isPasswordCorrect = await bcrypt.compare(password, validProfile.password);
|
||||
} catch (err) {
|
||||
isPasswordCorrect = false;
|
||||
}
|
||||
|
||||
// Step 2: If the bcrypt check failed, maybe they have an old plain-text password?
|
||||
// This is a "Fallback" mechanism to prevent old users from getting locked out after we added hashing.
|
||||
if (!isPasswordCorrect) {
|
||||
if (validProfile.password === password) {
|
||||
isPasswordCorrect = true;
|
||||
needsMigration = true; // Flag them for migration to secure hash
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPasswordCorrect) {
|
||||
return res.status(404).json({
|
||||
message: "Wrong Password",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: If they were using a plain-text password, let's secure it now!
|
||||
// We automatically update their password to a hash so next time they log in safely.
|
||||
if (needsMigration) {
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
await prisma.user.update({
|
||||
where: { profileId: validProfile.profileId },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
console.log(
|
||||
`[Security] Automatically migrated password for user ${email}`
|
||||
);
|
||||
} catch (migrationError) {
|
||||
console.log("Could not update password", migrationError);
|
||||
// It's checked, so we just log the error and continue
|
||||
}
|
||||
}
|
||||
|
||||
// Create token object
|
||||
const tokenObject = {
|
||||
id: validProfile.profileId,
|
||||
@@ -41,7 +79,11 @@ const applicantLogin = async (req, res) => {
|
||||
|
||||
// Set the token as a cookie
|
||||
return res
|
||||
.cookie("access_token", token, { sameSite: 'None', secure: true, httpOnly: true })
|
||||
.cookie("access_token", token, {
|
||||
sameSite: "Lax",
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
})
|
||||
.status(200)
|
||||
.json({
|
||||
message: "Login Successful",
|
||||
@@ -62,7 +104,7 @@ const validatorLogin = async (req, res) => {
|
||||
// Check if the validator profile exists
|
||||
let validProfile = await prisma.user.findUnique({
|
||||
where: {
|
||||
email
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,14 +115,48 @@ const validatorLogin = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the password is correct
|
||||
if (validProfile.password !== password) {
|
||||
let isPasswordCorrect = false;
|
||||
let needsMigration = false;
|
||||
|
||||
// Step 1: Try checking if it's a hashed password
|
||||
try {
|
||||
isPasswordCorrect = await bcrypt.compare(password, validProfile.password);
|
||||
} catch (err) {
|
||||
isPasswordCorrect = false;
|
||||
}
|
||||
|
||||
// Step 2: If the bcrypt check failed, maybe they have an old plain-text password?
|
||||
// We check this so old users don't get locked out.
|
||||
if (!isPasswordCorrect) {
|
||||
if (validProfile.password === password) {
|
||||
isPasswordCorrect = true;
|
||||
needsMigration = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPasswordCorrect) {
|
||||
return res.status(404).json({
|
||||
message: "Wrong Password",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: If they had a plain-text password, let's secure it now!
|
||||
// We update it to a hash so next time it is safe.
|
||||
if (needsMigration) {
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(password, salt);
|
||||
await prisma.user.update({
|
||||
where: { profileId: validProfile.profileId },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
console.log(`Updated password for validator ${email}`);
|
||||
} catch (migrationError) {
|
||||
console.log("Could not update password", migrationError);
|
||||
}
|
||||
}
|
||||
|
||||
// Create token object
|
||||
const tokenObject = {
|
||||
id: validProfile.profileId,
|
||||
@@ -95,7 +171,11 @@ const validatorLogin = async (req, res) => {
|
||||
|
||||
// Set the token as a cookie
|
||||
return res
|
||||
.cookie("access_token", token, { sameSite: 'None', secure: true, httpOnly: true })
|
||||
.cookie("access_token", token, {
|
||||
sameSite: "Lax",
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
})
|
||||
.status(200)
|
||||
.json({
|
||||
message: "Login Successful",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
applicantDesignations,
|
||||
validatorDesignations,
|
||||
} from "../config/designations.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const dataRoot = async (req, res) => {
|
||||
try {
|
||||
@@ -98,7 +99,12 @@ const getApplicationsByStatus = async (req, res) => {
|
||||
}),
|
||||
...(status === "ACCEPTED" && {
|
||||
AND: [
|
||||
{ OR: [{ facultyValidation: "ACCEPTED" }, { facultyValidation: null }] },
|
||||
{
|
||||
OR: [
|
||||
{ facultyValidation: "ACCEPTED" },
|
||||
{ facultyValidation: null },
|
||||
],
|
||||
},
|
||||
{ OR: [{ hodValidation: "ACCEPTED" }, { hodValidation: null }] },
|
||||
{ OR: [{ hoiValidation: "ACCEPTED" }, { hoiValidation: null }] },
|
||||
{ OR: [{ vcValidation: "ACCEPTED" }, { vcValidation: null }] },
|
||||
@@ -185,15 +191,18 @@ const getApplicationsByStatus = async (req, res) => {
|
||||
}
|
||||
|
||||
// Format response with selected fields
|
||||
const responseApplications = applications.map((application) => ({
|
||||
applicationId: application.applicationId,
|
||||
applicantName: application.applicantName,
|
||||
formData: {
|
||||
eventName: application.formData.eventName,
|
||||
applicantDepartment: application.formData.applicantDepartment,
|
||||
},
|
||||
createdAt: application.createdAt,
|
||||
}));
|
||||
const responseApplications = applications.map((application) => {
|
||||
const parsedFormData = JSON.parse(application.formData);
|
||||
return {
|
||||
applicationId: application.applicationId,
|
||||
applicantName: application.applicantName,
|
||||
formData: {
|
||||
eventName: parsedFormData.eventName,
|
||||
applicantDepartment: parsedFormData.applicantDepartment,
|
||||
},
|
||||
createdAt: application.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: `${status} Applications Fetched Successfully`,
|
||||
@@ -308,9 +317,14 @@ const getApplicationData = async (req, res) => {
|
||||
}
|
||||
|
||||
// Respond with the full application data and current status
|
||||
const parsedApplicationFull = {
|
||||
...applicationFull,
|
||||
formData: JSON.parse(applicationFull.formData),
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Application data retrieved successfully",
|
||||
data: { ...applicationFull, currentStatus },
|
||||
data: { ...parsedApplicationFull, currentStatus },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error retrieving application data:", error);
|
||||
@@ -436,4 +450,62 @@ const getFile = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { getApplicationData, getFile, dataRoot, getApplicationsByStatus };
|
||||
export {
|
||||
getApplicationData,
|
||||
getFile,
|
||||
dataRoot,
|
||||
getApplicationsByStatus,
|
||||
changePassword,
|
||||
};
|
||||
|
||||
const changePassword = async (req, res) => {
|
||||
try {
|
||||
const user = req.user;
|
||||
const { oldPassword, newPassword } = req.body;
|
||||
|
||||
if (!user || !user.id) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
// Get the current user from DB to check password
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { profileId: user.id },
|
||||
});
|
||||
|
||||
if (!dbUser) {
|
||||
return res.status(404).json({ message: "User not found" });
|
||||
}
|
||||
|
||||
let isPasswordCorrect = false;
|
||||
|
||||
// 1. Try bcrypt
|
||||
try {
|
||||
isPasswordCorrect = await bcrypt.compare(oldPassword, dbUser.password);
|
||||
} catch (err) {
|
||||
isPasswordCorrect = false;
|
||||
}
|
||||
|
||||
// 2. Try plaintext (fallback)
|
||||
if (!isPasswordCorrect && dbUser.password === oldPassword) {
|
||||
isPasswordCorrect = true;
|
||||
}
|
||||
|
||||
if (!isPasswordCorrect) {
|
||||
return res.status(400).json({ message: "Incorrect old password" });
|
||||
}
|
||||
|
||||
// Hash the new password
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(newPassword, salt);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { profileId: user.id },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Password updated successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Internal Server Error" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import express from 'express';
|
||||
import { applicantLogin, logout, validatorLogin } from '../controllers/authControllers.js';
|
||||
import express from "express";
|
||||
import {
|
||||
applicantLogin,
|
||||
logout,
|
||||
validatorLogin,
|
||||
} from "../controllers/authControllers.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -8,4 +12,8 @@ router.post('/validator-login', validatorLogin);
|
||||
|
||||
router.get('/logout', logout)
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
res.send("Travel Policy Backend is Running!");
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,11 +1,19 @@
|
||||
import express from 'express';
|
||||
import { dataRoot, getApplicationData, getApplicationsByStatus, getFile } from '../controllers/generalControllers.js';
|
||||
import express from "express";
|
||||
import {
|
||||
dataRoot,
|
||||
getApplicationData,
|
||||
getApplicationsByStatus,
|
||||
getFile,
|
||||
changePassword,
|
||||
} from "../controllers/generalControllers.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/dataRoot", dataRoot );
|
||||
|
||||
router.get('/getApplications/:status', getApplicationsByStatus);
|
||||
router.post("/changePassword", changePassword);
|
||||
|
||||
router.get("/getApplications/:status", getApplicationsByStatus);
|
||||
|
||||
router.get("/getApplicationData/:applicationId", getApplicationData);
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import nodeMailer from 'nodemailer';
|
||||
|
||||
export default async function sendMail({ emailId, link, type, status, designation }) {
|
||||
if (!process.env.TravelPolicyEmail || !process.env.TravelPolicyEmailPass) {
|
||||
return;
|
||||
}
|
||||
export default async function sendMail({
|
||||
emailId,
|
||||
link,
|
||||
type,
|
||||
status,
|
||||
designation,
|
||||
}) {
|
||||
// Check if we have email password in env
|
||||
if (!process.env.TravelPolicyEmail || !process.env.TravelPolicyEmailPass) {
|
||||
console.log("No email password found in .env, so I can't send emails.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("parametrs", emailId, link, type, status, designation);
|
||||
|
||||
@@ -48,20 +56,20 @@ export default async function sendMail({ emailId, link, type, status, designatio
|
||||
<a href=${link}>View Application</a>
|
||||
<p>Thank you.</p>
|
||||
`,
|
||||
};
|
||||
break;
|
||||
case 'accounts':
|
||||
mailOptions = {
|
||||
from: process.env.TravelPolicyEmail,
|
||||
to: emailId,
|
||||
subject: 'Transfer money to the applicant',
|
||||
html: `
|
||||
<p>Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:</p>`
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid email type');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "accounts":
|
||||
mailOptions = {
|
||||
from: process.env.TravelPolicyEmail,
|
||||
to: emailId,
|
||||
subject: "Transfer money to the applicant",
|
||||
html: `
|
||||
<p>Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:</p>`,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid email type");
|
||||
}
|
||||
|
||||
try {
|
||||
await transporter.sendMail(mailOptions);
|
||||
|
||||
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
@@ -92,6 +92,7 @@
|
||||
"version": "7.24.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
@@ -405,6 +406,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
|
||||
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0"
|
||||
}
|
||||
@@ -1716,6 +1718,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2153,6 +2156,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"electron-to-chromium": "^1.5.4",
|
||||
@@ -2252,6 +2256,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
|
||||
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -2868,6 +2873,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -5138,6 +5144,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -5355,6 +5362,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -5376,6 +5384,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -5502,6 +5511,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -6169,6 +6179,7 @@
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
|
||||
"integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
@@ -6458,6 +6469,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6791,6 +6803,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import React, { Suspense } from "react";
|
||||
import { ThemeProvider } from "./context/ThemeContext";
|
||||
import "./App.css";
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
@@ -15,7 +16,10 @@ const Applications = React.lazy(() => import("./pages/Applications/Applications"
|
||||
const Report = React.lazy(() => import("./pages/Report/Report"));
|
||||
const LoginRoot = React.lazy(() => import("./components/LoginRoot/LoginRoot"));
|
||||
const ContactUs = React.lazy(() => import("./pages/ContactUs/ContactUs"));
|
||||
const ApplicationView = React.lazy(() => import("./pages/ApplicationView/ApplicationView"));
|
||||
const ApplicationView = React.lazy(() =>
|
||||
import("./pages/ApplicationView/ApplicationView")
|
||||
);
|
||||
const Settings = React.lazy(() => import("./pages/Settings/Settings"));
|
||||
|
||||
import userDataLoader from "./services/userDataLoader";
|
||||
import { upsertApplicationAction } from "./services/upsertApplicationAction";
|
||||
@@ -41,9 +45,14 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{ path: "dashboard", element: <Dashboard /> },
|
||||
{ path: "dashboard/:status", element: <Applications /> },
|
||||
{ path: "dashboard/:status/:applicationId", element: <ApplicationView />, action: upsertApplicationAction },
|
||||
{
|
||||
path: "dashboard/:status/:applicationId",
|
||||
element: <ApplicationView />,
|
||||
action: upsertApplicationAction,
|
||||
},
|
||||
{ path: "form", element: <Form />, action: upsertApplicationAction },
|
||||
{ path: "contact-us", element: <ContactUs /> },
|
||||
{ path: "settings", element: <Settings /> },
|
||||
{ path: "policy", element: <Policy /> },
|
||||
],
|
||||
},
|
||||
@@ -55,8 +64,13 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{ path: "dashboard", element: <Dashboard /> },
|
||||
{ path: "dashboard/:status", element: <Applications /> },
|
||||
{ path: "dashboard/:status/:applicationId", element: <ApplicationView />, action: applicationStatusAction },
|
||||
{
|
||||
path: "dashboard/:status/:applicationId",
|
||||
element: <ApplicationView />,
|
||||
action: applicationStatusAction,
|
||||
},
|
||||
{ path: "report", element: <Report /> },
|
||||
{ path: "settings", element: <Settings /> },
|
||||
{ path: "policy", element: <Policy /> },
|
||||
],
|
||||
},
|
||||
@@ -64,12 +78,12 @@ const router = createBrowserRouter([
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider>
|
||||
<ToastContainer position="top-center" />
|
||||
<Suspense fallback={<Loading/>}>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,17 @@ const Root = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<Navbar userData={user} role={role} setSidebarIsVisible={setSidebarIsVisible} sidebarIsVisible={sidebarIsVisible} />
|
||||
<div className= "flex h-full bg-gray-100 overflow-auto">
|
||||
{sidebarIsVisible && !(urlPath.split("/").at(-1).includes("dashboard")) && <Sidebar role={role} />}
|
||||
<Navbar
|
||||
userData={user}
|
||||
role={role}
|
||||
setSidebarIsVisible={setSidebarIsVisible}
|
||||
sidebarIsVisible={sidebarIsVisible}
|
||||
/>
|
||||
<div className="flex h-full bg-gray-100 dark:bg-google-dark overflow-auto transition-colors duration-200">
|
||||
{sidebarIsVisible &&
|
||||
!urlPath.split("/").at(-1).includes("dashboard") && (
|
||||
<Sidebar role={role} />
|
||||
)}
|
||||
<div className="w-full min-h-full h-screen overflow-y-scroll">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -1,258 +1,388 @@
|
||||
import React from "react";
|
||||
import { FaSignOutAlt } from "react-icons/fa";
|
||||
import { FaSignOutAlt, FaMoon, FaSun } from "react-icons/fa";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const handleLogout = async () => {
|
||||
let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
const handleLogout = async () => {
|
||||
// Step 1: Call the backend logout route
|
||||
// We need to do this to clear the httpOnly cookie from the browser
|
||||
try {
|
||||
let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, {
|
||||
method: "GET",
|
||||
credentials: "include", // Important: This ensures the cookie is sent/cleared
|
||||
});
|
||||
|
||||
return res;
|
||||
// Step 2: If successful, we can redirect or show a message
|
||||
if (res.ok) {
|
||||
toast.info("Logged out successfully");
|
||||
}
|
||||
return res;
|
||||
} catch (error) {
|
||||
// console.log("Logout failed", error);
|
||||
toast.error("Logout failed, please check your connection.");
|
||||
}
|
||||
};
|
||||
|
||||
const Sidebar = ({ role }) => (
|
||||
<div className="w-72 h-screen bg-white p-6 shadow-lg z-10 flex flex-col overflow-y-auto">
|
||||
<div className="mb-8 text-center border-b-2 border-gray-200 pb-6">
|
||||
<div className="bg-white shadow-lg rounded-lg px-4 py-4 border border-gray-300">
|
||||
<h2 className="text-xl font-semibold text-red-700 tracking-tight">
|
||||
{`${role} Portal`}
|
||||
</h2>
|
||||
<p className="text-gray-700 text-sm font-medium py-1">
|
||||
Travel Policy SVU
|
||||
</p>
|
||||
const Sidebar = ({ role }) => {
|
||||
// Using our custom ThemeContext to switch between Light and Dark mode
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="w-72 h-full md:h-screen fixed md:relative top-0 left-0 z-40 bg-white dark:bg-google-dark dark:text-google-text p-6 shadow-lg flex flex-col overflow-y-auto transition-colors duration-200">
|
||||
<div className="mb-8 text-center border-b-2 border-gray-200 pb-6">
|
||||
<div className="bg-white dark:bg-google-gray shadow-lg rounded-lg px-4 py-4 border border-gray-300 dark:border-gray-600 transition-colors duration-200">
|
||||
<h2 className="text-xl font-semibold text-red-700 dark:text-red-500 tracking-tight">
|
||||
{`${role} Portal`}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-google-text-secondary text-sm font-medium py-1">
|
||||
Travel Policy SVU
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-grow">
|
||||
<ul className="space-y-4 text-sm">
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard`}
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 13l4 4L10 13m5-5h6a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6m-4 6l4 4 4-4"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Dashboard
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="text-gray-700 dark:text-google-text border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<span className="flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded cursor-pointer">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Application Status
|
||||
</span>
|
||||
<ul className="pl-4 mt-2 border-l border-gray-200 ml-2">
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard/pending`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-600 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-black" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 3v18m9-9H3"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Pending
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard/accepted`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-600 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-black" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Accepted
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard/rejected`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-600 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-black" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Rejected
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{/*
|
||||
Conditional Rendering based on User Role.
|
||||
If role is "Applicant", show Applicant links.
|
||||
Otherwise (Validator), show Validator links.
|
||||
*/}
|
||||
{role === "Applicant" ? (
|
||||
<>
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to="/applicant/policy"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Policy
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/applicant/contact-us"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 10h5l2 6h6l2-6h5"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Contact Us
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to="/applicant/settings"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
></path>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
></path>
|
||||
</svg>
|
||||
Settings
|
||||
</NavLink>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
//role = "Validator"
|
||||
<>
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to="/validator/policy"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Policy
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to="/validator/report"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 10h5l2 6h6l2-6h5"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Report
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
|
||||
<NavLink
|
||||
to="/validator/settings"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
></path>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
></path>
|
||||
</svg>
|
||||
Settings
|
||||
</NavLink>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* Spacer to push logout to the bottom */}
|
||||
<div className="mt-14 space-y-4">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="flex items-center w-full text-gray-700 dark:text-google-text hover:bg-gray-100 dark:hover:bg-gray-700 px-4 py-2 rounded-md transition-all duration-200"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<>
|
||||
<FaMoon className="w-4 h-4 mr-2" />
|
||||
<span>Dark Mode</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaSun className="w-4 h-4 mr-2 text-yellow-400" />
|
||||
<span>Light Mode</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
onClick={handleLogout}
|
||||
className="flex items-center text-gray-700 dark:text-google-text hover:bg-red-700 hover:text-white px-4 py-2 rounded-md transition-all duration-200"
|
||||
>
|
||||
<FaSignOutAlt className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-grow">
|
||||
<ul className="space-y-4 text-sm">
|
||||
<li className="border-b border-gray-200 pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard`}
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 13l4 4L10 13m5-5h6a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6m-4 6l4 4 4-4"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Dashboard
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="text-gray-700 border-b border-gray-200 pb-2">
|
||||
<span className="flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded cursor-pointer">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Application Status
|
||||
</span>
|
||||
<ul className="pl-4 mt-2 border-l border-gray-200 ml-2">
|
||||
<li className="border-b border-gray-200 pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard/pending`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-black" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 3v18m9-9H3"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Pending
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="border-b border-gray-200 pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard/accepted`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-black" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Accepted
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="pb-2">
|
||||
<NavLink
|
||||
to={`/${role.toLowerCase()}/dashboard/rejected`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-black" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Rejected
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{role === "Applicant" ? (
|
||||
<>
|
||||
<li className="border-b border-gray-200 pb-2">
|
||||
<NavLink
|
||||
to="/applicant/policy"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Policy
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/applicant/contact-us"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 10h5l2 6h6l2-6h5"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Contact Us
|
||||
</NavLink>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
//role = "Validator"
|
||||
<>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/validator/report"
|
||||
className={({ isActive }) =>
|
||||
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
|
||||
isActive ? "font-extrabold" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3 10h5l2 6h6l2-6h5"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Report
|
||||
</NavLink>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* Spacer to push logout to the bottom */}
|
||||
<div className="mt-14">
|
||||
<Link
|
||||
to="/"
|
||||
onClick={handleLogout}
|
||||
className="flex items-center text-gray-700 hover:bg-red-700 hover:text-white px-4 py-2 rounded-md transition-all duration-200"
|
||||
>
|
||||
<FaSignOutAlt className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
@@ -40,7 +40,7 @@ const LoginRoot = () => {
|
||||
|
||||
<footer className="flex items-center justify-center h-6 w-full bg-gray-100 text-gray-800 fixed bottom-0 left-0 z-50">
|
||||
<div className="text-center text-sm">
|
||||
©2024 KJSCE, All Rights Reserved.
|
||||
©{new Date().getFullYear()} KJSCE, All Rights Reserved.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ const Modal = ({ onClose, children }) => {
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-white p-6 rounded-lg relative w-11/12 md:w-3/5 lg:w-2/5 max-h-[85%] h-min overflow-auto"
|
||||
className="bg-white dark:bg-[#303134] p-6 rounded-lg relative w-11/12 md:w-3/5 lg:w-2/5 max-h-[85%] h-min overflow-auto transition-colors duration-200"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-start p-2">
|
||||
<button
|
||||
type='button'
|
||||
className="absolute top-3 right-3 text-xl font-bold text-gray-700 hover:text-gray-900"
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-xl font-bold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
||||
onClick={onClose}
|
||||
>
|
||||
X
|
||||
|
||||
35
frontend/src/context/ThemeContext.jsx
Normal file
35
frontend/src/context/ThemeContext.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
const ThemeContext = createContext({
|
||||
theme: "light",
|
||||
toggleTheme: () => {},
|
||||
});
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
// Check local storage or default to light
|
||||
return localStorage.getItem("theme") || "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext);
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
console.log("Main.jsx is executing...");
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -2,99 +2,131 @@ 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">
|
||||
<div className="bg-white text-gray-800 transition-colors duration-200 min-h-screen">
|
||||
{/*
|
||||
Hero Section:
|
||||
Displays a welcoming background image with a call-to-action button.
|
||||
We use inline styles for the background image to easily swap it if needed.
|
||||
*/}
|
||||
<div
|
||||
className="relative h-[60vh] flex items-center justify-center bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?ixlib=rb-4.0.3&auto=format&fit=crop&w=1600&q=80')",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-red-900 via-red-800 to-red-900 opacity-80"></div>
|
||||
<div className="relative z-10 text-center px-6 animate-fade-in">
|
||||
<h1 className="text-4xl md:text-6xl font-extrabold mb-4 text-white drop-shadow-lg">
|
||||
Welcome to Our Travel Policy
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl">
|
||||
Structured, efficient, and research-focused travel planning.
|
||||
<p className="text-lg md:text-xl text-gray-100 max-w-2xl mx-auto font-medium">
|
||||
Structured, efficient, and research-focused travel planning for the
|
||||
modern academic world.
|
||||
</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">
|
||||
<a
|
||||
href="#approach"
|
||||
className="inline-block mt-8 px-8 py-3 bg-white text-red-700 font-bold rounded-full shadow-lg hover:bg-gray-100 hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
Learn More
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission & Vision */}
|
||||
<div className="flex flex-col md:flex-row items-center py-12 px-6 md:px-12 gap-12">
|
||||
{/*
|
||||
Our Approach Section:
|
||||
Explains the philosophy behind the travel policy.
|
||||
*/}
|
||||
<div
|
||||
id="approach"
|
||||
className="flex flex-col md:flex-row items-center py-16 px-6 md:px-12 gap-12 max-w-7xl mx-auto"
|
||||
>
|
||||
<div className="md:w-1/2">
|
||||
<h2 className="text-3xl font-bold mb-4">Our Approach</h2>
|
||||
<p className="text-lg leading-relaxed">
|
||||
<h2 className="text-3xl font-bold mb-6 text-red-700">Our Approach</h2>
|
||||
<p className="text-lg leading-relaxed text-gray-700">
|
||||
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.
|
||||
efficiency and alignment with academic and research objectives. We
|
||||
believe in transparency and speed, enabling you to focus on your
|
||||
work.
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-1/2">
|
||||
<img
|
||||
src="https://via.placeholder.com/600x400"
|
||||
src="https://images.unsplash.com/photo-1436491865332-7a61a109cc05?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80"
|
||||
alt="Travel Policy"
|
||||
className="rounded-lg shadow-lg transform transition duration-300 hover:scale-105"
|
||||
className="rounded-xl shadow-2xl transform transition duration-500 hover:scale-105 w-full object-cover h-64 md:h-80"
|
||||
/>
|
||||
</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 className="bg-gray-50 py-16 px-6 md:px-12 transition-colors duration-200">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12 text-gray-900">
|
||||
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-8 bg-white shadow-lg rounded-xl transition-all duration-300 hover:-translate-y-2 hover:shadow-xl border border-transparent"
|
||||
>
|
||||
<div className="text-5xl mb-6">{item.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-3 text-gray-900">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-center leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</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="py-16 px-6 md:px-12 max-w-7xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12 text-gray-900">
|
||||
Why Our Policy Stands Out
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
@@ -112,7 +144,8 @@ const About = () => {
|
||||
},
|
||||
{
|
||||
title: "Global Outlook",
|
||||
description: "Facilitates international collaborations and exchanges.",
|
||||
description:
|
||||
"Facilitates international collaborations and exchanges.",
|
||||
},
|
||||
{
|
||||
title: "Comprehensive",
|
||||
@@ -120,24 +153,25 @@ const About = () => {
|
||||
},
|
||||
{
|
||||
title: "Transparent",
|
||||
description: "Approval criteria and funding sources clearly defined.",
|
||||
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"
|
||||
className="flex flex-col items-start p-6 bg-white shadow-md rounded-lg transition transform hover:scale-105 border-l-4 border-red-600"
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-2">{item.title}</h3>
|
||||
<p className="text-gray-700">{item.description}</p>
|
||||
<h3 className="text-xl font-bold mb-2 text-gray-800">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-gray-600">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Back to Top */}
|
||||
<div className="text-center py-6">
|
||||
|
||||
</div>
|
||||
{/* Footer / Back to Top */}
|
||||
<div className="text-center py-8 border-t border-gray-200"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,9 +39,11 @@ function Input({
|
||||
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"
|
||||
className="space-y-4 bg-white dark:bg-google-gray p-6 rounded-lg shadow-md min-w-fit border-t-4 border-red-700 mb-4 transition-colors duration-200"
|
||||
>
|
||||
<h3 className="text-xl font-semibold mt-2 mb-4">{section.label}</h3>
|
||||
<h3 className="text-xl font-semibold mt-2 mb-4 dark:text-google-text">
|
||||
{section.label}
|
||||
</h3>
|
||||
<div
|
||||
className={`${
|
||||
section.label === "Expense Details"
|
||||
@@ -76,11 +78,11 @@ function Input({
|
||||
return (
|
||||
<div
|
||||
key={formFeild.name}
|
||||
className="space-y-1 bg-slate-50 p-3 rounded-md"
|
||||
className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
|
||||
>
|
||||
<label
|
||||
htmlFor={formFeild.name}
|
||||
className="block font-medium"
|
||||
className="block font-medium dark:text-google-text"
|
||||
>
|
||||
{formFeild.label}
|
||||
</label>
|
||||
@@ -90,7 +92,7 @@ function Input({
|
||||
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"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white"
|
||||
disabled={formFeild?.disabled}
|
||||
>
|
||||
<option value="" label="Select option" />
|
||||
@@ -119,11 +121,11 @@ function Input({
|
||||
return (
|
||||
<div
|
||||
key={formFeild.name}
|
||||
className="space-y-1 bg-slate-50 p-3 rounded-md"
|
||||
className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
|
||||
>
|
||||
<label
|
||||
htmlFor={formFeild.name}
|
||||
className="inline-flex items-center space-x-2"
|
||||
className="inline-flex items-center space-x-2 dark:text-google-text"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -149,11 +151,11 @@ function Input({
|
||||
return (
|
||||
<div
|
||||
key={formFeild.name}
|
||||
className="space-y-1 bg-slate-50 p-3 rounded-md"
|
||||
className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
|
||||
>
|
||||
<label
|
||||
htmlFor={formFeild.name}
|
||||
className="block font-medium"
|
||||
className="block font-medium dark:text-google-text"
|
||||
>
|
||||
{formFeild.label}
|
||||
</label>
|
||||
@@ -163,7 +165,7 @@ function Input({
|
||||
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"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md max-h-32 min-h-20 focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white"
|
||||
disabled={formFeild?.disabled}
|
||||
/>
|
||||
<p className="text-red-500 text-sm">
|
||||
@@ -178,11 +180,11 @@ function Input({
|
||||
return (
|
||||
<div
|
||||
key={formFeild.name}
|
||||
className="space-y-1 bg-slate-50 p-3 rounded-md"
|
||||
className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
|
||||
>
|
||||
<label
|
||||
htmlFor={formFeild.name}
|
||||
className="block font-medium"
|
||||
className="block font-medium dark:text-google-text"
|
||||
>
|
||||
{formFeild.label}
|
||||
</label>
|
||||
@@ -206,7 +208,7 @@ function Input({
|
||||
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"
|
||||
className="w-full bg-white dark:bg-[#303134] dark:text-white px-3 py-2 border border-gray-300 dark:border-gray-600 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] &&
|
||||
@@ -222,7 +224,7 @@ function Input({
|
||||
return (
|
||||
<div
|
||||
key={formFeild.name}
|
||||
className="space-y-4 bg-slate-50 p-6 rounded-md w-full"
|
||||
className="space-y-4 bg-slate-50 dark:bg-[#3c4043] p-6 rounded-md w-full transition-colors duration-200"
|
||||
>
|
||||
{pdfIsVisible && (
|
||||
<PdfViewer
|
||||
@@ -235,7 +237,7 @@ function Input({
|
||||
<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"
|
||||
className="block text-lg font-medium text-gray-800 dark:text-google-text mb-3 sm:mb-0 sm:w-1/2"
|
||||
>
|
||||
{`${formFeild.label}: ₹${values[formFeild.name]
|
||||
?.reduce(
|
||||
@@ -341,11 +343,11 @@ function Input({
|
||||
return (
|
||||
<div
|
||||
key={formFeild.name}
|
||||
className="space-y-1 bg-slate-50 p-3 rounded-md"
|
||||
className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
|
||||
>
|
||||
<label
|
||||
htmlFor={formFeild.name}
|
||||
className="block font-medium"
|
||||
className="block font-medium dark:text-google-text"
|
||||
>
|
||||
{formFeild.label}
|
||||
</label>
|
||||
@@ -356,7 +358,7 @@ function Input({
|
||||
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"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white"
|
||||
disabled={formFeild?.disabled}
|
||||
max={formFeild?.max}
|
||||
min={formFeild?.min}
|
||||
|
||||
@@ -14,11 +14,11 @@ function AcceptChoice({
|
||||
|
||||
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">
|
||||
<div className="bg-white dark:bg-google-gray rounded-lg p-6 shadow-lg mx-auto transition-colors duration-200">
|
||||
<h2 className="text-2xl font-semibold text-red-700 dark:text-red-500 mb-4">
|
||||
Confirm Application Approval
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||
{(() => {
|
||||
switch (designation) {
|
||||
case "FACULTY":
|
||||
|
||||
@@ -128,7 +128,7 @@ function ApplicationView() {
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy application ID: ", err);
|
||||
alert("Failed to copy application ID. Please try again.");
|
||||
});
|
||||
@@ -138,10 +138,12 @@ function ApplicationView() {
|
||||
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="min-w-min bg-white dark:bg-google-gray shadow rounded-lg p-2 sm:p-4 md:p-6 m-4 transition-colors duration-200">
|
||||
<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>
|
||||
<h1 className="text-3xl font-extrabold text-gray-800 dark:text-google-text">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
{isTravelIntimationForm && (
|
||||
<button
|
||||
|
||||
@@ -17,15 +17,18 @@ function RejectionFeedback({ onClose, onSubmit }) {
|
||||
|
||||
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.
|
||||
<div className="bg-white dark:bg-google-gray rounded-lg p-1 shadow-lg transition-colors duration-200">
|
||||
<h2 className="text-2xl font-semibold text-red-700 dark:text-red-500 mb-4">
|
||||
Confirm Application Rejection
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 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"
|
||||
className="w-full p-4 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-700 dark:bg-[#303134] dark:text-white transition-colors duration-200"
|
||||
placeholder="Enter the reason for rejection"
|
||||
rows="4"
|
||||
value={reason}
|
||||
|
||||
@@ -37,15 +37,13 @@ function ValidationStatus({ validations, rejectionFeedback }) {
|
||||
role.status
|
||||
)}`}
|
||||
></div>
|
||||
<p>{role.name}</p>
|
||||
<p className="dark:text-google-text">{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="mt-4 p-4 bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-400 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>
|
||||
|
||||
@@ -74,9 +74,25 @@ const Applications = () => {
|
||||
applications={applications}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-600">
|
||||
No {status.toLowerCase()} applications found.
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center py-10 opacity-70">
|
||||
<svg
|
||||
className="w-24 h-24 text-gray-400 dark:text-gray-500 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
></path>
|
||||
</svg>
|
||||
<p className="text-xl font-medium text-gray-600 dark:text-google-text-secondary">
|
||||
No {status.toLowerCase()} applications found.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
@@ -91,7 +107,7 @@ const Applications = () => {
|
||||
|
||||
return (
|
||||
<main className="flex flex-col p-6">
|
||||
<div className="min-w-min bg-white shadow rounded-lg p-6 mb-20">
|
||||
<div className="min-w-min bg-white dark:bg-google-gray shadow rounded-lg p-6 mb-20 transition-colors duration-200">
|
||||
<ApplicationsStatusDescription />
|
||||
|
||||
{role === "Validator" && (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const ApplicationTable = ({ title, applications,}) => {
|
||||
|
||||
const ApplicationTable = ({ title, applications }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@@ -11,11 +10,21 @@ const ApplicationTable = ({ title, applications,}) => {
|
||||
<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>
|
||||
<th className="border-b p-4 text-gray-700 dark:text-google-text">
|
||||
Topic
|
||||
</th>
|
||||
<th className="border-b p-4 text-gray-700 dark:text-google-text">
|
||||
Name
|
||||
</th>
|
||||
<th className="border-b p-4 text-gray-700 dark:text-google-text">
|
||||
Submitted
|
||||
</th>
|
||||
<th className="border-b p-4 text-gray-700 dark:text-google-text">
|
||||
Branch
|
||||
</th>
|
||||
<th className="border-b p-4 text-gray-700 dark:text-google-text">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -28,14 +37,16 @@ const ApplicationTable = ({ title, applications,}) => {
|
||||
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' }}
|
||||
className="odd:bg-gray-50 even:bg-white dark:odd:bg-[#303134] dark:even:bg-[#202124] hover:bg-gray-200 dark:hover:bg-[#3c4043] cursor-pointer text-gray-900 dark:text-google-text transition-colors duration-200"
|
||||
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>
|
||||
<td className="p-4 text-green-500 dark:text-green-400">
|
||||
{title.split(" ")[0]}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -9,16 +9,16 @@ function ApplicationsStatusDescription() {
|
||||
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">
|
||||
<div className="bg-slate-50 dark:bg-[#3c4043] shadow-md rounded-lg p-6 mb-8 border border-slate-400 dark:border-gray-600 transition-colors duration-200">
|
||||
<div className="relative flex flex-col md:flex-row items-center justify-center mb-6 gap-5">
|
||||
<h1 className="text-3xl font-semibold text-gray-800 dark:text-google-text text-center">
|
||||
{`${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"
|
||||
className="md:absolute md:right-0 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"
|
||||
@@ -38,7 +38,7 @@ function ApplicationsStatusDescription() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600 text-lg leading-relaxed sm:block hidden">
|
||||
<p className="text-gray-600 dark:text-google-text-secondary text-lg leading-relaxed sm:block hidden">
|
||||
Easily track the details and statuses of all your submitted applications
|
||||
in one place.
|
||||
<br />
|
||||
|
||||
@@ -13,7 +13,7 @@ function ContactUs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full bg-white text-gray-900">
|
||||
<div className="h-full bg-white dark:bg-google-dark text-gray-900 dark:text-google-text transition-colors duration-200">
|
||||
{/* 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>
|
||||
@@ -25,44 +25,67 @@ function ContactUs() {
|
||||
</section>
|
||||
|
||||
{/* Contact Form */}
|
||||
<section id="contact-form" className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100">
|
||||
<section
|
||||
id="contact-form"
|
||||
className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100 dark:bg-google-gray transition-colors duration-200"
|
||||
>
|
||||
<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>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-google-text"
|
||||
>
|
||||
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"
|
||||
className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-google-text"
|
||||
>
|
||||
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"
|
||||
className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</label>
|
||||
<label
|
||||
htmlFor="subject"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-google-text"
|
||||
>
|
||||
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"
|
||||
className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</label>
|
||||
<label
|
||||
htmlFor="message"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-google-text"
|
||||
>
|
||||
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"
|
||||
className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -82,7 +105,7 @@ function ContactUs() {
|
||||
</section>
|
||||
|
||||
{/* Location Map */}
|
||||
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white dark:bg-google-dark transition-colors duration-200">
|
||||
<div className="max-w-screen-xl mx-auto">
|
||||
<h2 className="text-2xl font-semibold text-center mb-6">Our Location</h2>
|
||||
<div className="relative">
|
||||
@@ -95,17 +118,20 @@ function ContactUs() {
|
||||
</section>
|
||||
|
||||
{/* Contact Details */}
|
||||
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100">
|
||||
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100 dark:bg-google-gray transition-colors duration-200">
|
||||
<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
|
||||
<h3 className="text-xl font-semibold text-gray-800 dark:text-google-text">
|
||||
Contact Details
|
||||
</h3>
|
||||
<p className="mt-2 text-gray-600 dark:text-google-text-secondary">
|
||||
<span className="font-medium">Address:</span> K. J. Somaiya
|
||||
College of Engineering, Vidya Nagar, Mumbai, India
|
||||
</p>
|
||||
<p className="mt-2 text-gray-600">
|
||||
<p className="mt-2 text-gray-600 dark:text-google-text-secondary">
|
||||
<span className="font-medium">Phone:</span> (022) 6728 8000
|
||||
</p>
|
||||
<p className="mt-2 text-gray-600">
|
||||
<p className="mt-2 text-gray-600 dark:text-google-text-secondary">
|
||||
<span className="font-medium">Email:</span> info@somaiya.edu
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ function Dashboard() {
|
||||
const greetingLine2 = `${designation} in ${department} Department, ${institute}`;
|
||||
|
||||
return (
|
||||
<div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" >
|
||||
<div className="font-sans bg-white dark:bg-google-dark overflow-y-scroll scroll-smooth snap-y h-screen transition-colors duration-200">
|
||||
{/* Hero Section */}
|
||||
<section
|
||||
className="relative w-full h-screen flex items-center justify-center text-white overflow-hidden bg-cover bg-center snap-start"
|
||||
@@ -68,19 +68,21 @@ function Dashboard() {
|
||||
{/* 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"
|
||||
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 dark:from-google-dark dark:via-[#25262a] dark:to-[#1e1f21] min-h-screen snap-start transition-colors duration-200"
|
||||
>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-semibold text-center text-red-700 mb-10 sm:mb-12">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-semibold text-center text-red-700 dark:text-red-500 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">
|
||||
<div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
|
||||
<div className="text-5xl mb-4 text-red-700 dark:text-red-500">
|
||||
🔍
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
|
||||
View Your Applications
|
||||
</h3>
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
|
||||
{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."}
|
||||
@@ -97,12 +99,14 @@ function Dashboard() {
|
||||
|
||||
{/* 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">
|
||||
<div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
|
||||
<div className="text-5xl mb-4 text-red-700 dark:text-red-500">
|
||||
📊
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
|
||||
View Insights
|
||||
</h3>
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
|
||||
Analyze and gain insights about funding and related data.
|
||||
</p>
|
||||
<button
|
||||
@@ -116,12 +120,14 @@ function Dashboard() {
|
||||
|
||||
{/* 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">
|
||||
<div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
|
||||
<div className="text-5xl mb-4 text-red-700 dark:text-red-500">
|
||||
📝
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
|
||||
Create New Application
|
||||
</h3>
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
|
||||
Start your application process to apply for funding and
|
||||
financial assistance.
|
||||
</p>
|
||||
@@ -135,12 +141,14 @@ function Dashboard() {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
|
||||
<div className="text-5xl mb-4 text-red-700 dark:text-red-500">
|
||||
📚
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
|
||||
Understand the Policy
|
||||
</h3>
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700">
|
||||
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
|
||||
Learn about the eligibility, funding process, and guidelines for
|
||||
financial assistance.
|
||||
</p>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react'
|
||||
|
||||
function Policy() {
|
||||
const Policy = () => {
|
||||
return (
|
||||
<div>
|
||||
Policy
|
||||
<div className="flex flex-col h-screen w-full bg-gray-100 dark:bg-google-dark">
|
||||
<div className="flex-grow w-full h-full">
|
||||
<iframe
|
||||
src="https://svu-iqac.somaiya.edu/University+Policies/12.Travel+Policy.pdf"
|
||||
className="w-full h-full border-none"
|
||||
title="Travel Policy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Policy
|
||||
export default Policy;
|
||||
|
||||
@@ -11,7 +11,7 @@ function Report() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<main className="flex flex-col p-6">
|
||||
<div className="bg-white shadow rounded-lg p-6 w-full">
|
||||
<div className="bg-white dark:bg-google-gray shadow rounded-lg p-6 w-full transition-colors duration-200">
|
||||
<FilterDataForm setReportData={setReportData} setLoading={setLoading} />
|
||||
{loading ? <Loading /> : <Charts reportData={reportData} />}
|
||||
</div>
|
||||
|
||||
@@ -3,29 +3,42 @@ 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>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse shadow-sm rounded-lg overflow-hidden bg-white dark:bg-[#3c4043] transition-colors duration-200">
|
||||
<thead className="bg-gray-100 dark:bg-[#303134]">
|
||||
<tr className="text-left">
|
||||
<th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
|
||||
ID
|
||||
</th>
|
||||
<th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
|
||||
Stream
|
||||
</th>
|
||||
<th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
|
||||
Scholarship
|
||||
</th>
|
||||
<th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
|
||||
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
|
||||
key={row.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
|
||||
{row.id}
|
||||
</td>
|
||||
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
|
||||
{row.Stream}
|
||||
</td>
|
||||
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
|
||||
{row.Scholarship}
|
||||
</td>
|
||||
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
|
||||
{row.Funds}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from "chart.js";
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||
import Table from "./Table";
|
||||
import { PDFDownloadLink, PDFViewer } from "@react-pdf/renderer";
|
||||
import ApprovalVsRejectionTrends from "./map";
|
||||
@@ -46,19 +46,24 @@ function Charts({ reportData }) {
|
||||
);
|
||||
}
|
||||
|
||||
const { acceptedApplications, rejectedApplications, pendingApplications } = data;
|
||||
const { acceptedApplications, rejectedApplications, pendingApplications } =
|
||||
data;
|
||||
|
||||
const tableData = [];
|
||||
const groupedData = {};
|
||||
|
||||
// --- Data Processing for Table --- (Simple aggregation logic)
|
||||
if (acceptedApplications) {
|
||||
for (const item of acceptedApplications) {
|
||||
const { institute, department, formData } = item;
|
||||
const { totalExpense } = formData;
|
||||
|
||||
// Initialize institute object if not exists
|
||||
if (!groupedData[institute]) {
|
||||
groupedData[institute] = {};
|
||||
}
|
||||
|
||||
// If filtering by specific institute, we group by Department (e.g. Computer, IT, Mech)
|
||||
if (query.institute) {
|
||||
if (!groupedData[institute][department]) {
|
||||
groupedData[institute][department] = {
|
||||
@@ -67,11 +72,12 @@ function Charts({ reportData }) {
|
||||
};
|
||||
}
|
||||
|
||||
// Aggregate the data
|
||||
// Add expense and increment count
|
||||
groupedData[institute][department].totalExpense +=
|
||||
parseFloat(totalExpense); // Summing the expenses
|
||||
parseFloat(totalExpense);
|
||||
groupedData[institute][department].applications += 1;
|
||||
} else {
|
||||
// If viewing all institutes, we group by Institute (e.g. KJSCE, KJSIM)
|
||||
if (!groupedData[institute].applications) {
|
||||
groupedData[institute] = {
|
||||
totalExpense: 0,
|
||||
@@ -79,38 +85,40 @@ function Charts({ reportData }) {
|
||||
};
|
||||
}
|
||||
|
||||
// Aggregate the data
|
||||
groupedData[institute].totalExpense += parseFloat(totalExpense); // Summing the expenses
|
||||
// Add expense and increment count
|
||||
groupedData[institute].totalExpense += parseFloat(totalExpense);
|
||||
groupedData[institute].applications += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Transform grouped data into desired table format
|
||||
// --- Transform Groups to Array for Display ---
|
||||
if (query.institute) {
|
||||
// Loop through departments for the selected 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
|
||||
Stream: department, // 'Stream' here refers to the Department name
|
||||
Scholarship: departmentData.applications, // Number of applications
|
||||
Purpose_of_Travel: departmentData.purposeOfTravel, // (Placeholder)
|
||||
Funds: departmentData.totalExpense.toFixed(2), // Total money spent
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Loop through all institutes
|
||||
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
|
||||
Stream: institute, // 'Stream' here is the Institute name
|
||||
Scholarship: instituteData.applications, // Number of applications
|
||||
Purpose_of_Travel: instituteData.purposeOfTravel,
|
||||
Funds: instituteData.totalExpense.toFixed(2), // Formatting funds to 2 decimal places
|
||||
Funds: instituteData.totalExpense.toFixed(2), // Total money spent
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -122,6 +130,8 @@ function Charts({ reportData }) {
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
// --- Chart Configuration (Preserved for future use) ---
|
||||
|
||||
// Line Chart Data and Options
|
||||
const lineOptions = {
|
||||
responsive: true,
|
||||
@@ -281,80 +291,86 @@ function Charts({ reportData }) {
|
||||
],
|
||||
};
|
||||
|
||||
// const barChartRef = useRef();
|
||||
// const pieChartRef1 = useRef();
|
||||
// const pieChartRef2 = useRef();
|
||||
const barChartRef = useRef();
|
||||
const pieChartRef1 = useRef();
|
||||
const pieChartRef2 = useRef();
|
||||
|
||||
// const loadChartsInPdf = () => {
|
||||
// const barChartInstance = barChartRef.current;
|
||||
// const pieChartInstance1 = pieChartRef1.current;
|
||||
// const pieChartInstance2 = pieChartRef2.current;
|
||||
// Note: Chart generation logic for PDF is currently disabled as we focus on the Table view.
|
||||
// Uncomment this when we are ready to integrate charts into the PDF report.
|
||||
/*
|
||||
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 (barChartInstance) {
|
||||
const barBase64Image = barChartInstance.toBase64Image();
|
||||
setChartImages((prevImages) => ({
|
||||
...prevImages,
|
||||
barChart: barBase64Image,
|
||||
}));
|
||||
}
|
||||
|
||||
// if (pieChartInstance1) {
|
||||
// const pieBase64Image = pieChartInstance1.toBase64Image();
|
||||
// setChartImages((prevImages) => ({
|
||||
// ...prevImages,
|
||||
// pieChart1: pieBase64Image,
|
||||
// }));
|
||||
// }
|
||||
if (pieChartInstance1) {
|
||||
const pieBase64Image = pieChartInstance1.toBase64Image();
|
||||
setChartImages((prevImages) => ({
|
||||
...prevImages,
|
||||
pieChart1: pieBase64Image,
|
||||
}));
|
||||
}
|
||||
|
||||
// if (pieChartInstance2) {
|
||||
// const pieBase64Image = pieChartInstance2.toBase64Image();
|
||||
// setChartImages((prevImages) => ({
|
||||
// ...prevImages,
|
||||
// pieChart2: pieBase64Image,
|
||||
// }));
|
||||
// }
|
||||
// };
|
||||
if (pieChartInstance2) {
|
||||
const pieBase64Image = pieChartInstance2.toBase64Image();
|
||||
setChartImages((prevImages) => ({
|
||||
...prevImages,
|
||||
pieChart2: pieBase64Image,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// setChartImages((prevImages) => ({ ...prevImages, isLoading: true }));
|
||||
useEffect(() => {
|
||||
setChartImages((prevImages) => ({ ...prevImages, isLoading: true }));
|
||||
|
||||
// const handleRender = () => {
|
||||
// loadChartsInPdf();
|
||||
// setChartImages((prevImages) => ({ ...prevImages, isLoading: false }));
|
||||
// };
|
||||
const handleRender = () => {
|
||||
loadChartsInPdf();
|
||||
setChartImages((prevImages) => ({ ...prevImages, isLoading: false }));
|
||||
};
|
||||
|
||||
// const barChartInstance = barChartRef.current;
|
||||
// const pieChartInstance1 = pieChartRef1.current;
|
||||
// const pieChartInstance2 = pieChartRef2.current;
|
||||
const barChartInstance = barChartRef.current;
|
||||
const pieChartInstance1 = pieChartRef1.current;
|
||||
const pieChartInstance2 = pieChartRef2.current;
|
||||
|
||||
// if (barChartInstance) {
|
||||
// barChartInstance.options.animation.onComplete = handleRender;
|
||||
// }
|
||||
if (barChartInstance) {
|
||||
barChartInstance.options.animation.onComplete = handleRender;
|
||||
}
|
||||
|
||||
// if (pieChartInstance1) {
|
||||
// pieChartInstance1.options.animation.onComplete = handleRender;
|
||||
// }
|
||||
if (pieChartInstance1) {
|
||||
pieChartInstance1.options.animation.onComplete = handleRender;
|
||||
}
|
||||
|
||||
// if (pieChartInstance2) {
|
||||
// pieChartInstance2.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 () => {
|
||||
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>
|
||||
<h1 className="text-3xl mb-6 text-gray-800 dark:text-google-text">
|
||||
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">
|
||||
@@ -387,9 +403,11 @@ function Charts({ reportData }) {
|
||||
|
||||
<div className="flex flex-col gap-10 items-center justify-center my-10">
|
||||
<div className="w-full">
|
||||
{/* Reuse the Table component to show aggregated data */}
|
||||
<Table tableData={tableData} />
|
||||
</div>
|
||||
{/*
|
||||
|
||||
{/*
|
||||
<div>
|
||||
<Pie options={pie_Options} data={pie_Data} ref={pieChartRef2} />
|
||||
</div> */}
|
||||
@@ -428,9 +446,11 @@ function Charts({ reportData }) {
|
||||
}
|
||||
</PDFDownloadLink>
|
||||
|
||||
<PDFViewer style={{ width: "70vw", height: "100vh" }}>
|
||||
<ReportPDF tableData={tableData} chartImages={chartImages} />
|
||||
</PDFViewer>
|
||||
<div className="mt-8 hidden md:block">
|
||||
<PDFViewer style={{ width: "70vw", height: "100vh" }}>
|
||||
<ReportPDF tableData={tableData} chartImages={chartImages} />
|
||||
</PDFViewer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
122
frontend/src/pages/Settings/Settings.jsx
Normal file
122
frontend/src/pages/Settings/Settings.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import axios from "axios";
|
||||
|
||||
// Settings Change Password Component
|
||||
const Settings = () => {
|
||||
const [passwords, setPasswords] = useState({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
// Handle input changes
|
||||
const handleChange = (e) => {
|
||||
setPasswords({ ...passwords, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
// Submit the form to the backend
|
||||
const handleSubmit = async (e) => {
|
||||
// Step 1: Prevent the page from reloading
|
||||
e.preventDefault();
|
||||
|
||||
// Step 2: Check if the new password and confirm password are the same
|
||||
if (passwords.newPassword !== passwords.confirmPassword) {
|
||||
toast.error("New passwords do not match!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 3: Send the old and new password to the server
|
||||
const res = await axios.post(
|
||||
`${import.meta.env.VITE_APP_API_URL}/general/changePassword`,
|
||||
{
|
||||
oldPassword: passwords.oldPassword,
|
||||
newPassword: passwords.newPassword,
|
||||
},
|
||||
{ withCredentials: true }
|
||||
);
|
||||
|
||||
// Step 4: If everything is good, show success message
|
||||
if (res.status === 200) {
|
||||
toast.success("Password updated successfully!");
|
||||
// Clear the form inputs
|
||||
setPasswords({ oldPassword: "", newPassword: "", confirmPassword: "" });
|
||||
}
|
||||
} catch (error) {
|
||||
// Step 5: If there is an error (like wrong old password), show it
|
||||
console.log(error);
|
||||
toast.error(error.response?.data?.message || "Failed to update password");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 bg-gray-50 dark:bg-google-dark min-h-screen transition-colors duration-200">
|
||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-google-text mb-6 border-b-2 border-red-700 pb-2 inline-block">
|
||||
Account Settings
|
||||
</h1>
|
||||
|
||||
<div className="bg-white dark:bg-google-gray p-8 rounded-xl shadow-lg border border-gray-100 dark:border-gray-700 max-w-lg mt-4 mx-auto transition-colors duration-200">
|
||||
<h2 className="text-2xl font-semibold mb-6 text-red-700 dark:text-red-500 flex items-center">
|
||||
Change Password
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
||||
<div>
|
||||
<label className="block text-gray-700 dark:text-google-text mb-2 font-medium">
|
||||
Current Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="oldPassword"
|
||||
value={passwords.oldPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter your current password"
|
||||
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all dark:bg-[#3c4043] dark:text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-700 dark:text-google-text mb-2 font-medium">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
value={passwords.newPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter new password"
|
||||
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all dark:bg-[#3c4043] dark:text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-700 dark:text-google-text mb-2 font-medium">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={passwords.confirmPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="Confirm new password"
|
||||
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all dark:bg-[#3c4043] dark:text-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-2 bg-red-700 text-white font-bold py-3 px-6 rounded-lg hover:bg-red-800 transition-colors shadow-md hover:shadow-lg transform active:scale-95 duration-200"
|
||||
>
|
||||
Update Password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,11 +1,16 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
"google-dark": "#202124",
|
||||
"google-gray": "#303134",
|
||||
"google-text": "#E8EAED",
|
||||
"google-text-secondary": "#9AA0A6",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Reference in New Issue
Block a user