├── .gitignore
├── .nvmrc
├── INSTALLATION.md
├── LICENSE
├── README.md
├── SECURITY.md
├── client
├── .env.example
├── _redirects
├── eslint.config.js
├── index.html
├── netlify.toml
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── cursor.png
│ ├── hrms.png
│ ├── images.png
│ ├── menu.svg
│ ├── metro.png
│ ├── robots.txt
│ ├── sitemap.xml
│ ├── unknown.jpeg
│ └── verify.avif
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── admin
│ │ ├── attendance
│ │ │ ├── Attendance.jsx
│ │ │ └── CheckAttendance.jsx
│ │ ├── complaint
│ │ │ └── Complaint.jsx
│ │ ├── dashboard
│ │ │ └── Dashboard.jsx
│ │ ├── department
│ │ │ └── Department.jsx
│ │ ├── employee
│ │ │ ├── CreateEmployee.jsx
│ │ │ ├── Employee.jsx
│ │ │ ├── UpdateEmployee.jsx
│ │ │ └── ViewEmployee.jsx
│ │ ├── feedback
│ │ │ └── Feedback.jsx
│ │ ├── leave
│ │ │ ├── EmployeeOnLeave.jsx
│ │ │ └── LeaveRequest.jsx
│ │ ├── mail
│ │ │ └── Communication.jsx
│ │ ├── payroll
│ │ │ └── Payroll.jsx
│ │ ├── performance
│ │ │ └── Performance.jsx
│ │ ├── recruitment
│ │ │ ├── JobApplications.jsx
│ │ │ ├── JobOpenings.jsx
│ │ │ └── PostJob.jsx
│ │ └── report
│ │ │ └── Report.jsx
│ ├── app
│ │ ├── admin.jsx
│ │ └── employee.jsx
│ ├── auth
│ │ ├── EmailConfirmation.jsx
│ │ ├── ForgetPassword.jsx
│ │ ├── InvalidResetLink.jsx
│ │ ├── Login.jsx
│ │ └── ResetPassword.jsx
│ ├── axios
│ │ └── axiosInstance.js
│ ├── careers
│ │ └── Career.jsx
│ ├── components
│ │ ├── shared
│ │ │ ├── buttons
│ │ │ │ └── FilterButton.jsx
│ │ │ ├── cards
│ │ │ │ ├── InfoCard.jsx
│ │ │ │ └── JobCard.jsx
│ │ │ ├── charts
│ │ │ │ ├── BarGraph.jsx
│ │ │ │ ├── LineChart.jsx
│ │ │ │ └── Pie.jsx
│ │ │ ├── chat
│ │ │ │ ├── ChatPanel.jsx
│ │ │ │ ├── ContactItem.jsx
│ │ │ │ └── Message.jsx
│ │ │ ├── error
│ │ │ │ ├── Error.jsx
│ │ │ │ ├── FetchError.jsx
│ │ │ │ ├── NoDataMessage.jsx
│ │ │ │ └── NotFound.jsx
│ │ │ ├── loaders
│ │ │ │ ├── ButtonLoader.jsx
│ │ │ │ ├── ChatbotLoader.jsx
│ │ │ │ ├── ComponentLoader.jsx
│ │ │ │ └── Loader.jsx
│ │ │ ├── modals
│ │ │ │ ├── ApplicantionModal.jsx
│ │ │ │ ├── DepartmentModal.jsx
│ │ │ │ ├── ImportExcelModal.jsx
│ │ │ │ ├── InviteModal.jsx
│ │ │ │ ├── JobApplicationModal.jsx
│ │ │ │ ├── JobOpeningModal.jsx
│ │ │ │ ├── Modal.jsx
│ │ │ │ ├── MonthSheetModal.jsx
│ │ │ │ ├── PayrollModal.jsx
│ │ │ │ ├── PerformanceModal.jsx
│ │ │ │ ├── ProfileModal.jsx
│ │ │ │ ├── RemarksModal.jsx
│ │ │ │ ├── RoleModal.jsx
│ │ │ │ ├── SettingModal.jsx
│ │ │ │ ├── SheetModal.jsx
│ │ │ │ └── SubstituteModal.jsx
│ │ │ └── others
│ │ │ │ ├── ChatPanel.jsx
│ │ │ │ ├── FilterBar.jsx
│ │ │ │ └── Pagination.jsx
│ │ └── ui
│ │ │ ├── EmployeeSidebar.jsx
│ │ │ ├── Footer.jsx
│ │ │ └── Sidebar.jsx
│ ├── constants
│ │ └── index.js
│ ├── context
│ │ └── index.jsx
│ ├── hooks
│ │ └── useDebounce.jsx
│ ├── main.jsx
│ ├── pages
│ │ ├── attendance
│ │ │ ├── Attendance.jsx
│ │ │ └── MarkAttendance.jsx
│ │ ├── complaint
│ │ │ └── Complaint.jsx
│ │ ├── feedback
│ │ │ └── Feedback.jsx
│ │ ├── home
│ │ │ └── Home.jsx
│ │ ├── leave
│ │ │ └── Leave.jsx
│ │ └── updates
│ │ │ └── Update.jsx
│ ├── reducers
│ │ ├── attendance.reducer.js
│ │ ├── authentication.reducer.js
│ │ ├── complaint.reducer.js
│ │ ├── department.reducer.js
│ │ ├── employee.reducer.js
│ │ ├── feedback.reducer.js
│ │ ├── inshights.reducer.js
│ │ ├── leave.reducer.js
│ │ ├── payroll.reducer.js
│ │ ├── performance.reducer.js
│ │ ├── recruitment.reducer.js
│ │ ├── role.reducer.js
│ │ └── update.reducer.js
│ ├── services
│ │ ├── attendance.service.js
│ │ ├── authentication.service.js
│ │ ├── chat.service.js
│ │ ├── complaint.service.js
│ │ ├── department.service.js
│ │ ├── employee.service.js
│ │ ├── feedback.service.js
│ │ ├── insights.service.js
│ │ ├── leave.service.js
│ │ ├── payroll.service.js
│ │ ├── performance.service.js
│ │ ├── recruitment.service.js
│ │ └── role.service.js
│ ├── store
│ │ └── index.js
│ ├── utils
│ │ └── index.js
│ └── validations
│ │ └── index.js
├── tailwind.config.js
└── vite.config.js
├── public
└── hrms.png
└── server
├── .env.example
├── package-lock.json
├── package.json
├── src
├── config
│ └── index.js
├── controllers
│ ├── attendance.controller.js
│ ├── authentication.controller.js
│ ├── chatbot.controller.js
│ ├── complaint.controller.js
│ ├── department.controller.js
│ ├── employee.controller.js
│ ├── feedback.controller.js
│ ├── insights.controller.js
│ ├── leave.controller.js
│ ├── payroll.controller.js
│ ├── performance.controller.js
│ ├── recruitment.controller.js
│ ├── role.controller.js
│ └── update.controller.js
├── doc
│ └── index.js
├── gemini
│ └── index.js
├── index.js
├── middlewares
│ └── index.js
├── models
│ ├── attendance.model.js
│ ├── complaint.model.js
│ ├── department.model.js
│ ├── employee.model.js
│ ├── feedback.model.js
│ ├── leave.model.js
│ ├── payroll.model.js
│ ├── performance.model.js
│ ├── recruitment.model.js
│ ├── role.model.js
│ ├── session.model.js
│ └── update.model.js
├── predictions
│ ├── chatbot.js
│ ├── index.js
│ ├── sentiment.js
│ └── substitute.js
├── public
│ └── welcome.html
├── routes
│ ├── attendance.routes.js
│ ├── authentication.routes.js
│ ├── complaint.routes.js
│ ├── department.routes.js
│ ├── employee.routes.js
│ ├── feedback.routes.js
│ ├── index.routes.js
│ ├── insights.routes.js
│ ├── leave.routes.js
│ ├── payroll.routes.js
│ ├── performance.routes.js
│ ├── recruitment.routes.js
│ └── role.routes.js
├── seeders
│ └── index.js
├── setup
│ └── index.js
├── templates
│ └── index.js
└── utils
│ └── index.js
├── test.yaml
└── vercel.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js dependencies
2 | client/node_modules
3 | server/node_modules
4 |
5 |
6 | # Ignore all .env files
7 | server/.env
8 | server/host.env
9 | server/.vercel
10 | client/.env
11 |
12 | # Ignore client-specific files
13 | client/format.json
14 | client/employees.xlsx
15 |
16 | # Ignore server-specific files
17 | server/src/uploads
18 | server/src/qrcodes
19 |
20 | # Logs
21 | client/*.log
22 | server/*.log
23 | client/npm-debug.log*
24 | server/npm-debug.log*
25 | client/yarn-debug.log*
26 | server/yarn-debug.log*
27 | client/yarn-error.log*
28 | server/yarn-error.log*
29 |
30 | # Build outputs
31 | client/build
32 | client/dist
33 |
34 | # Cache and system files
35 | .DS_Store
36 | Thumbs.db
37 |
38 | # IDE and editor settings
39 | .vscode/
40 | .idea/
41 | *.swp
42 | *.swo
43 |
44 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.18.0
--------------------------------------------------------------------------------
/INSTALLATION.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | ## _Prerequisites_ :
8 |
9 | - _Node.js and npm must be installed on your machine_
10 | - _You can install them from the [Node.js Official Website](https://nodejs.org/)_
11 |
12 | ## _Installation Steps_ :
13 |
14 | - _Clone the repository to your local machine:_
15 | ```bash
16 | git clone https://github.com/WhatsWrongOB/AI-HRMS.git
17 | ```
18 | - _Navigate to the project’s client folder and install dependencies:_
19 | ```bash
20 | cd client
21 | npm install
22 | ```
23 |
24 | - _Navigate to the project’s server folder and install dependencies:_
25 | ```bash
26 | cd server
27 | npm install
28 | ```
29 |
30 | ## _Environment Variables_ :
31 |
32 | - _In the server and client folder, rename the .env.example file to .env:_
33 | - _Fill in all the required environment variables for proper configuration._
34 | ```bash
35 | # In client .env
36 | VITE_URL=
37 | VITE_ENV=
38 |
39 | # In server .env
40 | PORT=
41 | JWTSECRET=
42 | CLIENT_URL=
43 | SERVER_URL=
44 | MONGO_URI=
45 | SMTP_HOST=
46 | SMTP_PORT=
47 | SMTP_USER=
48 | SMTP_PASS=
49 | GEMINI=
50 | LATITUDE=
51 | LONGITUDE=
52 | CLOUDINARY_CLOUD_NAME=
53 | CLOUDINARY_API_KEY=
54 | CLOUDINARY_API_SECRET=
55 | ```
56 |
57 | ## _Initial Project Setup_ :
58 |
59 | - _In the server folder, run the setup command to seed the database with an initial data_
60 | ```bash
61 | npm run setup
62 | ```
63 | - _This will create an employee with ID: 000 & Password: password_
64 | - _You can use these credentials to log in to the hrms application._
65 |
66 | ## _Running the Project_ :
67 |
68 | - _Start the client and server of the application as needed._
69 | ```bash
70 | # In client/
71 | npm run dev
72 |
73 | # In server/
74 | npm run dev
75 | ```
76 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Obaid
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
AI-Driven Human Resource Management ⚡
6 |
7 |
Streamlining HR Operations | AI-Powered Insights
8 |
9 |
10 |
11 | ```
12 | Give a Star ⭐️ & Fork to this project ... Happy coding! 🤩`
13 | ```
14 |
15 | *The AI-Driven HRMS is a robust HR management system, built on the MERN stack. It integrates AI for sentiment analysis, automated substitute assignment, and a Gemini-powered chatbot*
16 |
17 |
18 |
19 | ***Self-hosted Version*** : [_click here_](https://metrohrms.site)
20 |
21 | ***Installation Guide*** : [_click here_](https://github.com/WhatsWrongOB/AI-HRMS/blob/main/INSTALLATION.md)
22 |
23 | ## _Key Features_ :
24 |
25 | - _Robust Authentication_
26 | - _Employee Management_
27 | - _Attendance Management_
28 | - _Leave Management_
29 | - _Payroll Management_
30 | - _Recruitment Management_
31 | - _Performance Management_
32 | - _Complaint Management_
33 | - _Feedback Management_
34 | - _Reports & Analytics_
35 |
36 | ## _Standout Features_ :
37 |
38 | - _AI Sentiment Analysis_
39 | - _AI Substitute Assignment_
40 | - _QR Code-Based Attendance_
41 | - _GEMINI-Powered Chatbot_
42 |
43 | ## _Impact or Outcomes_ :
44 |
45 | - _Faster Decision-Making_
46 | - _Reduced Manual Workload_
47 | - _Improved Attendance Accuracy_
48 | - _Enhanced Employee Satisfaction_
49 |
50 |
51 | ## _Tech Stack_ :
52 |
53 | - _MERN Stack for Development_
54 | - _Redux for State Management_
55 | - _AI Integration with Gemini_
56 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/.env.example:
--------------------------------------------------------------------------------
1 |
2 | VITE_URL=http://localhost:3000/api
3 | VITE_ENV=development
--------------------------------------------------------------------------------
/client/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/client/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | { ignores: ['dist'] },
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react-refresh/only-export-components': [
33 | 'warn',
34 | { allowConstantExport: true },
35 | ],
36 | },
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Metro HR
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/client/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "https://metrohrms.netlify.app/*"
3 | to = "https://metrohrms.site/:splat"
4 | status = 301
5 | force = true
6 |
7 | [[redirects]]
8 | from = "/*"
9 | to = "/index.html"
10 | status = 200
11 |
12 | [build]
13 | image = "ubuntu-22.04"
14 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hrms",
3 | "private": true,
4 | "version": "0.0.0",
5 | "description": "AI Driven Human Resource Managemen System - Final Year Project",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite --host",
9 | "build": "vite build",
10 | "lint": "eslint .",
11 | "preview": "vite preview"
12 | },
13 | "author": "Obaid Ali Siddiqui",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/WhatsWrongOB/AI-HRMS"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/WhatsWrongOB/AI-HRMS/issues"
20 | },
21 | "homepage": "https://metrohrms.netlify.app",
22 | "keywords": [
23 | "HRMS",
24 | "AI",
25 | "MERN",
26 | "Human Resources",
27 | "Payroll",
28 | "Attendance"
29 | ],
30 | "dependencies": {
31 | "@fortawesome/fontawesome-free": "^6.7.2",
32 | "@hookform/resolvers": "^3.9.1",
33 | "@reduxjs/toolkit": "^2.3.0",
34 | "axios": "^1.7.7",
35 | "chart.js": "^4.4.6",
36 | "filepond": "^4.32.7",
37 | "react": "^18.3.1",
38 | "react-chartjs-2": "^5.2.0",
39 | "react-dom": "^18.3.1",
40 | "react-filepond": "^7.1.3",
41 | "react-helmet": "^6.1.0",
42 | "react-hook-form": "^7.54.1",
43 | "react-hot-toast": "^2.4.1",
44 | "react-icons": "^5.4.0",
45 | "react-markdown": "^9.0.3",
46 | "react-redux": "^9.1.2",
47 | "react-router-dom": "^6.27.0",
48 | "socket.io-client": "^4.8.1",
49 | "typeface-poppins": "^1.1.13",
50 | "xlsx": "^0.18.5",
51 | "zod": "^3.24.2"
52 | },
53 | "devDependencies": {
54 | "@eslint/js": "^9.13.0",
55 | "@types/react": "^18.3.11",
56 | "@types/react-dom": "^18.3.1",
57 | "@vitejs/plugin-react": "^4.3.3",
58 | "autoprefixer": "^10.4.20",
59 | "eslint": "^9.13.0",
60 | "eslint-plugin-react": "^7.37.1",
61 | "eslint-plugin-react-hooks": "^5.0.0",
62 | "eslint-plugin-react-refresh": "^0.4.13",
63 | "globals": "^15.11.0",
64 | "postcss": "^8.4.47",
65 | "tailwindcss": "^3.4.14",
66 | "vite": "^5.4.9"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/public/cursor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/client/public/cursor.png
--------------------------------------------------------------------------------
/client/public/hrms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/client/public/hrms.png
--------------------------------------------------------------------------------
/client/public/images.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/client/public/images.png
--------------------------------------------------------------------------------
/client/public/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/client/public/metro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/client/public/metro.png
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /private/
3 | Sitemap: https://metrohrms.site/sitemap.xml
--------------------------------------------------------------------------------
/client/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | https://metrohrms.site/
6 | 2025-08-04
7 | 1.00
8 |
9 |
10 |
11 | https://metrohrms.site/careers
12 | 2025-08-04
13 | 0.80
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/client/public/unknown.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/client/public/unknown.jpeg
--------------------------------------------------------------------------------
/client/public/verify.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/client/public/verify.avif
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import "typeface-poppins";
2 | import Login from "./auth/Login";
3 | import AdminApp from "./app/admin";
4 | import EmployeeApp from "./app/employee";
5 | import { Toaster } from "react-hot-toast";
6 | import { ThemeProvider } from "./context";
7 | import { useSelector } from "react-redux";
8 | import "@fortawesome/fontawesome-free/css/all.min.css";
9 | import Loader from "./components/shared/loaders/Loader";
10 | import { Navigate, Route, Routes } from "react-router-dom";
11 | import { lazy, Suspense, useEffect, useState } from "react";
12 |
13 | const Career = lazy(() => import("./careers/Career"));
14 | const ResetPassword = lazy(() => import("./auth/ResetPassword"));
15 | const ForgetPassword = lazy(() => import("./auth/ForgetPassword"));
16 | const InvalidResetLink = lazy(() => import("./auth/InvalidResetLink"));
17 | const EmailConfirmation = lazy(() => import("./auth/EmailConfirmation"));
18 |
19 | function HrmsForMetroCashAndCarry() {
20 | const { user } = useSelector((state) => state.authentication);
21 |
22 | useEffect(() => {
23 | const syncLogout = (event) => {
24 | if (event.key === "logout") {
25 | sessionStorage.clear();
26 | localStorage.clear();
27 |
28 | window.location.href = "/login";
29 | }
30 | };
31 |
32 | window.addEventListener("storage", syncLogout);
33 | return () => {
34 | window.removeEventListener("storage", syncLogout);
35 | };
36 | }, []);
37 |
38 | if (user?.authority === "admin") return ;
39 | if (user?.authority === "employee") return ;
40 |
41 | return ;
42 | }
43 |
44 | function EmployeeRouter() {
45 | return (
46 |
47 |
48 | } />
49 |
50 |
51 | );
52 | }
53 |
54 | function AdminRouter() {
55 | return (
56 |
57 |
58 | } />
59 |
60 |
61 | );
62 | }
63 |
64 | function AuthRouter() {
65 | return (
66 |
67 | } />
68 | } />
69 | } />
70 | } />
71 | } />
72 | } />
73 | } />
74 |
75 | );
76 | }
77 |
78 | const RootApp = () => {
79 | return (
80 | }>
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default RootApp;
88 |
--------------------------------------------------------------------------------
/client/src/admin/mail/Communication.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FaSearch } from "react-icons/fa";
3 | import ChatPanel from "../../components/shared/chat/ChatPanel";
4 | import ContactItem from "../../components/shared/chat/ContactItem";
5 |
6 | const employees = [
7 | {
8 | id: 1,
9 | name: "Obaid Ali",
10 | designation: "Manager",
11 | profilePic: "https://randomuser.me/api/portraits/men/1.jpg",
12 | status: "online",
13 | lastSeen: "2 min ago",
14 | },
15 | {
16 | id: 2,
17 | name: "Noman Rehan",
18 | designation: "Supervisor",
19 | profilePic: "https://randomuser.me/api/portraits/women/1.jpg",
20 | status: "online",
21 | lastSeen: "5 min ago",
22 | },
23 | {
24 | id: 3,
25 | name: "Mujtaba Abid",
26 | designation: "Developer",
27 | profilePic: "https://randomuser.me/api/portraits/men/2.jpg",
28 | status: "offline",
29 | lastSeen: "1 hour ago",
30 | },
31 | {
32 | id: 4,
33 | name: "Mohsin Khan",
34 | designation: "Designer",
35 | profilePic: "https://randomuser.me/api/portraits/women/2.jpg",
36 | status: "online",
37 | lastSeen: "Just now",
38 | },
39 | {
40 | id: 5,
41 | name: "Qaim Ali",
42 | designation: "QA Engineer",
43 | profilePic: "https://randomuser.me/api/portraits/men/3.jpg",
44 | status: "offline",
45 | lastSeen: "3 hours ago",
46 | },
47 | ];
48 |
49 | const Communication = () => {
50 |
51 | const [message, setMessage] = useState("");
52 | const [searchTerm, setSearchTerm] = useState("");
53 | const [showContacts, setShowContacts] = useState(true);
54 | const [selectedEmployee, setSelectedEmployee] = useState(null);
55 |
56 | const filteredEmployees = employees.filter(
57 | (employee) =>
58 | employee.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
59 | employee.designation.toLowerCase().includes(searchTerm.toLowerCase())
60 | );
61 |
62 | const handleBack = () => {
63 | setShowContacts(true);
64 | setSelectedEmployee(null)
65 | };
66 |
67 | const handleSelectEmployee = (employee) => {
68 | setSelectedEmployee(employee);
69 | setShowContacts(false);
70 | };
71 |
72 | return (
73 |
75 |
80 |
81 |
82 | Contacts
83 |
84 |
85 |
86 | setSearchTerm(e.target.value)}
92 | />
93 |
94 |
95 |
96 |
99 | {filteredEmployees.length > 0 ? (
100 | filteredEmployees.map((employee) => (
101 |
107 | ))
108 | ) : (
109 |
110 | No contacts found
111 |
112 | )}
113 |
114 |
115 |
116 |
122 |
123 | );
124 | };
125 |
126 | export default Communication;
127 |
--------------------------------------------------------------------------------
/client/src/app/admin.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import Sidebar from "../components/ui/Sidebar";
3 | import { lazy, Suspense, useEffect } from "react";
4 | import { Route, Routes } from "react-router-dom";
5 | import { getRoles } from "../services/role.service";
6 | import Loader from "../components/shared/loaders/Loader";
7 | import NotFound from "../components/shared/error/NotFound";
8 | import { getInsights } from "../services/insights.service";
9 | import { getDepartments } from "../services/department.service";
10 |
11 | const Dashboard = lazy(() => import("../admin/dashboard/Dashboard"));
12 | const Employee = lazy(() => import("../admin/employee/Employee"));
13 | const AddEmployee = lazy(() => import("../admin/employee/CreateEmployee"));
14 | const EditEmployee = lazy(() => import("../admin/employee/UpdateEmployee"));
15 | const ViewEmployee = lazy(() => import("../admin/employee/ViewEmployee"));
16 | const Department = lazy(() => import("../admin/department/Department"));
17 | const Attendance = lazy(() => import("../admin/attendance/Attendance"));
18 | const CheckAttendance = lazy(() =>
19 | import("../admin/attendance/CheckAttendance")
20 | );
21 | const Feedback = lazy(() => import("../admin/feedback/Feedback"));
22 | const LeaveRequest = lazy(() => import("../admin/leave/LeaveRequest"));
23 | const EmployeeOnLeave = lazy(() => import("../admin/leave/EmployeeOnLeave"));
24 | const Complaint = lazy(() => import("../admin/complaint/Complaint"));
25 | const JobApplications = lazy(() =>
26 | import("../admin/recruitment/JobApplications")
27 | );
28 | const JobOpenings = lazy(() => import("../admin/recruitment/JobOpenings"));
29 | const PostJob = lazy(() => import("../admin/recruitment/PostJob"));
30 | const Performance = lazy(() => import("../admin/performance/Performance"));
31 | const Report = lazy(() => import("../admin/report/Report"));
32 | const Payroll = lazy(() => import("../admin/payroll/Payroll"));
33 |
34 | const AdminApp = () => {
35 | const dispatch = useDispatch();
36 |
37 | useEffect(() => {
38 | dispatch(getRoles());
39 | dispatch(getInsights());
40 | dispatch(getDepartments());
41 | }, []);
42 |
43 | return (
44 |
48 |
49 | }>
50 |
54 |
55 | } />
56 | } />
57 | } />
58 | } />
59 | } />
60 | } />
61 | } />
62 | } />
63 | } />
64 | } />
65 | } />
66 | } />
67 | } />
68 | } />
69 | } />
70 | } />
71 | } />
72 | } />
73 | } />
74 | } />
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default AdminApp;
83 |
--------------------------------------------------------------------------------
/client/src/app/employee.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { lazy, Suspense, useEffect } from "react";
3 | import { Route, Routes } from "react-router-dom";
4 | import Loader from "../components/shared/loaders/Loader";
5 | import NotFound from "../components/shared/error/NotFound";
6 | import EmployeeSidebar from "../components/ui/EmployeeSidebar";
7 | import { getEmployeeInsights } from "../services/insights.service";
8 |
9 | const Home = lazy(() => import("../pages/home/Home"));
10 | const Leave = lazy(() => import("../pages/leave/Leave"));
11 | const Update = lazy(() => import("../pages/updates/Update"));
12 | const Feedback = lazy(() => import("../pages/feedback/Feedback"));
13 | const Complaint = lazy(() => import("../pages/complaint/Complaint"));
14 | const Attendance = lazy(() => import("../pages/attendance/Attendance"));
15 | const MarkAttendance = lazy(() => import("../pages/attendance/MarkAttendance"));
16 |
17 | const EmployeeApp = () => {
18 | const dispatch = useDispatch();
19 |
20 | useEffect(() => {
21 | dispatch(getEmployeeInsights());
22 | }, []);
23 |
24 | return (
25 |
29 |
30 | }>
31 |
35 |
36 | } />
37 | } />
38 | } />
39 | } />
40 | } />
41 | } />
42 | } />
43 | } />
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default EmployeeApp;
52 |
--------------------------------------------------------------------------------
/client/src/auth/EmailConfirmation.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet";
3 | import { Link } from "react-router-dom";
4 |
5 | const EmailConfirmation = () => {
6 | return (
7 | <>
8 |
9 | Email Confirmation - Metro HR
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 | A password reset link has been sent to your email. Follow the
22 | instructions to reset password
23 |
24 |
28 | Back to Login Page
29 |
30 |
31 |
32 |
33 | >
34 | );
35 | };
36 |
37 | export default EmailConfirmation;
38 |
--------------------------------------------------------------------------------
/client/src/auth/InvalidResetLink.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet";
3 | import { Link } from "react-router-dom";
4 |
5 | const InvalidResetLink = () => {
6 | return (
7 | <>
8 |
9 | Invalid Reset Link - Metro HR
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 | Oops! This reset link has expired or is invalid. Try requesting a
22 | new one.
23 |
24 |
28 | Back to Login Page
29 |
30 |
31 |
32 |
33 | >
34 | );
35 | };
36 |
37 | export default InvalidResetLink;
38 |
--------------------------------------------------------------------------------
/client/src/axios/axiosInstance.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getToken } from "../utils";
3 |
4 | const axiosInstance = axios.create({
5 | baseURL: import.meta.env.VITE_URL || "http://localhost:3000",
6 | headers: { "Content-Type": "application/json" },
7 | });
8 |
9 | axiosInstance.interceptors.request.use(
10 | (config) => {
11 | const token = getToken();
12 | if (token) config.headers.Authorization = `Bearer ${token}`;
13 | return config;
14 | },
15 | (error) => Promise.reject(error)
16 | );
17 |
18 | axiosInstance.interceptors.response.use(
19 | (response) => response,
20 | (error) => {
21 | const msg = error.response?.data?.message?.toLowerCase();
22 | if (
23 | error.response?.status === 500 &&
24 | (msg.includes("jwt") ||
25 | msg.includes("unauthorized") ||
26 | msg.includes("session expired"))
27 | ) {
28 | sessionStorage.clear();
29 | localStorage.clear();
30 |
31 | window.location.href = "/login";
32 | }
33 | return Promise.reject(error);
34 | }
35 | );
36 |
37 | export default axiosInstance;
38 |
--------------------------------------------------------------------------------
/client/src/components/shared/buttons/FilterButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const FilterButton = ({ setState, state, filter }) => {
4 | return (
5 | setState(filter.value)}
7 | className={`hidden sm:flex flex-grow sm:flex-grow-0 justify-center items-center gap-2 text-sm font-semibold border py-1 px-5 rounded-3xl transition-all ease-in-out duration-300
8 | ${
9 | state === filter.value
10 | ? "border-blue-500 bg-blue-100 text-blue-600 dark:bg-transparent dark:text-blue-400"
11 | : "border-gray-300 dark:border-gray-500 text-gray-700 dark:text-gray-300"
12 | }
13 | hover:border-blue-500 hover:bg-blue-100 hover:text-blue-600
14 | dark:hover:border-blue-500 dark:hover:bg-transparent dark:hover:text-blue-500
15 | focus:outline-none focus:ring-1 focus:ring-blue-500`}
16 | >
17 |
18 | {filter.label}
19 |
20 | );
21 | };
22 |
23 | export default FilterButton;
24 |
--------------------------------------------------------------------------------
/client/src/components/shared/cards/InfoCard.jsx:
--------------------------------------------------------------------------------
1 | const InfoCard = ({ detail }) => {
2 | return (
3 |
4 |
5 |
6 | {detail.title}
7 |
8 |
9 |
10 |
11 | {detail?.stats || 0}
12 |
13 |
14 |
15 |
16 | Since last month
17 |
18 |
19 |
20 |
23 |
24 | );
25 | };
26 |
27 | export default InfoCard;
28 |
--------------------------------------------------------------------------------
/client/src/components/shared/cards/JobCard.jsx:
--------------------------------------------------------------------------------
1 | import { formatDate } from "../../../utils";
2 |
3 | const JobCard = ({ job, onApply }) => {
4 | return (
5 |
9 |
10 |
11 |
{job.title}
12 |
13 | {job.type}
14 |
15 |
16 |
17 |
18 |
19 | {job.department.name}
20 |
21 |
22 | {job.role.name}
23 |
24 |
25 |
26 |
27 |
28 |
29 | {job.location}
30 |
31 |
32 | ${job.minSalary}{" "}
33 | - ${job.maxSalary}
34 |
35 |
36 |
37 |
{job.description}
38 |
39 |
40 |
41 | Deadline: {" "}
42 | {formatDate(job.deadline)}
43 |
44 |
onApply(job._id)}
46 | className="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-4 rounded-lg transition duration-300 flex items-center"
47 | >
48 | Apply Now
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default JobCard;
58 |
--------------------------------------------------------------------------------
/client/src/components/shared/charts/BarGraph.jsx:
--------------------------------------------------------------------------------
1 | import { Bar } from "react-chartjs-2";
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend,
10 | ArcElement,
11 | } from "chart.js";
12 |
13 | ChartJS.register(
14 | CategoryScale,
15 | LinearScale,
16 | BarElement,
17 | ArcElement,
18 | Title,
19 | Tooltip,
20 | Legend
21 | );
22 |
23 | const BarGraph = ({
24 | text,
25 | title,
26 | departments,
27 | departmentAttendancePercentage,
28 | }) => {
29 | const data = {
30 | labels: departments || [],
31 | datasets: [
32 | {
33 | label: "Attendance Rate (%)",
34 | data: departmentAttendancePercentage || [],
35 | backgroundColor: [
36 | "rgba(30, 144, 255, 0.7)",
37 | "rgba(220, 20, 60, 0.7)",
38 | "rgba(255, 193, 7, 0.7)",
39 | "rgba(32, 178, 170, 0.7)",
40 | "rgba(138, 43, 226, 0.7)",
41 | "rgba(255, 140, 0, 0.7)",
42 | ],
43 |
44 | borderColor: [
45 | "rgba(30, 144, 255, 1)",
46 | "rgba(220, 20, 60, 1)",
47 | "rgba(255, 193, 7, 1)",
48 | "rgba(32, 178, 170, 1)",
49 | "rgba(138, 43, 226, 1)",
50 | "rgba(255, 140, 0, 1)",
51 | ],
52 |
53 | borderWidth: 1,
54 | },
55 | ],
56 | };
57 |
58 | const options = {
59 | responsive: true,
60 | plugins: {
61 | legend: {
62 | position: "top",
63 | },
64 | title: {
65 | display: true,
66 | text: text,
67 | },
68 | },
69 | scales: {
70 | x: {
71 | grid: {
72 | display: true,
73 | },
74 | },
75 | y: {
76 | grid: {
77 | display: true,
78 | },
79 | title: {
80 | display: true,
81 | text: title,
82 | },
83 | },
84 | },
85 | };
86 |
87 | return ;
88 | };
89 |
90 | export default BarGraph;
91 |
--------------------------------------------------------------------------------
/client/src/components/shared/charts/LineChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Line } from "react-chartjs-2";
3 | import {
4 | Chart as ChartJS,
5 | CategoryScale,
6 | LinearScale,
7 | PointElement,
8 | LineElement,
9 | Title,
10 | Tooltip,
11 | Legend,
12 | Filler,
13 | } from "chart.js";
14 |
15 | ChartJS.register(
16 | CategoryScale,
17 | LinearScale,
18 | PointElement,
19 | LineElement,
20 | Title,
21 | Tooltip,
22 | Legend,
23 | Filler
24 | );
25 |
26 | const LineChart = ({ label, title, chartData }) => {
27 | const data = {
28 | labels: [
29 | "Jan",
30 | "Feb",
31 | "Mar",
32 | "Apr",
33 | "May",
34 | "Jun",
35 | "Jul",
36 | "Aug",
37 | "Sep",
38 | "Oct",
39 | "Nov",
40 | "Dec",
41 | ],
42 | datasets: [
43 | {
44 | label: label,
45 | data: chartData || [10, 30, 40, 25, 40, 74, 20, 60, 65, 76, 50, 40],
46 | fill: true,
47 | backgroundColor: "rgba(54, 162, 235, 0.2)",
48 | borderColor: "rgba(54, 162, 235, 1)",
49 | tension: 0.4,
50 | },
51 | ],
52 | };
53 |
54 | const options = {
55 | responsive: true,
56 | maintainAspectRatio: false,
57 | plugins: {
58 | title: {
59 | display: true,
60 | text: title,
61 | },
62 | },
63 | scales: {
64 | x: {
65 | position: "bottom",
66 | },
67 | y: {
68 | position: "left",
69 | min: 0,
70 | max: 100,
71 | ticks: {
72 | stepSize: 20,
73 | callback: (value) => `${value}%`,
74 | },
75 | },
76 | },
77 | };
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default LineChart;
87 |
--------------------------------------------------------------------------------
/client/src/components/shared/charts/Pie.jsx:
--------------------------------------------------------------------------------
1 | import { Pie } from "react-chartjs-2";
2 | import {
3 | Chart as ChartJS,
4 | CategoryScale,
5 | LinearScale,
6 | BarElement,
7 | Title,
8 | Tooltip,
9 | Legend,
10 | ArcElement,
11 | } from "chart.js";
12 | import { useSelector } from "react-redux";
13 |
14 | ChartJS.register(
15 | CategoryScale,
16 | LinearScale,
17 | BarElement,
18 | ArcElement,
19 | Title,
20 | Tooltip,
21 | Legend
22 | );
23 |
24 | const PieGraph = ({ labels, title, label, data1, data2 }) => {
25 | const pieData = {
26 | labels: [labels.category1, labels.category2],
27 | datasets: [
28 | {
29 | label: label,
30 | data: [data1, data2],
31 | backgroundColor: ["rgba(30, 144, 255, 0.7)", "rgba(220, 20, 60, 0.7)"],
32 |
33 | borderColor: ["rgba(30, 144, 255, 1)", "rgba(220, 20, 60, 1)"],
34 |
35 | borderWidth: 1,
36 | },
37 | ],
38 | };
39 |
40 | const pieOptions = {
41 | responsive: true,
42 | plugins: {
43 | legend: {
44 | position: "top",
45 | },
46 | title: {
47 | display: true,
48 | text: title,
49 | },
50 | },
51 | };
52 |
53 | return ;
54 | };
55 |
56 | export default PieGraph;
57 |
--------------------------------------------------------------------------------
/client/src/components/shared/chat/ContactItem.jsx:
--------------------------------------------------------------------------------
1 | const ContactItem = ({ employee, isSelected, onSelect }) => (
2 | onSelect(employee)}
9 | >
10 |
11 |
16 |
21 |
22 |
23 |
24 | {employee.name}
25 |
26 |
27 |
28 | {employee.designation}
29 |
30 |
31 | ~ {employee.lastSeen}
32 |
33 |
34 |
35 |
36 | );
37 |
38 | export default ContactItem;
39 |
--------------------------------------------------------------------------------
/client/src/components/shared/chat/Message.jsx:
--------------------------------------------------------------------------------
1 | const Message = ({ isSender, text, time }) => (
2 |
5 |
12 |
{text}
13 |
20 | {time}
21 |
22 |
23 |
24 | );
25 |
26 | export default Message;
27 |
--------------------------------------------------------------------------------
/client/src/components/shared/error/Error.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Error = () => {
4 | return (
5 |
6 |
⚠️
7 |
8 | Internal Server Error, Try Again Later
9 |
10 |
11 | );
12 | };
13 |
14 | export default Error;
15 |
--------------------------------------------------------------------------------
/client/src/components/shared/error/FetchError.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const FetchError = ({ error }) => {
4 |
5 | return (
6 |
12 | );
13 | };
14 |
15 | export default FetchError;
16 |
--------------------------------------------------------------------------------
/client/src/components/shared/error/NoDataMessage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NoDataMessage = ({ message }) => {
4 | return (
5 |
9 | );
10 | };
11 |
12 | export default NoDataMessage;
13 |
--------------------------------------------------------------------------------
/client/src/components/shared/error/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
7 |
8 |
404 Page not Found
9 |
10 |
11 | );
12 | };
13 |
14 | export default NotFound;
15 |
--------------------------------------------------------------------------------
/client/src/components/shared/loaders/ButtonLoader.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ButtonLoader = () => {
4 | return (
5 |
11 |
19 |
24 |
25 | );
26 | };
27 |
28 | export default ButtonLoader;
29 |
--------------------------------------------------------------------------------
/client/src/components/shared/loaders/ChatbotLoader.jsx:
--------------------------------------------------------------------------------
1 | const ChatbotLoader = () => {
2 | return (
3 |
8 | );
9 | };
10 |
11 | export default ChatbotLoader;
12 |
--------------------------------------------------------------------------------
/client/src/components/shared/loaders/ComponentLoader.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ComponentLoader = () => {
4 | return (
5 |
12 | );
13 | };
14 |
15 | export default ComponentLoader;
16 |
--------------------------------------------------------------------------------
/client/src/components/shared/loaders/Loader.jsx:
--------------------------------------------------------------------------------
1 | const Loader = () => {
2 | return (
3 |
12 | );
13 | };
14 |
15 | export default Loader;
16 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/ApplicantionModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { jobStatus } from "../../../constants";
4 | import { updateApplication } from "../../../services/recruitment.service";
5 |
6 | const ApplicationModal = ({ onClose, application, jobId }) => {
7 | const dispatch = useDispatch();
8 |
9 | const [formData, setFormData] = useState({
10 | status: application.status,
11 | });
12 |
13 | const handleChange = (e) => {
14 | const { name, value } = e.target;
15 | setFormData({ ...formData, [name]: value });
16 | };
17 |
18 | const handleSubmit = (e) => {
19 | e.preventDefault();
20 |
21 | const formattedData = {
22 | status: formData.status,
23 | };
24 |
25 | dispatch(
26 | updateApplication({
27 | jobId: jobId,
28 | applicantId: application._id,
29 | application: formattedData,
30 | })
31 | );
32 |
33 | onClose();
34 | };
35 |
36 | return (
37 |
82 | );
83 | };
84 |
85 | export default ApplicationModal;
86 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/InviteModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { inviteForInterview } from "../../../services/recruitment.service";
4 |
5 | const InviteModal = ({ onClose, application, jobId }) => {
6 | const dispatch = useDispatch();
7 |
8 | const [formData, setFormData] = useState({
9 | interviewDate: new Date().toISOString().split("T")[0],
10 | interviewTime: "10:00",
11 | });
12 |
13 | const handleChange = (e) => {
14 | const { name, value } = e.target;
15 | setFormData({ ...formData, [name]: value });
16 | };
17 |
18 | const handleSubmit = (e) => {
19 | e.preventDefault();
20 |
21 | dispatch(
22 | inviteForInterview({
23 | jobId: jobId,
24 | applicationId: application._id,
25 | interviewDetails: formData,
26 | })
27 | );
28 |
29 | onClose();
30 | };
31 |
32 | return (
33 |
85 | );
86 | };
87 |
88 | export default InviteModal;
89 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/JobOpeningModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { updateJob } from "../../../services/recruitment.service";
4 |
5 | const JobOpeningModal = ({ onClose, job }) => {
6 | const dispatch = useDispatch();
7 |
8 | const [formData, setFormData] = useState({
9 | status: job.status,
10 | deadline: job.deadline.split("T")[0],
11 | });
12 |
13 | const handleChange = (e) => {
14 | const { name, value } = e.target;
15 | setFormData({ ...formData, [name]: value });
16 | };
17 |
18 | const handleSubmit = (e) => {
19 | e.preventDefault();
20 |
21 | const formattedData = {
22 | status: formData.status,
23 | deadline: formData.deadline,
24 | };
25 |
26 | dispatch(updateJob({ id: job._id, job: formattedData }));
27 |
28 | onClose();
29 | };
30 |
31 | return (
32 |
87 | );
88 | };
89 |
90 | export default JobOpeningModal;
91 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Modal = ({ onClose, isConfirm, action }) => {
4 | return (
5 |
6 |
10 |
14 | Confirmation ✨
15 |
16 |
Are you sure you want to {action}?
17 |
18 | isConfirm()}
20 | id="modal-confirm"
21 | className="bg-blue-500 text-sm text-white px-4 py-2 rounded"
22 | >
23 | Confirm
24 |
25 |
30 | Cancel
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Modal;
39 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/MonthSheetModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { months } from "../../../constants";
3 |
4 | const MonthSheetModal = ({
5 | onClose,
6 | departments,
7 | selectedDate,
8 | setSelectedDate,
9 | handleModalSubmit,
10 | selectedDepartment,
11 | setSelectedDepartment,
12 | }) => {
13 | return (
14 |
77 | );
78 | };
79 |
80 | export default MonthSheetModal;
81 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/PayrollModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { updatePayroll } from "../../../services/payroll.service";
4 |
5 | const PayrollModal = ({ onClose, payroll }) => {
6 | const dispatch = useDispatch();
7 |
8 | const [formData, setFormData] = useState({
9 | allowances: payroll?.allowances || 0,
10 | bonuses: payroll?.bonuses || 0,
11 | deductions: payroll?.deductions || 0,
12 | });
13 |
14 | const handleChange = (e) => {
15 | const { name, value } = e.target;
16 | setFormData({
17 | ...formData,
18 | [name]: Number(value) || 0,
19 | });
20 | };
21 |
22 | const handleSubmit = (e) => {
23 | e.preventDefault();
24 | dispatch(
25 | updatePayroll({
26 | id: payroll._id,
27 | formData,
28 | })
29 | );
30 | onClose();
31 | };
32 |
33 | return (
34 |
107 | );
108 | };
109 |
110 | export default PayrollModal;
111 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/PerformanceModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { updatePerformance } from "../../../services/performance.service";
4 |
5 | const PerformanceModal = ({ onClose, performance }) => {
6 | const dispatch = useDispatch();
7 |
8 | const [formData, setFormData] = useState({
9 | rating: performance.rating || "",
10 | feedback: performance.feedback || "",
11 | });
12 |
13 | const handleChange = (e) => {
14 | const { name, value } = e.target;
15 | setFormData({ ...formData, [name]: value });
16 | };
17 |
18 | const handleSubmit = (e) => {
19 | e.preventDefault();
20 |
21 | const formattedData = {
22 | rating: parseInt(formData.rating),
23 | feedback: formData.feedback,
24 | kpis: performance.kpis,
25 | };
26 |
27 | dispatch(
28 | updatePerformance({ id: performance._id, performance: formattedData })
29 | );
30 |
31 | onClose();
32 | };
33 |
34 | return (
35 |
97 | );
98 | };
99 |
100 | export default PerformanceModal;
101 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/ProfileModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ProfileModal = ({
4 | handleFileChange,
5 | imagePreview,
6 | name,
7 | showButton,
8 | loading,
9 | handleClick,
10 | }) => {
11 |
12 | const handleEditImage = () => {
13 | document.getElementById("fileInput").click();
14 | };
15 |
16 | return (
17 |
18 |
22 | {/* Edit Icon */}
23 |
28 |
29 |
30 |
31 | {/* Hidden File Input */}
32 |
39 |
40 | {/* Profile Image */}
41 |
42 |
47 |
48 | {/* Name */}
49 |
55 | {name}
56 |
57 |
58 | {showButton && (
59 |
64 | {loading ? : "Update"}
65 |
66 | )}
67 |
68 |
69 | );
70 | };
71 |
72 | export default ProfileModal;
73 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/RemarksModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const RemarksModal = ({ onClose, isConfirm }) => {
4 | const [remarks, setRemarks] = useState("");
5 |
6 | return (
7 |
8 |
12 |
13 | Add Remarks
14 |
15 |
16 |
setRemarks(e.target.value)}
19 | placeholder="Write your remarks"
20 | className="w-full p-4 text-black bg-[#EFEFEF] text-sm rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-700 resize-none"
21 | rows={4}
22 | required
23 | />
24 |
25 |
26 |
30 | Cancel
31 |
32 | isConfirm(remarks)}
34 | className="px-4 py-2 text-sm text-white bg-blue-500 rounded hover:bg-blue-600"
35 | >
36 | Submit
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default RemarksModal;
45 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/RoleModal.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import React, { useState, useEffect } from "react";
3 | import { createRole, updateRole } from "../../../services/role.service";
4 |
5 | const RoleModal = ({ action, onClose, role }) => {
6 | const dispatch = useDispatch();
7 |
8 | const [formData, setFormData] = useState({
9 | name: "",
10 | description: "",
11 | });
12 |
13 | useEffect(() => {
14 | if (action === "update" && role) {
15 | setFormData({
16 | name: role.name || "",
17 | description: role.description || "",
18 | });
19 | }
20 | }, [action, role]);
21 |
22 | const handleChange = (e) => {
23 | const { name, value } = e.target;
24 | setFormData((prevData) => ({
25 | ...prevData,
26 | [name]: value,
27 | }));
28 | };
29 |
30 | const handleSubmit = (e) => {
31 | e.preventDefault();
32 |
33 | if (action === "update")
34 | dispatch(updateRole({ id: role._id, role: formData }));
35 | else dispatch(createRole(formData));
36 |
37 | onClose();
38 | };
39 |
40 | return (
41 |
42 |
47 | {/* Modal Header */}
48 |
49 |
50 | {action === "create" ? "Create" : "Update"} Position
51 |
52 |
57 |
58 |
59 |
60 |
61 | {/* Role name */}
62 |
76 |
77 | {/* Description Textarea */}
78 |
79 |
88 |
89 |
90 | {/* Submit Button */}
91 |
92 |
96 | Submit
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default RoleModal;
105 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/SheetModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SheetModal = ({
4 | onClose,
5 | departments,
6 | selectedDate,
7 | setSelectedDate,
8 | handleModalSubmit,
9 | selectedDepartment,
10 | setSelectedDepartment,
11 | }) => {
12 | return (
13 |
14 |
19 |
20 |
21 | Department & Date
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | setSelectedDepartment(e.target.value)}
36 | id="select"
37 | className="w-full bg-[#EFEFEF] text-center text-sm p-[17px] rounded-full focus:outline focus:outline-2 focus:outline-gray-700 font-[500] pl-12"
38 | required
39 | >
40 | --- Select Depart ---
41 | {departments &&
42 | departments.map((department) => (
43 |
44 | {department.name}
45 |
46 | ))}
47 |
48 |
49 |
50 |
51 |
52 |
53 | setSelectedDate(e.target.value)}
58 | className="w-full bg-[#EFEFEF] text-sm sm:text-center p-[17px] rounded-full focus:outline focus:outline-2 focus:outline-gray-400 font-[500] pl-12"
59 | required
60 | />
61 |
62 |
63 |
64 |
65 |
69 | Get Sheet
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default SheetModal;
78 |
--------------------------------------------------------------------------------
/client/src/components/shared/modals/SubstituteModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { assignSustitute } from "../../../services/leave.service";
4 | import { getAllEmployeesForHead } from "../../../services/department.service";
5 |
6 | const SubstituteModal = ({ onClose, leaveId }) => {
7 | const dispatch = useDispatch();
8 | const { heads } = useSelector((state) => state.department);
9 |
10 | const [employee, setEmployee] = useState("");
11 |
12 | const handleSubmit = (e) => {
13 | e.preventDefault();
14 |
15 | dispatch(assignSustitute({ leaveId, employee }));
16 |
17 | onClose();
18 | };
19 |
20 | useEffect(() => {
21 | dispatch(getAllEmployeesForHead());
22 | }, []);
23 |
24 | return (
25 |
26 |
31 |
32 |
Assign Substitute
33 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | setEmployee(e.target.value)}
49 | className="w-full bg-[#EFEFEF] text-center text-sm p-[17px] rounded-full focus:outline focus:outline-2 focus:outline-gray-700 font-medium pl-12"
50 | required
51 | >
52 | --- Select Substitute ---
53 | {heads.length > 0 &&
54 | heads.map((head) => (
55 |
56 | {head.name}
57 |
58 | ))}
59 |
60 |
61 |
62 |
63 |
67 | Submit
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default SubstituteModal;
76 |
--------------------------------------------------------------------------------
/client/src/components/shared/others/Pagination.jsx:
--------------------------------------------------------------------------------
1 | import { React } from "react";
2 |
3 |
4 | function Pagination({ currentPage, totalPages, onPageChange }) {
5 | const goToPrevPage = () => {
6 | if (currentPage > 1) {
7 | onPageChange(currentPage - 1);
8 | }
9 | };
10 |
11 | const goToNextPage = () => {
12 | if (currentPage < totalPages) {
13 | onPageChange(currentPage + 1);
14 | }
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 | Page {currentPage} of {totalPages}
22 |
23 |
30 |
31 |
32 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default Pagination;
47 |
--------------------------------------------------------------------------------
/client/src/context/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState, useEffect, useContext } from "react";
2 |
3 | const ThemeContext = createContext();
4 |
5 | export const ThemeProvider = ({ children }) => {
6 | const [theme, setTheme] = useState(
7 | () => localStorage.getItem("theme") || "light"
8 | );
9 |
10 | const toggleTheme = () => {
11 | const newTheme = theme === "light" ? "dark" : "light";
12 | setTheme(newTheme);
13 | localStorage.setItem("theme", newTheme);
14 | };
15 |
16 | useEffect(() => {
17 | document.documentElement.classList.toggle("dark", theme === "dark");
18 | }, [theme]);
19 |
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | };
26 |
27 | export const useTheme = () => useContext(ThemeContext);
28 |
--------------------------------------------------------------------------------
/client/src/hooks/useDebounce.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | Custom hook for debounce user input
4 | */
5 |
6 | import { useState, useEffect } from "react";
7 |
8 | export const useDebounce = (value, delay = 500) => {
9 | const [debouncedValue, setDebouncedValue] = useState(value);
10 |
11 | useEffect(() => {
12 | const handler = setTimeout(() => {
13 | setDebouncedValue(value);
14 | }, delay);
15 |
16 | return () => {
17 | clearTimeout(handler);
18 | };
19 | }, [value, delay]);
20 |
21 | return debouncedValue;
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import RootApp from "./App.jsx";
3 | import store from "./store/index.js";
4 | import { Provider } from "react-redux";
5 | import { createRoot } from "react-dom/client";
6 | import { BrowserRouter } from "react-router-dom";
7 |
8 | createRoot(document.getElementById("hrms")).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/client/src/pages/attendance/MarkAttendance.jsx:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { Helmet } from "react-helmet";
3 | import { formatDate } from "../../utils";
4 | import { useState, useEffect } from "react";
5 | import { useDispatch, useSelector } from "react-redux";
6 | import {
7 | generateQRCodeForAttendance,
8 | markAttendanceUsingQrCode,
9 | } from "../../services/attendance.service";
10 | import axiosInstance from "../../axios/axiosInstance";
11 |
12 | const MarkAttendance = () => {
13 | const dispatch = useDispatch();
14 |
15 | const [currentDate, setCurrentDate] = useState(null);
16 |
17 | const { loading, qrcode } = useSelector((state) => state.attendance);
18 |
19 | const handleQrCodeGeneration = () => {
20 | navigator.geolocation.getCurrentPosition(
21 | async (position) => {
22 | const latitude = position.coords.latitude;
23 | const longitude = position.coords.longitude;
24 | dispatch(generateQRCodeForAttendance({ latitude, longitude }));
25 | },
26 | (error) => {
27 | toast.error("Error getting location:", error.message);
28 | }
29 | );
30 | };
31 |
32 | const handleMarkAttendanceUsingQr = () => {
33 | dispatch(
34 | markAttendanceUsingQrCode({
35 | dispatch,
36 | qrcode,
37 | })
38 | );
39 | };
40 |
41 | useEffect(() => {
42 | async function getDateFromAPI() {
43 | const { data } = await axiosInstance.get("/insights/date");
44 | console.log(data);
45 | setCurrentDate(data.datetime);
46 | }
47 | getDateFromAPI();
48 | }, []);
49 |
50 | return (
51 | <>
52 |
53 | Mark Attendance - Metro HR
54 |
55 |
56 |
57 |
58 | {qrcode ? (
59 |
60 |
61 |
66 |
71 | {loading ? (
72 |
73 | ) : (
74 | <>
75 |
76 | Mark attendance for {formatDate(new Date())}
77 | >
78 | )}
79 |
80 | {loading && (
81 |
84 | )}
85 |
86 |
87 | ) : (
88 |
93 | {loading ? (
94 |
95 | Marking
96 |
97 |
98 | ) : (
99 | <>
100 |
101 | Generate QR code for{" "}
102 | {currentDate ? formatDate(currentDate) : "loading..."}
103 | >
104 | )}
105 |
106 | )}
107 |
108 |
109 | >
110 | );
111 | };
112 |
113 | export default MarkAttendance;
114 |
--------------------------------------------------------------------------------
/client/src/pages/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Helmet } from "react-helmet";
2 | import { useSelector } from "react-redux";
3 | import InfoCard from "../../components/shared/cards/InfoCard";
4 | import LineChart from "../../components/shared/charts/LineChart";
5 | import FetchError from "../../components/shared/error/FetchError";
6 | import ComponentLoader from "../../components/shared/loaders/ComponentLoader";
7 |
8 | const Home = () => {
9 | const { employeeInsights, loading, error } = useSelector(
10 | (state) => state.insight
11 | );
12 |
13 | const attendanceByMonth = employeeInsights?.attendanceRecord?.map((item) => {
14 | return item.attendancePercentage;
15 | });
16 |
17 | const employeeInsightsData = [
18 | { title: "Leaves Taken", stats: employeeInsights?.leavesTaken },
19 | { title: "Leave Balance", stats: employeeInsights?.leaveBalance },
20 | { title: "Feedbacks", stats: employeeInsights?.feedbackSubmitted },
21 | { title: "Complaintss", stats: employeeInsights?.complaintResolved },
22 | { title: "KPI Score", stats: `${employeeInsights?.kpiScore}%` },
23 | {
24 | title: "Attendance",
25 | stats: `${employeeInsights?.attendancePercentage}%`,
26 | },
27 | ];
28 |
29 | if (error) return ;
30 | if (loading || !employeeInsights) return ;
31 |
32 | return (
33 | <>
34 |
35 | Dashbbard - Metro HR
36 |
37 |
38 |
39 |
40 | {employeeInsightsData.map((report, index) => (
41 |
42 | ))}
43 |
44 |
45 |
46 |
50 |
51 | Overall Attendance Overview
52 |
53 |
54 |
59 |
60 |
61 |
62 |
63 | >
64 | );
65 | };
66 |
67 | export default Home;
68 |
--------------------------------------------------------------------------------
/client/src/reducers/complaint.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import {
3 | createComplaint,
4 | getComplaints,
5 | respondToComplaintRequest,
6 | } from "../services/complaint.service";
7 |
8 | const initialState = {
9 | complaints: [],
10 | loading: false,
11 | error: null,
12 | pagination: null,
13 | fetch: true,
14 | };
15 |
16 | const complaintsSlice = createSlice({
17 | name: "complaints",
18 | initialState,
19 | reducers: {
20 | setFetchFlag: (state, action) => {
21 | state.fetch = action.payload;
22 | },
23 | },
24 | extraReducers: (builder) => {
25 | // Handle getComplaints action
26 | builder
27 | .addCase(getComplaints.pending, (state) => {
28 | state.loading = true;
29 | state.error = null;
30 | })
31 | .addCase(getComplaints.fulfilled, (state, action) => {
32 | state.fetch = false;
33 | state.loading = false;
34 | state.complaints = action.payload.complaint;
35 | state.pagination = action.payload.pagination;
36 | })
37 | .addCase(getComplaints.rejected, (state, action) => {
38 | state.loading = false;
39 | state.error = action.payload;
40 | })
41 |
42 | .addCase(respondToComplaintRequest.pending, (state) => {
43 | state.loading = true;
44 | state.error = null;
45 | })
46 | .addCase(respondToComplaintRequest.fulfilled, (state, action) => {
47 | state.loading = false;
48 |
49 | const updatedComplaint = action.payload;
50 |
51 | state.complaints = state.complaints.filter(
52 | (complaint) => complaint._id !== updatedComplaint._id
53 | );
54 | })
55 |
56 | .addCase(respondToComplaintRequest.rejected, (state, action) => {
57 | state.loading = false;
58 | state.error = action.payload;
59 | });
60 |
61 | // Handle createComplaint action
62 | builder
63 | .addCase(createComplaint.pending, (state) => {
64 | state.loading = true;
65 | state.error = null;
66 | })
67 | .addCase(createComplaint.fulfilled, (state, action) => {
68 | state.loading = false;
69 | state.complaints.push(action.payload);
70 | })
71 | .addCase(createComplaint.rejected, (state, action) => {
72 | state.loading = false;
73 | state.error = action.payload;
74 | });
75 | },
76 | });
77 |
78 | export const { setFetchFlag } = complaintsSlice.actions;
79 | export default complaintsSlice.reducer;
80 |
--------------------------------------------------------------------------------
/client/src/reducers/department.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import {
3 | getDepartments,
4 | createDepartment,
5 | updateDepartment,
6 | getAllEmployeesForHead,
7 | } from "../services/department.service";
8 |
9 | const initialState = {
10 | departments: [],
11 | heads: [],
12 | loading: false,
13 | error: null,
14 | };
15 |
16 | const departmentSlice = createSlice({
17 | name: "department",
18 | initialState,
19 | reducers: {},
20 | extraReducers: (builder) => {
21 | builder
22 | // Handling the getDepartments action
23 | .addCase(getDepartments.pending, (state) => {
24 | state.loading = true;
25 | state.error = null;
26 | })
27 | .addCase(getDepartments.fulfilled, (state, action) => {
28 | state.departments = action.payload;
29 | state.loading = false;
30 | })
31 | .addCase(getDepartments.rejected, (state, action) => {
32 | state.loading = false;
33 | state.error = action.payload;
34 | })
35 |
36 | // Handling the getAllEmployeesForHead action
37 | .addCase(getAllEmployeesForHead.pending, (state) => {
38 | state.loading = true;
39 | state.error = null;
40 | })
41 | .addCase(getAllEmployeesForHead.fulfilled, (state, action) => {
42 | state.heads = action.payload;
43 | state.loading = false;
44 | })
45 | .addCase(getAllEmployeesForHead.rejected, (state, action) => {
46 | state.loading = false;
47 | state.error = action.payload;
48 | })
49 |
50 | // Handling the updateDepartment action
51 | .addCase(updateDepartment.pending, (state) => {
52 | state.loading = true;
53 | state.error = null;
54 | })
55 | .addCase(updateDepartment.fulfilled, (state, action) => {
56 | const updatedDepartments = [...state.departments];
57 | const findIndex = updatedDepartments.findIndex(
58 | (department) => department._id === action.payload._id
59 | );
60 | if (findIndex !== -1) {
61 | updatedDepartments[findIndex] = action.payload;
62 | state.departments = updatedDepartments;
63 | }
64 | state.loading = false;
65 | })
66 | .addCase(updateDepartment.rejected, (state, action) => {
67 | state.loading = false;
68 | state.error = action.payload;
69 | })
70 |
71 | // Handling the createDepartment action
72 | .addCase(createDepartment.pending, (state) => {
73 | state.loading = true;
74 | state.error = null;
75 | })
76 | .addCase(createDepartment.fulfilled, (state, action) => {
77 | state.departments = [...state.departments, action.payload];
78 | state.loading = false;
79 | })
80 | .addCase(createDepartment.rejected, (state, action) => {
81 | state.loading = false;
82 | state.error = action.payload;
83 | });
84 | },
85 | });
86 |
87 | export default departmentSlice.reducer;
88 |
--------------------------------------------------------------------------------
/client/src/reducers/employee.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import {
3 | addEmployee,
4 | editEmployee,
5 | deleteEmployee,
6 | getAllEmployees,
7 | getEmployeeById,
8 | bulkUploadEmployees,
9 | } from "../services/employee.service";
10 |
11 | const initialState = {
12 | employees: [],
13 | pagination: null,
14 | employee: null,
15 | loading: false,
16 | formLoading: false,
17 | error: null,
18 | fetch: true,
19 | };
20 |
21 | const employeeSlice = createSlice({
22 | name: "employee",
23 | initialState,
24 | reducers: {
25 | setFetchFlag: (state, action) => {
26 | state.fetch = action.payload;
27 | },
28 | },
29 | extraReducers: (builder) => {
30 | builder
31 | // Handle getAllEmployees async action
32 | .addCase(getAllEmployees.pending, (state) => {
33 | state.loading = true;
34 | state.error = null;
35 | })
36 | .addCase(getAllEmployees.fulfilled, (state, action) => {
37 | state.loading = false;
38 | state.fetch = false;
39 | state.employees = action.payload.employees;
40 | state.pagination = action.payload.pagination;
41 | })
42 | .addCase(getAllEmployees.rejected, (state, action) => {
43 | state.loading = false;
44 | state.error = action.payload;
45 | })
46 |
47 | // Handle getEmployeeById async action
48 | .addCase(getEmployeeById.pending, (state) => {
49 | state.loading = true;
50 | state.error = null;
51 | })
52 | .addCase(getEmployeeById.fulfilled, (state, action) => {
53 | state.loading = false;
54 | state.employee = action.payload;
55 | })
56 | .addCase(getEmployeeById.rejected, (state, action) => {
57 | state.loading = false;
58 | state.error = action.payload;
59 | })
60 |
61 | // Handle addEmployee async action
62 | .addCase(addEmployee.pending, (state) => {
63 | state.formLoading = true;
64 | state.error = null;
65 | })
66 | .addCase(addEmployee.fulfilled, (state, action) => {
67 | state.formLoading = false;
68 | state.employees.push(action.payload);
69 | })
70 | .addCase(addEmployee.rejected, (state, action) => {
71 | state.formLoading = false;
72 | state.error = action.payload;
73 | })
74 |
75 | // Handle bulkUploadEmployee async action
76 | .addCase(bulkUploadEmployees.pending, (state) => {
77 | state.loading = true;
78 | state.error = null;
79 | })
80 | .addCase(bulkUploadEmployees.fulfilled, (state, action) => {
81 | state.employees = [...state.employees, ...action.payload];
82 | state.loading = false;
83 | })
84 | .addCase(bulkUploadEmployees.rejected, (state, action) => {
85 | state.loading = false;
86 | state.error = action.payload;
87 | })
88 |
89 | // Handle editEmployee async action
90 | .addCase(editEmployee.pending, (state) => {
91 | state.formLoading = true;
92 | state.error = null;
93 | })
94 | .addCase(editEmployee.fulfilled, (state, action) => {
95 | state.formLoading = false;
96 | const index = state.employees.findIndex(
97 | (employee) => employee._id === action.payload._id
98 | );
99 | if (index !== -1) {
100 | state.employees[index] = action.payload;
101 | }
102 | })
103 | .addCase(editEmployee.rejected, (state, action) => {
104 | state.formLoading = false;
105 | state.error = action.payload;
106 | })
107 |
108 | // Handle deleteEmployee async action
109 | .addCase(deleteEmployee.pending, (state) => {
110 | state.loading = true;
111 | state.error = null;
112 | })
113 | .addCase(deleteEmployee.fulfilled, (state, action) => {
114 | state.loading = false;
115 | state.employees = state.employees.filter(
116 | (employee) => employee._id !== action.payload
117 | );
118 | })
119 | .addCase(deleteEmployee.rejected, (state, action) => {
120 | state.loading = false;
121 | state.error = action.payload;
122 | });
123 | },
124 | });
125 |
126 | export const { setFetchFlag } = employeeSlice.actions;
127 | export default employeeSlice.reducer;
128 |
--------------------------------------------------------------------------------
/client/src/reducers/feedback.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { createFeedback, getFeedbacks } from "../services/feedback.service";
3 |
4 | const initialState = {
5 | feedbacks: [],
6 | pagination: null,
7 | loading: false,
8 | error: null,
9 | fetch: true,
10 | };
11 |
12 | const feedbackSlice = createSlice({
13 | name: "feedback",
14 | initialState,
15 | reducers: {
16 | setFetchFlag: (state, action) => {
17 | state.fetch = action.payload;
18 | },
19 | },
20 | extraReducers: (builder) => {
21 | builder
22 | // Handling getFeedbacks action
23 | .addCase(getFeedbacks.pending, (state) => {
24 | state.loading = true;
25 | state.error = null;
26 | })
27 | .addCase(getFeedbacks.fulfilled, (state, action) => {
28 | state.feedbacks = action.payload.feedback;
29 | state.pagination = action.payload.pagination;
30 | state.loading = false;
31 | state.fetch = false;
32 | })
33 | .addCase(getFeedbacks.rejected, (state, action) => {
34 | state.loading = false;
35 | state.error = action.payload;
36 | })
37 |
38 | // Handling createFeedback action
39 | .addCase(createFeedback.pending, (state) => {
40 | state.loading = true;
41 | state.error = null;
42 | })
43 | .addCase(createFeedback.fulfilled, (state) => {
44 | state.loading = false;
45 | state.fetch = true;
46 | })
47 | .addCase(createFeedback.rejected, (state, action) => {
48 | state.loading = false;
49 | state.error = action.payload;
50 | });
51 | },
52 | });
53 |
54 | export const { setFetchFlag } = feedbackSlice.actions;
55 | export default feedbackSlice.reducer;
56 |
--------------------------------------------------------------------------------
/client/src/reducers/inshights.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { getEmployeeInsights, getInsights } from "../services/insights.service";
3 |
4 | const initialState = {
5 | insights: null,
6 | employeeInsights: null,
7 | loading: false,
8 | error: null,
9 | };
10 |
11 | const insightSlice = createSlice({
12 | name: "insight",
13 | initialState,
14 | reducers: {},
15 | extraReducers: (builder) => {
16 | builder
17 | // Handling the getInsights action
18 | .addCase(getInsights.pending, (state) => {
19 | state.loading = true;
20 | state.error = null;
21 | })
22 | .addCase(getInsights.fulfilled, (state, action) => {
23 | state.loading = false;
24 | state.insights = action.payload;
25 | })
26 | .addCase(getInsights.rejected, (state, action) => {
27 | state.loading = false;
28 | state.error = action.payload;
29 | })
30 |
31 | // Handling the getEmployeeInsights action
32 | .addCase(getEmployeeInsights.pending, (state) => {
33 | state.loading = true;
34 | state.error = null;
35 | })
36 | .addCase(getEmployeeInsights.fulfilled, (state, action) => {
37 | state.loading = false;
38 | state.employeeInsights = action.payload;
39 | })
40 | .addCase(getEmployeeInsights.rejected, (state, action) => {
41 | state.loading = false;
42 | state.error = action.payload;
43 | });
44 | },
45 | });
46 |
47 | export default insightSlice.reducer;
48 |
--------------------------------------------------------------------------------
/client/src/reducers/leave.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import {
3 | createLeave,
4 | assignSustitute,
5 | getLeavesByStatus,
6 | getEmployeesOnLeave,
7 | respondToLeaveRequest,
8 | } from "../services/leave.service";
9 |
10 | const initialState = {
11 | leaves: [],
12 | employeesOnLeaveToday: [],
13 | loading: false,
14 | error: null,
15 | fetch: true,
16 | };
17 |
18 | const leavesSlice = createSlice({
19 | name: "leave",
20 | initialState,
21 | reducers: {
22 | setFetchFlag: (state, action) => {
23 | state.fetch = action.payload;
24 | },
25 | },
26 | extraReducers: (builder) => {
27 | builder
28 | // Handling getLeavesByStatus action
29 | .addCase(getLeavesByStatus.pending, (state) => {
30 | state.loading = true;
31 | state.error = null;
32 | })
33 | .addCase(getLeavesByStatus.fulfilled, (state, action) => {
34 | state.fetch = false;
35 | state.loading = false;
36 | state.leaves = action.payload;
37 | })
38 | .addCase(getLeavesByStatus.rejected, (state, action) => {
39 | state.loading = false;
40 | state.error = action.payload;
41 | })
42 |
43 | // Handling getEmployeesOnLeave action
44 | .addCase(getEmployeesOnLeave.pending, (state) => {
45 | state.loading = true;
46 | state.error = null;
47 | })
48 | .addCase(getEmployeesOnLeave.fulfilled, (state, action) => {
49 | state.employeesOnLeaveToday = action.payload;
50 | state.loading = false;
51 | })
52 | .addCase(getEmployeesOnLeave.rejected, (state, action) => {
53 | state.loading = false;
54 | state.error = action.payload;
55 | })
56 |
57 | // Handling respondToLeaveRequest action
58 | .addCase(respondToLeaveRequest.pending, (state) => {
59 | state.loading = true;
60 | state.error = null;
61 | })
62 | .addCase(respondToLeaveRequest.fulfilled, (state, action) => {
63 | const updatedLeave = action.payload;
64 |
65 | state.leaves = state.leaves.filter(
66 | (leave) => leave._id !== updatedLeave._id
67 | );
68 |
69 | state.loading = false;
70 | })
71 |
72 | .addCase(respondToLeaveRequest.rejected, (state, action) => {
73 | state.loading = false;
74 | state.error = action.payload;
75 | })
76 |
77 | // Handling createLeave action
78 | .addCase(createLeave.pending, (state) => {
79 | state.loading = true;
80 | state.error = null;
81 | })
82 | .addCase(createLeave.fulfilled, (state, action) => {
83 | state.leaves.push(action.payload);
84 | state.loading = false;
85 | })
86 | .addCase(createLeave.rejected, (state, action) => {
87 | state.loading = false;
88 | state.error = action.payload;
89 | })
90 |
91 | // Handling assignSubstitute action
92 | .addCase(assignSustitute.pending, (state) => {
93 | state.loading = true;
94 | state.error = null;
95 | })
96 | .addCase(assignSustitute.fulfilled, (state, action) => {
97 | const index = state.employeesOnLeaveToday.findIndex(
98 | (emp) => emp._id === action.payload._id
99 | );
100 |
101 | if (index !== -1) {
102 | state.employeesOnLeaveToday[index] = action.payload;
103 | }
104 | state.loading = false;
105 | })
106 |
107 | .addCase(assignSustitute.rejected, (state, action) => {
108 | state.loading = false;
109 | state.error = action.payload;
110 | });
111 | },
112 | });
113 |
114 | export const { setFetchFlag } = leavesSlice.actions;
115 | export default leavesSlice.reducer;
116 |
--------------------------------------------------------------------------------
/client/src/reducers/payroll.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import {
3 | markAsPaid,
4 | updatePayroll,
5 | getAllPayrolls,
6 | } from "../services/payroll.service";
7 |
8 | const initialState = {
9 | payrolls: [],
10 | pagination: null,
11 | loading: false,
12 | error: null,
13 | fetch: true,
14 | };
15 |
16 | const payrollSlice = createSlice({
17 | name: "payroll",
18 | initialState,
19 | reducers: {
20 | setFetchFlag: (state, action) => {
21 | state.fetch = action.payload;
22 | },
23 | },
24 | extraReducers: (builder) => {
25 | builder
26 | // Handling the getAllPayrolls action
27 | .addCase(getAllPayrolls.pending, (state) => {
28 | state.loading = true;
29 | state.error = null;
30 | })
31 | .addCase(getAllPayrolls.fulfilled, (state, action) => {
32 | state.fetch = false;
33 | state.loading = false;
34 | state.payrolls = action.payload.payrolls;
35 | state.pagination = action.payload.pagination;
36 | })
37 | .addCase(getAllPayrolls.rejected, (state, action) => {
38 | state.loading = false;
39 | state.error = action.payload;
40 | })
41 |
42 | // Handling the markAsPaid action
43 | .addCase(markAsPaid.pending, (state) => {
44 | state.loading = true;
45 | state.error = null;
46 | })
47 | .addCase(markAsPaid.fulfilled, (state, action) => {
48 | const index = state.payrolls.findIndex(
49 | (payroll) => payroll._id === action.payload._id
50 | );
51 |
52 | if (index !== -1) {
53 | state.payrolls[index] = action.payload;
54 | }
55 | state.loading = false;
56 | })
57 | .addCase(markAsPaid.rejected, (state, action) => {
58 | state.loading = false;
59 | state.error = action.payload;
60 | })
61 |
62 | // Handling the updatePayroll action
63 | .addCase(updatePayroll.pending, (state) => {
64 | state.loading = true;
65 | state.error = null;
66 | })
67 | .addCase(updatePayroll.fulfilled, (state, action) => {
68 | const index = state.payrolls.findIndex(
69 | (payroll) => payroll._id === action.payload._id
70 | );
71 |
72 | if (index !== -1) {
73 | state.payrolls[index] = action.payload;
74 | }
75 | state.loading = false;
76 | })
77 | .addCase(updatePayroll.rejected, (state, action) => {
78 | state.loading = false;
79 | state.error = action.payload;
80 | });
81 | },
82 | });
83 |
84 | export const { setFetchFlag } = payrollSlice.actions;
85 | export default payrollSlice.reducer;
86 |
--------------------------------------------------------------------------------
/client/src/reducers/performance.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import {
3 | getPerformances,
4 | updatePerformance,
5 | } from "../services/performance.service";
6 |
7 | const initialState = {
8 | performances: [],
9 | pagination: null,
10 | loading: false,
11 | error: null,
12 | fetch: true,
13 | };
14 |
15 | const performanceSlice = createSlice({
16 | name: "performance",
17 | initialState,
18 | reducers: {
19 | setFetchFlag: (state, action) => {
20 | state.fetch = action.payload;
21 | },
22 | },
23 | extraReducers: (builder) => {
24 | builder
25 | // Handling the getPerformances action
26 | .addCase(getPerformances.pending, (state) => {
27 | state.loading = true;
28 | state.error = null;
29 | })
30 | .addCase(getPerformances.fulfilled, (state, action) => {
31 | state.performances = action.payload.performances;
32 | state.pagination = action.payload.pagination;
33 | state.loading = false;
34 | state.fetch = false;
35 | })
36 | .addCase(getPerformances.rejected, (state, action) => {
37 | state.loading = false;
38 | state.error = action.payload;
39 | })
40 |
41 | // Handling the updatePerformance action
42 | .addCase(updatePerformance.pending, (state) => {
43 | state.loading = true;
44 | state.error = null;
45 | })
46 | .addCase(updatePerformance.fulfilled, (state, action) => {
47 | const index = state.performances.findIndex(
48 | (performance) => performance._id === action.payload._id
49 | );
50 |
51 | if (index !== -1) {
52 | state.performances[index] = action.payload;
53 | }
54 | state.loading = false;
55 | })
56 | .addCase(updatePerformance.rejected, (state, action) => {
57 | state.loading = false;
58 | state.error = action.payload;
59 | });
60 | },
61 | });
62 |
63 | export const { setFetchFlag } = performanceSlice.actions;
64 | export default performanceSlice.reducer;
65 |
--------------------------------------------------------------------------------
/client/src/reducers/role.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { createRole, getRoles, updateRole } from "../services/role.service";
3 |
4 | const initialState = {
5 | roles: [],
6 | loading: false,
7 | error: null,
8 | };
9 |
10 | const roleSlice = createSlice({
11 | name: "role",
12 | initialState,
13 | reducers: {},
14 | extraReducers: (builder) => {
15 | builder
16 | // Handling the getRoles action
17 | .addCase(getRoles.pending, (state) => {
18 | state.loading = true;
19 | state.error = null;
20 | })
21 | .addCase(getRoles.fulfilled, (state, action) => {
22 | state.roles = action.payload;
23 | state.loading = false;
24 | })
25 | .addCase(getRoles.rejected, (state, action) => {
26 | state.loading = false;
27 | state.error = action.payload;
28 | })
29 |
30 | // Handling the updateDepartment action
31 | .addCase(updateRole.pending, (state) => {
32 | state.loading = true;
33 | state.error = null;
34 | })
35 | .addCase(updateRole.fulfilled, (state, action) => {
36 | const updatedRoles = [...state.roles];
37 | console.log(action.payload);
38 | const findIndex = updatedRoles.findIndex(
39 | (role) => role._id === action.payload._id
40 | );
41 | if (findIndex !== -1) {
42 | updatedRoles[findIndex] = action.payload;
43 | state.roles = updatedRoles;
44 | }
45 | state.loading = false;
46 | })
47 | .addCase(updateRole.rejected, (state, action) => {
48 | state.loading = false;
49 | state.error = action.payload;
50 | })
51 |
52 | // Handling the createRole action
53 | .addCase(createRole.pending, (state) => {
54 | state.loading = true;
55 | state.error = null;
56 | })
57 | .addCase(createRole.fulfilled, (state, action) => {
58 | state.roles = [...state.roles, action.payload];
59 | state.loading = false;
60 | })
61 | .addCase(createRole.rejected, (state, action) => {
62 | state.loading = false;
63 | state.error = action.payload;
64 | });
65 | },
66 | });
67 |
68 | export default roleSlice.reducer;
69 |
--------------------------------------------------------------------------------
/client/src/reducers/update.reducer.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { getUpdates } from "../services/insights.service";
3 |
4 | const initialState = {
5 | updates: [],
6 | loading: false,
7 | error: null,
8 | };
9 |
10 | const updateSlice = createSlice({
11 | name: "update",
12 | initialState,
13 | reducers: {},
14 | extraReducers: (builder) => {
15 | builder
16 | // Handling the getUpdates action
17 | .addCase(getUpdates.pending, (state) => {
18 | state.loading = true;
19 | state.error = null;
20 | })
21 | .addCase(getUpdates.fulfilled, (state, action) => {
22 | state.loading = false;
23 | state.updates = action.payload;
24 | })
25 | .addCase(getUpdates.rejected, (state, action) => {
26 | state.loading = false;
27 | state.error = action.payload;
28 | });
29 | },
30 | });
31 |
32 | export default updateSlice.reducer;
33 |
--------------------------------------------------------------------------------
/client/src/services/attendance.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import axiosInstance from "../axios/axiosInstance";
3 | import { createAsyncThunk } from "@reduxjs/toolkit";
4 | import { removeQr } from "../reducers/attendance.reducer";
5 |
6 | // Get attendance list
7 | export const getAttendanceList = createAsyncThunk(
8 | "attendance/getAttendanceList",
9 | async ({ selectedDepartment, selectedDate }, { rejectWithValue }) => {
10 | try {
11 | const queryParams = new URLSearchParams({
12 | department: selectedDepartment || "",
13 | date: selectedDate || "",
14 | }).toString();
15 |
16 | const { data } = await axiosInstance.get(`/attendance/?${queryParams}`);
17 | return data.employees;
18 | } catch (error) {
19 | toast.error(error.response.data.message);
20 | return rejectWithValue(error.response?.data.message || error.message);
21 | }
22 | }
23 | );
24 |
25 | // Get attendance data
26 | export const getEmployeeAttendanceByDepartment = createAsyncThunk(
27 | "attendance/getEmployeeAttendanceByDepartment",
28 | async ({ selectedDepartment, selectedDate }, { rejectWithValue }) => {
29 | try {
30 | const queryParams = new URLSearchParams({
31 | department: selectedDepartment || "",
32 | date: selectedDate || "",
33 | }).toString();
34 |
35 | const { data } = await axiosInstance.get(
36 | `/attendance/department/?${queryParams}`
37 | );
38 | return data.attendanceRecord;
39 | } catch (error) {
40 | return rejectWithValue(error.response?.data.message || error.message);
41 | }
42 | }
43 | );
44 |
45 | // Get monthly attendance data
46 | export const getEmployeeMonthlyAttendanceByDepartment = createAsyncThunk(
47 | "attendance/getEmployeeMonthlyAttendanceByDepartment",
48 | async ({ selectedDepartment, selectedDate }, { rejectWithValue }) => {
49 | try {
50 | const queryParams = new URLSearchParams({
51 | department: selectedDepartment || "",
52 | month: selectedDate || "",
53 | }).toString();
54 |
55 | const { data } = await axiosInstance.get(
56 | `/attendance/month/department/?${queryParams}`
57 | );
58 | return data.attendanceRecord;
59 | } catch (error) {
60 | return rejectWithValue(error.response?.data.message || error.message);
61 | }
62 | }
63 | );
64 |
65 | // Mark Attendance
66 | export const markAttendance = createAsyncThunk(
67 | "attendance/markAttendance",
68 | async (attendanceRecords, { rejectWithValue }) => {
69 | try {
70 | const { data } = await axiosInstance.post("/attendance/mark", {
71 | attendanceRecords,
72 | });
73 | toast.success(data.message);
74 | } catch (error) {
75 | toast.error(error.response?.data.message || "An error occurred.");
76 | return rejectWithValue(error.response?.data.message || error.message);
77 | }
78 | }
79 | );
80 |
81 | // Get all employees attendance
82 | export const getEmployeeAttendance = createAsyncThunk(
83 | "attendance/getEmployeeAttendance",
84 | async (_, { rejectWithValue }) => {
85 | try {
86 | const { data } = await axiosInstance.get("/attendance/employee");
87 | return data.attendance;
88 | } catch (error) {
89 | console.log(error.response?.data.message || "An error occurred.");
90 | return rejectWithValue(error.response?.data.message || error.message);
91 | }
92 | }
93 | );
94 |
95 | // Mark Attendance
96 | export const generateQRCodeForAttendance = createAsyncThunk(
97 | "attendance/generateQRCodeForAttendance",
98 | async ({ latitude, longitude }, { rejectWithValue }) => {
99 | try {
100 | const { data } = await axiosInstance.post("/attendance/generate", {
101 | latitude,
102 | longitude,
103 | });
104 | return data.qrcode;
105 | } catch (error) {
106 | toast.error(error.response?.data.message || "An error occurred.");
107 | return rejectWithValue(error.response?.data.message || error.message);
108 | }
109 | }
110 | );
111 |
112 | // Mark Attendance using QR
113 | export const markAttendanceUsingQrCode = createAsyncThunk(
114 | "attendance/markAttendanceUsingQrCode",
115 | async ({ dispatch, qrcode }, { rejectWithValue }) => {
116 | try {
117 | const { data } = await axiosInstance.post("/attendance/mark/qr", {
118 | qrcode,
119 | });
120 | if (data.success) {
121 | dispatch(removeQr());
122 | }
123 | toast.success(data.message);
124 | } catch (error) {
125 | toast.error(error.response?.data.message || "An error occurred.");
126 | return rejectWithValue(error.response?.data.message || error.message);
127 | }
128 | }
129 | );
130 |
--------------------------------------------------------------------------------
/client/src/services/authentication.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Login
6 | export const login = createAsyncThunk(
7 | "auth/loginAdmin",
8 | async (credentials, { rejectWithValue }) => {
9 | try {
10 | const { data } = await axiosInstance.post("/auth/login", credentials);
11 | if (data.remember) {
12 | console.log("Run");
13 | localStorage.setItem("session", data.token);
14 | localStorage.setItem("loggedInUser", JSON.stringify(data.user));
15 | } else {
16 | sessionStorage.setItem("session", data.token);
17 | sessionStorage.setItem("loggedInUser", JSON.stringify(data.user));
18 | }
19 | localStorage.setItem("remember", data.remember);
20 | toast.success(data.message);
21 | return data.user;
22 | } catch (error) {
23 | return rejectWithValue(error.response?.data.message || error.message);
24 | }
25 | }
26 | );
27 |
28 | // Forget Password
29 | export const forgetPassword = createAsyncThunk(
30 | "auth/forgetPassword",
31 | async (email, { rejectWithValue }) => {
32 | try {
33 | const { data } = await axiosInstance.post("/auth/forget/password", email);
34 | return data.success;
35 | } catch (error) {
36 | return rejectWithValue(error.response?.data.message);
37 | }
38 | }
39 | );
40 |
41 | // Update Password
42 | export const updatePassword = createAsyncThunk(
43 | "auth/updatePassword",
44 | async (credentials, { rejectWithValue }) => {
45 | try {
46 | const { data } = await axiosInstance.patch("/auth/password/update", {
47 | credentials,
48 | });
49 | toast.success(data.message);
50 | return data.success;
51 | } catch (error) {
52 | return rejectWithValue(error.response?.data.message);
53 | }
54 | }
55 | );
56 |
57 | // Set New Password
58 | export const resetPassword = createAsyncThunk(
59 | "auth/resetPassword",
60 | async (credentials, { rejectWithValue }) => {
61 | try {
62 | const { data } = await axiosInstance.patch(
63 | "/auth/reset/password",
64 | credentials
65 | );
66 | toast.success(data.message);
67 | return data.success;
68 | } catch (error) {
69 | return rejectWithValue(error.response?.data.message);
70 | }
71 | }
72 | );
73 |
74 | // Check Reset Password
75 | export const checkResetPasswordValidity = async (
76 | setLoading,
77 | { employeeId, forgetPasswordToken }
78 | ) => {
79 | const queryParams = new URLSearchParams({
80 | employeeId: employeeId || "",
81 | forgetPasswordToken: forgetPasswordToken || "",
82 | }).toString();
83 |
84 | setLoading(true);
85 |
86 | try {
87 | const { data } = await axiosInstance.get(
88 | `/auth/reset/password/validate?${queryParams}`
89 | );
90 |
91 | return data.success;
92 | } catch (error) {
93 | return false;
94 | } finally {
95 | setLoading(false);
96 | }
97 | };
98 |
99 | // Logout
100 | export const logout = createAsyncThunk(
101 | "auth/logout",
102 | async (_, { rejectWithValue }) => {
103 | try {
104 | const { data } = await axiosInstance.get("/auth/logout");
105 | toast.success(data.message);
106 | return data.success;
107 | } catch (error) {
108 | toast.error(error.response?.data.message || error.message);
109 | return rejectWithValue(error.response?.data.message || error.message);
110 | }
111 | }
112 | );
113 |
114 | // Logout
115 | export const logoutAll = createAsyncThunk(
116 | "auth/logoutALl",
117 | async (_, { rejectWithValue }) => {
118 | try {
119 | const { data } = await axiosInstance.get("/auth/logout/all");
120 | toast.success(data.message);
121 | return data.success;
122 | } catch (error) {
123 | toast.error(error.response?.data.message || error.message);
124 | return rejectWithValue(error.response?.data.message || error.message);
125 | }
126 | }
127 | );
128 |
--------------------------------------------------------------------------------
/client/src/services/chat.service.js:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../axios/axiosInstance";
2 |
3 | // Fetch Roles
4 | export const chatWithGemini = async (prompt, setLoading) => {
5 | try {
6 | setLoading(true);
7 | const { data } = await axiosInstance.post("/insights/chat", { prompt });
8 | return data;
9 | } catch (error) {
10 | console.error(error);
11 | return rejectWithValue(error.response?.data.message || "Failed to chat");
12 | } finally {
13 | setLoading(false);
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/services/complaint.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import axiosInstance from "../axios/axiosInstance";
3 | import { createAsyncThunk } from "@reduxjs/toolkit";
4 |
5 | // Fetch Complaints
6 | export const getComplaints = createAsyncThunk(
7 | "complaints/getComplaints",
8 | async ({ status, currentPage, limit = 10 }, { rejectWithValue }) => {
9 | try {
10 | const queryParams = new URLSearchParams({
11 | status: status || "",
12 | page: currentPage,
13 | limit: limit || "",
14 | }).toString();
15 |
16 | const { data } = await axiosInstance.get(`/complaints?${queryParams}`);
17 | return data;
18 | } catch (error) {
19 | return rejectWithValue(
20 | error.response?.data.message || "Failed to fetch complaints"
21 | );
22 | }
23 | }
24 | );
25 |
26 | // Create Compaints
27 | export const createComplaint = createAsyncThunk(
28 | "complaints/createComplaint",
29 | async (complaint, { rejectWithValue }) => {
30 | try {
31 | const { data } = await axiosInstance.post("/complaints", complaint);
32 | toast.success(data.message);
33 | return data.leave;
34 | } catch (error) {
35 | toast.error(error.response?.data.message);
36 | return rejectWithValue(
37 | error.response?.data.message || "Failed to create complaint"
38 | );
39 | }
40 | }
41 | );
42 |
43 | // Respond to Compaints's (approve or reject)
44 | export const respondToComplaintRequest = createAsyncThunk(
45 | "complaints/respondToComplaintRequest",
46 | async ({ complaintID, status, remarks }, { rejectWithValue }) => {
47 | try {
48 | const { data } = await axiosInstance.patch(`/complaints/${complaintID}`, {
49 | remarks,
50 | status,
51 | });
52 | toast.success(data.message);
53 | return data.complaint;
54 | } catch (error) {
55 | toast.error(error.response?.data.message);
56 | return rejectWithValue(
57 | error.response?.data.message || "Failed to respond to complaint"
58 | );
59 | }
60 | }
61 | );
62 |
--------------------------------------------------------------------------------
/client/src/services/department.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Fetch Departments
6 | export const getDepartments = createAsyncThunk(
7 | "department/getDepartments",
8 | async (_, { rejectWithValue }) => {
9 | try {
10 | const { data } = await axiosInstance.get(`/departments`);
11 | return data.department;
12 | } catch (error) {
13 | return rejectWithValue(
14 | error.response?.data.message || "Failed to fetch departments"
15 | );
16 | }
17 | }
18 | );
19 |
20 | // Fetch Head
21 | export const getAllEmployeesForHead = createAsyncThunk(
22 | "department/getAllEmployeesForHead",
23 | async (_, { rejectWithValue }) => {
24 | try {
25 | const { data } = await axiosInstance.get(`/departments/head`);
26 | return data.employees;
27 | } catch (error) {
28 | return rejectWithValue(
29 | error.response?.data.message || "Failed to fetch heads"
30 | );
31 | }
32 | }
33 | );
34 |
35 | // Update Department
36 | export const updateDepartment = createAsyncThunk(
37 | "department/updateDepartment",
38 | async ({ id, department }, { rejectWithValue }) => {
39 | try {
40 | const { data } = await axiosInstance.patch(
41 | `/departments/${id}`,
42 | department
43 | );
44 | toast.success(data.message);
45 | return data.department;
46 | } catch (error) {
47 | return rejectWithValue(
48 | error.response?.data.message || "Failed to update department"
49 | );
50 | }
51 | }
52 | );
53 |
54 | // Create Department
55 | export const createDepartment = createAsyncThunk(
56 | "department/createDepartment",
57 | async (department, { rejectWithValue }) => {
58 | try {
59 | const { data } = await axiosInstance.post(`/departments`, department);
60 | toast.success(data.message);
61 | return data.department;
62 | } catch (error) {
63 | return rejectWithValue(
64 | error.response?.data.message || "Failed to create department"
65 | );
66 | }
67 | }
68 | );
69 |
--------------------------------------------------------------------------------
/client/src/services/employee.service.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getToken } from "../utils";
3 | import toast from "react-hot-toast";
4 | import axiosInstance from "../axios/axiosInstance";
5 | import { createAsyncThunk } from "@reduxjs/toolkit";
6 |
7 | // Fetch all employees
8 | export const getAllEmployees = createAsyncThunk(
9 | "employee/getAllEmployees",
10 | async ({ currentPage, filters }, { rejectWithValue }) => {
11 | const { department, role, status, name } = filters;
12 |
13 | try {
14 | const queryParams = new URLSearchParams({
15 | name: name || "",
16 | role: role || "",
17 | status: status || "",
18 | department: department || "",
19 | page: currentPage,
20 | }).toString();
21 |
22 | const { data } = await axiosInstance.get(`/employees?${queryParams}`);
23 | return data;
24 | } catch (error) {
25 | return rejectWithValue(
26 | error.response?.data.message || "Failed to fecth employees"
27 | );
28 | }
29 | }
30 | );
31 |
32 | // Fetch employee by ID
33 | export const getEmployeeById = createAsyncThunk(
34 | "employee/getEmployeeById",
35 | async (id, { rejectWithValue }) => {
36 | try {
37 | const { data } = await axiosInstance.get(`/employees/${id}`);
38 | return data.employee;
39 | } catch (error) {
40 | return rejectWithValue(
41 | error.response?.data.message || "Failed to fecth employee"
42 | );
43 | }
44 | }
45 | );
46 |
47 | // Create a new employee
48 | export const addEmployee = createAsyncThunk(
49 | "employee/addEmployee",
50 | async (employee, { rejectWithValue }) => {
51 | try {
52 | console.log(employee);
53 | const { data } = await axiosInstance.post("/employees", employee);
54 | toast.success(data.message);
55 | return data.employee;
56 | } catch (error) {
57 | toast.error(error.response?.data.message || error.message);
58 | return rejectWithValue(
59 | error.response?.data.message || "Failed to create employee"
60 | );
61 | }
62 | }
63 | );
64 |
65 | // Create a new employee
66 | export const bulkUploadEmployees = createAsyncThunk(
67 | "employee/bulkUploadEmployees",
68 | async (employeesRecords, { rejectWithValue }) => {
69 | try {
70 | const { data } = await axiosInstance.post(
71 | "/employees/bulk",
72 | employeesRecords
73 | );
74 | toast.success(data.message);
75 | return data.employees;
76 | } catch (error) {
77 | toast.error(error.response?.data.message || error.message);
78 | return rejectWithValue(
79 | error.response?.data.message || "Failed to bulk upload employees"
80 | );
81 | }
82 | }
83 | );
84 |
85 | // Update an existing employee
86 | export const editEmployee = createAsyncThunk(
87 | "employee/editEmployee",
88 | async ({ id, employee }, { rejectWithValue }) => {
89 | try {
90 | const { data } = await axiosInstance.patch(`/employees/${id}`, employee);
91 | toast.success(data.message);
92 | return data.employee;
93 | } catch (error) {
94 | toast.error(error.response?.data.message || error.message);
95 | return rejectWithValue(
96 | error.response?.data.message || "Failed to update employee"
97 | );
98 | }
99 | }
100 | );
101 |
102 | // Delete an employee
103 | export const deleteEmployee = createAsyncThunk(
104 | "employee/deleteEmployee",
105 | async (id, { rejectWithValue }) => {
106 | try {
107 | const { data } = await axiosInstance.delete(`/employees/${id}`);
108 | toast.success(data.message);
109 | return id;
110 | } catch (error) {
111 | toast.error(error.response?.data.message || error.message);
112 | return rejectWithValue(
113 | error.response?.data.message || "Failed to delete employee"
114 | );
115 | }
116 | }
117 | );
118 |
119 | // Update Profile
120 | export const updateProfile = async (setProfileLoading, formData) => {
121 | try {
122 | const token = getToken();
123 | setProfileLoading(true);
124 |
125 | const { data } = await axios.patch(
126 | `${import.meta.env.VITE_URL}/employees/profile`,
127 | formData,
128 | {
129 | headers: {
130 | Authorization: `Bearer ${token}`,
131 | "Content-Type": "multipart/form-data",
132 | },
133 | }
134 | );
135 | toast.success(data.message);
136 | return data.updatedProfile;
137 | } catch (error) {
138 | toast.error(error.response?.data.message || "Failed to update profile");
139 | } finally {
140 | setProfileLoading(false);
141 | }
142 | };
143 |
--------------------------------------------------------------------------------
/client/src/services/feedback.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Fetch Feedbacks
6 | export const getFeedbacks = createAsyncThunk(
7 | "feedbacks/getFeedbacks",
8 | async ({ review, currentPage }, { rejectWithValue }) => {
9 | try {
10 | const queryParams = new URLSearchParams({
11 | page: currentPage,
12 | review: review || "",
13 | }).toString();
14 |
15 | const { data } = await axiosInstance.get(`/feedbacks?${queryParams}`);
16 | return data;
17 | } catch (error) {
18 | return rejectWithValue(
19 | error.response?.data.message || "Failed to fetch feedbacks"
20 | );
21 | }
22 | }
23 | );
24 |
25 | // Create Feedbacks
26 | export const createFeedback = createAsyncThunk(
27 | "feedbacks/createFeedback",
28 | async (feedback, { rejectWithValue }) => {
29 | try {
30 | const { data } = await axiosInstance.post("/feedbacks", feedback);
31 | toast.success(data.message);
32 | return data;
33 | } catch (error) {
34 | toast.error(error.response?.data.message);
35 | return rejectWithValue(
36 | error.response?.data.message || "Failed to create feedback"
37 | );
38 | }
39 | }
40 | );
41 |
--------------------------------------------------------------------------------
/client/src/services/insights.service.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import axiosInstance from "../axios/axiosInstance";
3 |
4 | // Fetch Admin Insight
5 | export const getInsights = createAsyncThunk(
6 | "insight/getInsights",
7 | async (_, { rejectWithValue }) => {
8 | try {
9 | const { data } = await axiosInstance.get("/insights");
10 | return data.insights;
11 | } catch (error) {
12 | return rejectWithValue(
13 | error.response?.data.message || "Failed to fetch insights"
14 | );
15 | }
16 | }
17 | );
18 |
19 | // Fetch Employee Insights
20 | export const getEmployeeInsights = createAsyncThunk(
21 | "insight/getEmployeeInsights",
22 | async (_, { rejectWithValue }) => {
23 | try {
24 | const { data } = await axiosInstance.get("/insights/employee");
25 | return data.insights;
26 | } catch (error) {
27 | return rejectWithValue(
28 | error.response?.data.message || "Failed to fetch insights"
29 | );
30 | }
31 | }
32 | );
33 |
34 | // Fetch updates
35 | export const getUpdates = createAsyncThunk(
36 | "insight/getUpdates",
37 | async (_, { rejectWithValue }) => {
38 | try {
39 | const { data } = await axiosInstance.get("/insights/updates");
40 | return data.updates;
41 | } catch (error) {
42 | return rejectWithValue(
43 | error.response?.data.message || "Failed to fetch updates"
44 | );
45 | }
46 | }
47 | );
48 |
--------------------------------------------------------------------------------
/client/src/services/leave.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Fetch Leaves by Status
6 | export const getLeavesByStatus = createAsyncThunk(
7 | "leaves/getLeavesByStatus",
8 | async (status, { rejectWithValue }) => {
9 | try {
10 | const { data } = await axiosInstance.get(`/leaves?status=${status}`);
11 | return data.leaves;
12 | } catch (error) {
13 | return rejectWithValue(
14 | error.response?.data.message || "Failed to fetch leaves"
15 | );
16 | }
17 | }
18 | );
19 |
20 | // Fetch Employees on Leave Today
21 | export const getEmployeesOnLeave = createAsyncThunk(
22 | "leaves/getEmployeesOnLeaveToday",
23 | async (date, { rejectWithValue }) => {
24 | try {
25 | const { data } = await axiosInstance.get(`/leaves/employee?date=${date}`);
26 | return data.leaves;
27 | } catch (error) {
28 | return rejectWithValue(
29 | error.response?.data.message ||
30 | "Failed to fetch employees on leave today"
31 | );
32 | }
33 | }
34 | );
35 |
36 | // Appy for Leave
37 | export const createLeave = createAsyncThunk(
38 | "leaves/createLeave",
39 | async (leave, { rejectWithValue }) => {
40 | try {
41 | const { data } = await axiosInstance.post(`/leaves`, leave);
42 | toast.success(data.message);
43 | return data.leave;
44 | } catch (error) {
45 | toast.error(error.response?.data.message);
46 | return rejectWithValue(
47 | error.response?.data.message || "Failed to respond to leave request"
48 | );
49 | }
50 | }
51 | );
52 |
53 | // Respond to Employee's Leave Request (approve or reject)
54 | export const respondToLeaveRequest = createAsyncThunk(
55 | "leaves/respondToLeaveRequest",
56 | async ({ leaveID, status, remarks }, { rejectWithValue }) => {
57 | try {
58 | const { data } = await axiosInstance.patch(`/leaves/${leaveID}`, {
59 | remarks,
60 | status,
61 | });
62 | console.log(data)
63 | toast.success(data.message);
64 | return data.leave;
65 | } catch (error) {
66 | toast.success(error.response?.data.message);
67 | return rejectWithValue(
68 | error.response?.data.message || "Failed to respond to leave request"
69 | );
70 | }
71 | }
72 | );
73 |
74 | // Assign Substitute
75 | export const assignSustitute = createAsyncThunk(
76 | "leaves/assignSustitute",
77 | async ({ leaveId, employee }, { rejectWithValue }) => {
78 | try {
79 | const { data } = await axiosInstance.patch(
80 | `/leaves/${leaveId}/substitute`,
81 | {
82 | employee,
83 | }
84 | );
85 | toast.success(data.message);
86 | return data.leave;
87 | } catch (error) {
88 | toast.success(error.response?.data.message);
89 | return rejectWithValue(
90 | error.response?.data.message || "Failed to assign sustitute"
91 | );
92 | }
93 | }
94 | );
95 |
--------------------------------------------------------------------------------
/client/src/services/payroll.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Fetch Payroll
6 | export const getAllPayrolls = createAsyncThunk(
7 | "payroll/getAllPayrolls",
8 | async ({ currentPage, month, isPaid }, { rejectWithValue }) => {
9 | try {
10 | const queryParams = new URLSearchParams({
11 | month: month,
12 | isPaid: isPaid,
13 | page: currentPage,
14 | }).toString();
15 |
16 | const { data } = await axiosInstance.get(`/payrolls?${queryParams}`);
17 | return data;
18 | } catch (error) {
19 | return rejectWithValue(
20 | error.response?.data.message || "Failed to fetch payroll"
21 | );
22 | }
23 | }
24 | );
25 |
26 | // Mark Payroll Pais
27 | export const markAsPaid = createAsyncThunk(
28 | "payroll/markAsPaid",
29 | async (id, { rejectWithValue }) => {
30 | try {
31 | const { data } = await axiosInstance.patch(`/payrolls/${id}/pay`);
32 | toast.success(data.message);
33 | return data.payroll;
34 | } catch (error) {
35 | toast.error(error.response.data.message);
36 | return rejectWithValue(
37 | error.response?.data.message || "Failed to mark payroll"
38 | );
39 | }
40 | }
41 | );
42 |
43 | // Update Payroll
44 | export const updatePayroll = createAsyncThunk(
45 | "payroll/updatePayroll",
46 | async ({ id, formData }, { rejectWithValue }) => {
47 | try {
48 | const { data } = await axiosInstance.patch(`/payrolls/${id}`, formData);
49 | toast.success(data.message);
50 | return data.payroll;
51 | } catch (error) {
52 | toast.error(error.response.data.message);
53 | return rejectWithValue(
54 | error.response?.data.message || "Failed to update payroll"
55 | );
56 | }
57 | }
58 | );
59 |
--------------------------------------------------------------------------------
/client/src/services/performance.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Fetch Performance
6 | export const getPerformances = createAsyncThunk(
7 | "performance/getPerformances",
8 | async ({ status, currentPage }, { rejectWithValue }) => {
9 | try {
10 | const queryParams = new URLSearchParams({
11 | page: currentPage,
12 | status: status || "",
13 | }).toString();
14 |
15 | const { data } = await axiosInstance.get(`/performance?${queryParams}`);
16 | return data;
17 | } catch (error) {
18 | return rejectWithValue(
19 | error.response?.data.message || "Failed to get performance records"
20 | );
21 | }
22 | }
23 | );
24 |
25 | // Fetch Performance
26 | export const updatePerformance = createAsyncThunk(
27 | "performance/updatePerformance",
28 | async ({ id, performance }, { rejectWithValue }) => {
29 | try {
30 | const { data } = await axiosInstance.patch(
31 | `/performance/${id}`,
32 | performance
33 | );
34 | toast.success(data.message);
35 | return data.performance;
36 | } catch (error) {
37 | toast.error(error.response?.data.message);
38 | return rejectWithValue(
39 | error.response?.data.message || "Failed to update performance"
40 | );
41 | }
42 | }
43 | );
44 |
--------------------------------------------------------------------------------
/client/src/services/role.service.js:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import { createAsyncThunk } from "@reduxjs/toolkit";
3 | import axiosInstance from "../axios/axiosInstance";
4 |
5 | // Fetch Roles
6 | export const getRoles = createAsyncThunk(
7 | "role/getRoles",
8 | async (_, { rejectWithValue }) => {
9 | try {
10 | const { data } = await axiosInstance.get("/roles");
11 | return data.role;
12 | } catch (error) {
13 | return rejectWithValue(
14 | error.response?.data.message || "Failed to fetch roles"
15 | );
16 | }
17 | }
18 | );
19 |
20 | // Update Role
21 | export const updateRole = createAsyncThunk(
22 | "role/updateRole",
23 | async ({ id, role }, { rejectWithValue }) => {
24 | try {
25 | const { data } = await axiosInstance.patch(`/roles/${id}`, role);
26 | toast.success(data.message);
27 | return data.role;
28 | } catch (error) {
29 | return rejectWithValue(
30 | error.response?.data.message || "Failed to update role"
31 | );
32 | }
33 | }
34 | );
35 |
36 | // Create Role
37 | export const createRole = createAsyncThunk(
38 | "role/createRole",
39 | async (role, { rejectWithValue }) => {
40 | try {
41 | const { data } = await axiosInstance.post(`/roles`, role);
42 | toast.success(data.message);
43 | return data.role;
44 | } catch (error) {
45 | return rejectWithValue(
46 | error.response?.data.message || "Failed to create role"
47 | );
48 | }
49 | }
50 | );
51 |
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore, combineReducers } from "@reduxjs/toolkit";
2 | import authentication from "../reducers/authentication.reducer";
3 | import role from "../reducers/role.reducer";
4 | import employee from "../reducers/employee.reducer";
5 | import department from "../reducers/department.reducer";
6 | import insight from "../reducers/inshights.reducer";
7 | import attendance from "../reducers/attendance.reducer";
8 | import leave from "../reducers/leave.reducer";
9 | import feedback from "../reducers/feedback.reducer";
10 | import complaint from "../reducers/complaint.reducer";
11 | import update from "../reducers/update.reducer";
12 | import performance from "../reducers/performance.reducer";
13 | import payroll from "../reducers/payroll.reducer";
14 | import recruitment from "../reducers/recruitment.reducer";
15 |
16 | // Combine reducers
17 | const rootReducer = combineReducers({
18 | role,
19 | update,
20 | leave,
21 | insight,
22 | payroll,
23 | employee,
24 | feedback,
25 | complaint,
26 | department,
27 | attendance,
28 | performance,
29 | recruitment,
30 | authentication,
31 | });
32 |
33 | // Configure the store
34 | const store = configureStore({
35 | reducer: rootReducer,
36 | middleware: (getDefaultMiddleware) =>
37 | getDefaultMiddleware({
38 | serializableCheck: false,
39 | }),
40 | devTools: import.meta.env.VITE_ENV !== "production",
41 | });
42 |
43 | export default store;
44 |
--------------------------------------------------------------------------------
/client/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import * as XLSX from "xlsx";
2 |
3 | const formatDate = (date) => {
4 | if (!date) return "";
5 | const d = new Date(date);
6 | const day = d.getDate();
7 | const month = d.toLocaleString("en-US", { month: "short" });
8 | const year = d.getFullYear();
9 | return `${day} ${month}, ${year}`;
10 | };
11 |
12 | const convertDate = (date) => {
13 | if (!date) return "";
14 | return new Date(date).toISOString().split("T")[0];
15 | };
16 |
17 | function downloadXls(data) {
18 | const ws = XLSX.utils.json_to_sheet(data);
19 | const wb = XLSX.utils.book_new();
20 | XLSX.utils.book_append_sheet(wb, ws, "Employees");
21 |
22 | XLSX.writeFile(wb, "employees.xlsx");
23 | }
24 |
25 | const getMonthAbbreviation = (month) => {
26 | const months = [
27 | "Jan",
28 | "Feb",
29 | "Mar",
30 | "Apr",
31 | "May",
32 | "Jun",
33 | "Jul",
34 | "Aug",
35 | "Sep",
36 | "Oct",
37 | "Nov",
38 | "Dec",
39 | ];
40 | return months[month - 1] || "Invalid";
41 | };
42 |
43 | const getToken = () => {
44 | const remeber = localStorage.getItem("remember") === "true";
45 |
46 | try {
47 | const token = remeber
48 | ? localStorage.getItem("session")
49 | : sessionStorage.getItem("session");
50 |
51 | return token || null;
52 | } catch (error) {
53 | console.log(error.message);
54 | return null;
55 | }
56 | };
57 |
58 | export { formatDate, downloadXls, getMonthAbbreviation, convertDate, getToken };
59 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: "class",
4 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
5 | theme: {
6 | extend: {
7 | backgroundColor: {
8 | navy: "#1F2937",
9 | primary: "#111827",
10 | secondary: "#374151",
11 | head: "#4b5563",
12 | headLight : "#2C4A77"
13 | },
14 | colors: {
15 | primary: "#e5e7eb",
16 | },
17 | borderColor: {
18 | primary: "#4b5563",
19 | secondary: "#6b7280",
20 | },
21 | },
22 | },
23 | plugins: [],
24 | };
25 |
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 8000,
9 | },
10 | })
11 |
--------------------------------------------------------------------------------
/public/hrms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ObaidBuilds/AI-HRMS/8202fd65f10700c34731aff5d889845297a6a1a2/public/hrms.png
--------------------------------------------------------------------------------
/server/.env.example:
--------------------------------------------------------------------------------
1 | PORT=
2 | JWTSECRET=
3 | SMTP_HOST=
4 | SMTP_PORT=
5 | SMTP_USER=
6 | SMTP_PASS=
7 | GEMINI=
8 | CLOUDINARY_CLOUD_NAME=
9 | CLOUDINARY_API_KEY=
10 | CLOUDINARY_API_SECRET=
11 | LATITUDE=
12 | LONGITUDE=
13 | CLIENT_URL=
14 | SERVER_URL=
15 | MONGO_URI=
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hrms",
3 | "version": "1.0.0",
4 | "description": "AI Driven Human Resource Managemen System - Final Year Project",
5 | "main": "src/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node src/index.js",
9 | "dev": "nodemon src/index.js",
10 | "setup": "node src/setup/index.js"
11 | },
12 | "license": "ISC",
13 | "author": "Obaid Ali Siddiqui",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/WhatsWrongOB/AI-HRMS"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/WhatsWrongOB/AI-HRMS/issues"
20 | },
21 | "homepage": "https://metrohrms.netlify.app",
22 | "keywords": [
23 | "HRMS",
24 | "AI",
25 | "MERN",
26 | "Human Resources",
27 | "Payroll",
28 | "Attendance"
29 | ],
30 | "dependencies": {
31 | "@google/generative-ai": "^0.21.0",
32 | "axios": "^1.7.9",
33 | "bcrypt": "^5.1.1",
34 | "body-parser": "^1.20.3",
35 | "cloudinary": "^1.41.3",
36 | "cors": "2.8.5",
37 | "dotenv": "16.4.5",
38 | "express": "4.21.1",
39 | "express-rate-limit": "^8.0.1",
40 | "geolib": "^3.3.4",
41 | "jimp": "^1.6.0",
42 | "jsonwebtoken": "^9.0.2",
43 | "jsqr": "^1.4.0",
44 | "mongoose": "^8.7.3",
45 | "multer": "^1.4.5-lts.1",
46 | "multer-storage-cloudinary": "^4.0.0",
47 | "node-cache": "^5.1.2",
48 | "nodemailer": "^6.9.16",
49 | "nodemon": "^3.1.9",
50 | "qrcode": "^1.5.4",
51 | "socket.io": "^4.8.1",
52 | "streamifier": "^0.1.1",
53 | "swagger-jsdoc": "^6.2.8",
54 | "swagger-ui-express": "^5.0.1",
55 | "uuid": "^11.1.0",
56 | "uuidv4": "^6.2.13"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/config/index.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import multer from "multer";
3 | import mongoose from "mongoose";
4 | import cloudinary from "cloudinary";
5 | import { v4 as uuidv4 } from "uuid";
6 | import { CloudinaryStorage } from "multer-storage-cloudinary";
7 |
8 | // Database Connection Utilities
9 | const connectDB = async () => {
10 | try {
11 | const connection = await mongoose.connect(process.env.MONGO_URI, {
12 | serverSelectionTimeoutMS: 5000,
13 | socketTimeoutMS: 45000,
14 | });
15 | console.log(`MongoDB Connected: ${connection.connection.host}`);
16 | } catch (error) {
17 | console.error(`MongoDB Connection Error: ${error.message}`);
18 | process.exit(1);
19 | }
20 | };
21 |
22 | const disConnectDB = async () => {
23 | try {
24 | await mongoose.disconnect();
25 | console.log("MongoDB Disconnected");
26 | } catch (error) {
27 | console.error(`MongoDB Disconnection Error: ${error.message}`);
28 | process.exit(1);
29 | }
30 | };
31 |
32 | // File Upload Configurations
33 | const createImageStorage = () => {
34 | return new CloudinaryStorage({
35 | cloudinary: cloudinary.v2,
36 | params: {
37 | folder: "uploads",
38 | allowed_formats: ["jpg", "png", "jpeg", "svg"],
39 | transformation: [{ quality: "auto", fetch_format: "auto" }],
40 | max_file_size: 2097152,
41 | },
42 | });
43 | };
44 |
45 | const createResumeStorage = () => {
46 | return new CloudinaryStorage({
47 | cloudinary: cloudinary.v2,
48 | params: (req, file) => {
49 | const allowedExtensions = [".pdf", ".doc", ".docx"];
50 | const fileExt = path.extname(file.originalname).toLowerCase();
51 |
52 | if (!allowedExtensions.includes(fileExt)) {
53 | throw new Error("Invalid file type");
54 | }
55 |
56 | const parsedName = path.parse(file.originalname);
57 | const sanitizedName = parsedName.name
58 | .replace(/\s+/g, "_")
59 | .replace(/[^a-zA-Z0-9_-]/g, "");
60 |
61 | return {
62 | folder: "resumes",
63 | resource_type: "raw",
64 | allowed_formats: ["pdf", "doc", "docx"],
65 | public_id: `${sanitizedName}_${uuidv4().substring(0, 6)}`,
66 | format: fileExt.substring(1),
67 | transformation: [
68 | {
69 | flags: "attachment:inline",
70 | quality: "auto:best",
71 | fetch_format: "auto",
72 | },
73 | ],
74 | max_file_size: 5242880,
75 | invalidate: true,
76 | type: "authenticated",
77 | disposition: "inline",
78 | };
79 | },
80 | });
81 | };
82 |
83 | // Multer Initialization with Error Handling
84 | const initializeUploader = (storage, options = {}) => {
85 | return multer({
86 | storage,
87 | limits: {
88 | fileSize: options.maxFileSize || 5242880,
89 | files: options.maxFiles || 1,
90 | },
91 | fileFilter: (req, file, cb) => {
92 | const allowedMimeTypes = options.allowedMimeTypes || [
93 | "application/pdf",
94 | "application/msword",
95 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
96 | ];
97 |
98 | if (allowedMimeTypes.includes(file.mimetype)) {
99 | cb(null, true);
100 | } else {
101 | cb(
102 | new Error(
103 | `Invalid file type. Only ${
104 | options.allowedTypes || "specified"
105 | } files are allowed`
106 | ),
107 | false
108 | );
109 | }
110 | },
111 | });
112 | };
113 |
114 | // Configure uploaders
115 | const imageStorage = createImageStorage();
116 | const resumeStorage = createResumeStorage();
117 |
118 | const upload = initializeUploader(imageStorage, {
119 | allowedMimeTypes: ["image/jpeg", "image/png", "image/svg+xml"],
120 | maxFileSize: 2097152,
121 | allowedTypes: "JPG, PNG, JPEG, SVG",
122 | });
123 |
124 | const uploadResume = initializeUploader(resumeStorage, {
125 | allowedMimeTypes: [
126 | "application/pdf",
127 | "application/msword",
128 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
129 | ],
130 | maxFileSize: 5242880,
131 | allowedTypes: "PDF, DOC, DOCX",
132 | });
133 |
134 | export { connectDB, disConnectDB, upload, uploadResume };
135 |
--------------------------------------------------------------------------------
/server/src/controllers/chatbot.controller.js:
--------------------------------------------------------------------------------
1 | import { catchErrors } from "../utils/index.js";
2 | import { getAnswerFromChatbot } from "../predictions/index.js";
3 |
4 | const answerAdminQuery = catchErrors(async (req, res) => {
5 | const { prompt } = req.body;
6 |
7 | if (!prompt) throw new Error("Please provide a query");
8 |
9 | const response = await getAnswerFromChatbot(prompt);
10 |
11 | return res.status(200).json({
12 | success: true,
13 | message: "Gemini replied successfully",
14 | response: response || "⚠️ Failed to generate response, Try again later",
15 | });
16 | });
17 |
18 | export default answerAdminQuery;
19 |
--------------------------------------------------------------------------------
/server/src/controllers/complaint.controller.js:
--------------------------------------------------------------------------------
1 | import Complaint from "../models/complaint.model.js";
2 | import { createUpdate } from "./update.controller.js";
3 | import { complaintRespond } from "../templates/index.js";
4 | import { catchErrors, myCache } from "../utils/index.js";
5 |
6 | const getComplaints = catchErrors(async (req, res) => {
7 | const { status, page = 1, limit = 12 } = req.query;
8 |
9 | const query = {};
10 |
11 | if (status) query.status = { $regex: status, $options: "i" };
12 |
13 | const pageNumber = Math.max(parseInt(page), 1);
14 | const limitNumber = Math.max(parseInt(limit), 1);
15 | const skip = (pageNumber - 1) * limitNumber;
16 |
17 | const complaint = await Complaint.find(query)
18 | .sort({ createdAt: -1 })
19 | .populate({
20 | path: "employee",
21 | select: "name employeeId department role",
22 | populate: [
23 | {
24 | path: "department",
25 | select: "name",
26 | },
27 | {
28 | path: "role",
29 | select: "name",
30 | },
31 | ],
32 | })
33 | .skip(skip)
34 | .limit(limitNumber);
35 |
36 | const totalComplaints = await Complaint.countDocuments(query);
37 | const totalPages = Math.ceil(totalComplaints / limitNumber);
38 |
39 | return res.status(200).json({
40 | success: true,
41 | message: "Complaint fetched successfully",
42 | complaint,
43 | pagination: {
44 | currentPage: pageNumber,
45 | totalPages,
46 | totalComplaints,
47 | limit: limitNumber,
48 | },
49 | });
50 | });
51 |
52 | const createComplaint = catchErrors(async (req, res) => {
53 | const employee = req.user.id;
54 | const { complainType, complaintDetails, complainSubject } = req.body;
55 |
56 | if (!employee || !complainType || !complaintDetails || !complainSubject)
57 | throw new Error("All fields are required");
58 |
59 | const complaint = await Complaint.create({
60 | employee,
61 | complainType,
62 | complainSubject,
63 | complaintDetails,
64 | });
65 |
66 | myCache.del("insights");
67 |
68 | return res.status(200).json({
69 | success: true,
70 | message: "Complaint created successfully",
71 | complaint,
72 | });
73 | });
74 |
75 | const assignComplaintForResolution = catchErrors(async (req, res) => {
76 | const { id } = req.params;
77 | const { employee } = req.body;
78 |
79 | if (!id || !employee) throw new Error("All fields are required");
80 |
81 | const updatedComplaint = await Complaint.findByIdAndUpdate(
82 | id,
83 | { assignComplaint: employee },
84 | { new: true }
85 | ).populate("assignComplaint", "name email");
86 |
87 | if (!updatedComplaint) throw new Error("Complaint not found");
88 |
89 | res.status(200).json({
90 | success: true,
91 | message: "Complaint successfully assigned for resolution.",
92 | complaint: updatedComplaint,
93 | });
94 | });
95 |
96 | const respondComplaint = catchErrors(async (req, res) => {
97 | const { id } = req.params;
98 | const { remarks, status } = req.body;
99 |
100 | if (!status || !id) throw new Error("All fields are required");
101 |
102 | const complaint = await Complaint.findById(id).populate(
103 | "employee",
104 | "name email"
105 | );
106 |
107 | if (!complaint) throw new Error("Complaint not found");
108 |
109 | if (complaint.status === "Resolved")
110 | throw new Error("Complaint already resolved");
111 |
112 | complaint.status = status;
113 |
114 | if (remarks) complaint.remarks = remarks;
115 |
116 | await complaint.save();
117 |
118 | await createUpdate({
119 | employee: complaint.employee._id,
120 | status: complaint.status,
121 | type: `Complaint - ${complaint.complainType}`,
122 | remarks: remarks || "--",
123 | });
124 |
125 | await complaintRespond({
126 | status: complaint.status,
127 | type: complaint.complainType,
128 | name: complaint.employee.name,
129 | email: complaint.employee.email,
130 | });
131 |
132 | myCache.del("insights");
133 |
134 | return res.status(200).json({
135 | success: true,
136 | message: `Complaint ${status.toLowerCase()} successfully`,
137 | complaint,
138 | });
139 | });
140 |
141 | const deleteComplaint = async (employee) => {
142 | if (!employee) throw new Error("Please provide employee Id");
143 |
144 | const complaint = await Complaint.deleteOne({ employee });
145 |
146 | if (complaint.deletedCount) return;
147 |
148 | return "Complaint deleted successfuly";
149 | };
150 |
151 | export {
152 | getComplaints,
153 | deleteComplaint,
154 | createComplaint,
155 | respondComplaint,
156 | assignComplaintForResolution,
157 | };
158 |
--------------------------------------------------------------------------------
/server/src/controllers/department.controller.js:
--------------------------------------------------------------------------------
1 | import { myCache } from "../utils/index.js";
2 | import { catchErrors } from "../utils/index.js";
3 | import Employee from "../models/employee.model.js";
4 | import Department from "../models/department.model.js";
5 |
6 | const createDepartment = catchErrors(async (req, res) => {
7 | const { name, head, description } = req.body;
8 |
9 | if (!name || !head) throw new Error("Please provide all fields");
10 |
11 | const department = await Department.create({
12 | name,
13 | head,
14 | description,
15 | });
16 |
17 | await department.populate("head", "name");
18 |
19 | myCache.del("insights");
20 | myCache.del("department");
21 |
22 | return res.status(201).json({
23 | success: true,
24 | message: "Department created successfuly",
25 | department,
26 | });
27 | });
28 |
29 | const getAllDepartments = catchErrors(async (req, res) => {
30 | const cacheKey = "department";
31 |
32 | const cachedDepartments = myCache.get(cacheKey);
33 | if (cachedDepartments) {
34 | return res.status(200).json({
35 | success: true,
36 | message: "Departments fetched successfully (from cache)",
37 | department: cachedDepartments,
38 | });
39 | }
40 |
41 | const department = await Department.find().populate("head", "name").lean();
42 |
43 | myCache.set(cacheKey, department);
44 |
45 | if (!department) throw new Error("No departments found");
46 |
47 | return res.status(200).json({
48 | success: true,
49 | message: "Departments fetched successfully",
50 | department,
51 | });
52 | });
53 |
54 | const getAllEmployeesForHead = catchErrors(async (req, res) => {
55 | const employees = await Employee.find({ status: "Active" }).select("name");
56 |
57 | if (employees.length === 0) throw new Error("No employee found");
58 |
59 | return res.status(200).json({
60 | success: true,
61 | message: "Employees fetched successfully",
62 | employees,
63 | });
64 | });
65 |
66 | const getDepartmentById = catchErrors(async (req, res) => {
67 | const { id } = req.params;
68 |
69 | if (!id) throw new Error("Please provide departmed Id");
70 |
71 | const department = await Department.findById(id).populate("head");
72 |
73 | return res.status(200).json({
74 | success: true,
75 | message: "Department fetched successfuly",
76 | department,
77 | });
78 | });
79 |
80 | const getDepartmentEmployees = catchErrors(async (req, res) => {
81 | const { id } = req.params;
82 |
83 | if (!id) throw new Error("Please provide departmed Id");
84 |
85 | const department = await Department.findById(id).populate("head", "name");
86 |
87 | return res.status(200).json({
88 | success: true,
89 | message: "Department fetched successfuly",
90 | department,
91 | });
92 | });
93 |
94 | const deleteDepartment = catchErrors(async (req, res) => {
95 | const { id } = req.params;
96 |
97 | if (!id) throw new Error("Please provide departmed Id");
98 |
99 | await Department.findByIdAndDelete(id);
100 |
101 | myCache.del("insights");
102 | myCache.del("department");
103 |
104 | return res.status(200).json({
105 | success: true,
106 | message: "Department deleted successfuly",
107 | });
108 | });
109 |
110 | const updateDepartment = catchErrors(async (req, res) => {
111 | const { id } = req.params;
112 | const { name, head, description } = req.body;
113 |
114 | if (!id) throw new Error("Please provide departmed Id");
115 |
116 | const department = await Department.findByIdAndUpdate(
117 | id,
118 | { name, head, description },
119 | { new: true }
120 | ).populate("head", "name");
121 |
122 | myCache.del("insights");
123 | myCache.del("department");
124 |
125 | return res.status(200).json({
126 | success: true,
127 | message: "Department updated successfuly",
128 | department,
129 | });
130 | });
131 |
132 | export {
133 | createDepartment,
134 | getAllDepartments,
135 | getDepartmentById,
136 | deleteDepartment,
137 | updateDepartment,
138 | getAllEmployeesForHead,
139 | getDepartmentEmployees,
140 | };
141 |
--------------------------------------------------------------------------------
/server/src/controllers/feedback.controller.js:
--------------------------------------------------------------------------------
1 | import Feedback from "../models/feedback.model.js";
2 | import { catchErrors, myCache } from "../utils/index.js";
3 | import { getSentimentAnalysis } from "../predictions/index.js";
4 |
5 | const getFeedbacks = catchErrors(async (req, res) => {
6 | const { review, page = 1, limit = 12 } = req.query;
7 |
8 | const cacheKey = `feedbacks:${review || "all"}:page${page}:limit${limit}`;
9 |
10 | const cachedData = myCache.get(cacheKey);
11 | if (cachedData) {
12 | return res.status(200).json({
13 | success: true,
14 | message: "Feedback fetched successfully (from cache)",
15 | ...cachedData,
16 | });
17 | }
18 |
19 | const query = {};
20 | if (review) query.review = { $regex: review, $options: "i" };
21 |
22 | const pageNumber = Math.max(parseInt(page), 1);
23 | const limitNumber = Math.max(parseInt(limit), 1);
24 | const skip = (pageNumber - 1) * limitNumber;
25 |
26 | const feedback = await Feedback.find(query)
27 | .populate({
28 | path: "employee",
29 | select: "name employeeId department role",
30 | populate: [
31 | {
32 | path: "department",
33 | select: "name",
34 | },
35 | {
36 | path: "role",
37 | select: "name",
38 | },
39 | ],
40 | })
41 | .skip(skip)
42 | .limit(limitNumber)
43 | .sort({ createdAt: -1 })
44 | .lean();
45 |
46 | const totalFeedbacks = await Feedback.countDocuments(query);
47 | const totalPages = Math.ceil(totalFeedbacks / limitNumber);
48 |
49 | const responseData = {
50 | feedback,
51 | pagination: {
52 | currentPage: pageNumber,
53 | totalPages,
54 | totalFeedbacks,
55 | limit: limitNumber,
56 | },
57 | };
58 |
59 | myCache.set(cacheKey, responseData);
60 |
61 | return res.status(200).json({
62 | success: true,
63 | message: "Feedback fetched successfully",
64 | ...responseData,
65 | });
66 | });
67 |
68 | const createFeedback = catchErrors(async (req, res) => {
69 | const { description, rating, suggestion } = req.body;
70 | const employee = req.user.id;
71 |
72 | if (!employee || !description || !rating)
73 | throw new Error("All fields are required");
74 |
75 | const review = await getSentimentAnalysis(description, parseInt(rating));
76 |
77 | const feedback = await Feedback.create({
78 | employee,
79 | description,
80 | rating: parseInt(rating),
81 | review,
82 | suggestion,
83 | });
84 |
85 | myCache.del("insights");
86 | const cacheKeys = myCache.keys();
87 | cacheKeys.forEach((key) => {
88 | if (key.startsWith("feedbacks:")) {
89 | myCache.del(key);
90 | }
91 | });
92 |
93 | return res.status(201).json({
94 | success: true,
95 | message: "Feedback created successfully",
96 | feedback,
97 | });
98 | });
99 |
100 | const deleteFeedback = async (employee) => {
101 | if (!employee) throw new Error("Please provide employee Id");
102 |
103 | const feedback = await Feedback.deleteOne({ employee });
104 |
105 | if (feedback.deletedCount) return;
106 |
107 | return "Feedback deleted successfuly";
108 | };
109 |
110 | export { getFeedbacks, createFeedback, deleteFeedback };
111 |
--------------------------------------------------------------------------------
/server/src/controllers/performance.controller.js:
--------------------------------------------------------------------------------
1 | import { catchErrors } from "../utils/index.js";
2 | import Performance from "../models/performance.model.js";
3 | import { calculateAverageAttendance } from "./attendance.controller.js";
4 |
5 | const addPerformanceWithKPI = async (employee) => {
6 | if (!employee) throw new Error("All fields are required");
7 |
8 | const rating = 0;
9 |
10 | const kpis = {
11 | attendance: 0,
12 | };
13 |
14 | const kpiScore = kpis.attendance * 0.5 + (rating / 5) * 100 * 0.5;
15 |
16 | await Performance.create({
17 | employee,
18 | kpis,
19 | kpiScore,
20 | rating,
21 | feedback: "",
22 | });
23 |
24 | return true;
25 | };
26 |
27 | export const updatePerformance = catchErrors(async (req, res) => {
28 | const { employeeID } = req.params;
29 | const { kpis, feedback, rating } = req.body;
30 |
31 | if (!employeeID || !kpis) throw new Error("All fields are required");
32 |
33 | const kpiScore =
34 | kpis.attendance * 0.5 +
35 | (parseInt(rating) ? (parseInt(rating) / 5) * 100 * 0.5 : 0);
36 |
37 | const performance = await Performance.findByIdAndUpdate(
38 | employeeID,
39 | {
40 | kpis,
41 | kpiScore,
42 | feedback,
43 | rating: parseInt(rating),
44 | lastUpdated: Date.now(),
45 | },
46 | { new: true }
47 | ).populate({
48 | path: "employee",
49 | select: "name employeeId role",
50 | populate: [{ path: "role", select: "name" }],
51 | });
52 |
53 | if (!performance) throw new Error("Performance not found");
54 |
55 | return res.status(200).json({
56 | success: true,
57 | message: "Performance updated successfully",
58 | performance,
59 | });
60 | });
61 |
62 | const getAllPerformances = catchErrors(async (req, res) => {
63 | const { page = 1, limit = 12, status } = req.query;
64 |
65 | const pageNumber = Math.max(parseInt(page), 1);
66 | const limitNumber = Math.max(parseInt(limit), 1);
67 | const skip = (pageNumber - 1) * limitNumber;
68 |
69 | let query = {};
70 |
71 | if (status === "good") query.kpiScore = { $gt: 75 };
72 | else if (status === "average") query.kpiScore = { $gt: 50, $lte: 75 };
73 | else if (status === "poor") query.kpiScore = { $lte: 50 };
74 |
75 | const performances = await Performance.find(query)
76 | .populate({
77 | path: "employee",
78 | select: "name employeeId role",
79 | populate: [{ path: "role", select: "name" }],
80 | })
81 | .skip(skip)
82 | .limit(limitNumber)
83 | .sort({ kpiScore: -1 });
84 |
85 | for (let performance of performances) {
86 | const attendance = await calculateAverageAttendance(
87 | performance.employee._id
88 | );
89 |
90 | performance.kpis.attendance = attendance;
91 | performance.kpiScore =
92 | attendance * 0.5 + (performance.rating / 5) * 100 * 0.5;
93 | await performance.save();
94 | }
95 |
96 | const totalPerformances = await Performance.countDocuments(query);
97 | const totalPages = Math.ceil(totalPerformances / limitNumber);
98 |
99 | return res.status(200).json({
100 | success: true,
101 | message: "Performance fetched successfully",
102 | performances,
103 | pagination: {
104 | currentPage: pageNumber,
105 | totalPages,
106 | totalPerformances,
107 | limit: limitNumber,
108 | },
109 | });
110 | });
111 |
112 | const getPerformanceMetricsById = catchErrors(async (req, res) => {
113 | const { employeeID } = req.params;
114 |
115 | if (!employeeID) throw new Error("Employee ID is required");
116 |
117 | const performance = await Performance.findOne({
118 | employee: employeeID,
119 | }).populate({
120 | path: "employee",
121 | select: "name employeeId department role",
122 | populate: [
123 | {
124 | path: "department",
125 | select: "name",
126 | },
127 | {
128 | path: "role",
129 | select: "name",
130 | },
131 | ],
132 | });
133 |
134 | if (!performance) throw new Error("No performance data found");
135 |
136 | return res.status(200).json({
137 | success: true,
138 | performance,
139 | });
140 | });
141 |
142 | const deletePerformance = async (employee) => {
143 | if (!employee) throw new Error("Please provide employee Id");
144 |
145 | const performance = await Performance.deleteOne({ employee });
146 |
147 | if (performance.deletedCount) return;
148 |
149 | return "Performance deleted successfuly";
150 | };
151 |
152 | export {
153 | deletePerformance,
154 | getAllPerformances,
155 | addPerformanceWithKPI,
156 | getPerformanceMetricsById,
157 | };
158 |
--------------------------------------------------------------------------------
/server/src/controllers/role.controller.js:
--------------------------------------------------------------------------------
1 | import Role from "../models/role.model.js";
2 | import { myCache } from "../utils/index.js";
3 | import { catchErrors } from "../utils/index.js";
4 |
5 | const createRole = catchErrors(async (req, res) => {
6 | const { name, description } = req.body;
7 |
8 | if (!name || !description) throw new Error("Please provide all fields");
9 |
10 | const role = await Role.create({ name, description });
11 |
12 | myCache.del("insights");
13 | myCache.del("role");
14 |
15 | return res.status(201).json({
16 | success: true,
17 | message: "Role created successfuly",
18 | role,
19 | });
20 | });
21 |
22 | const getAllRoles = catchErrors(async (req, res) => {
23 | const { name } = req.query;
24 |
25 | const cacheKey = name ? `role:${name.toLowerCase()}` : "role:all";
26 |
27 | const cachedRoles = myCache.get(cacheKey);
28 | if (cachedRoles) {
29 | return res.status(200).json({
30 | success: true,
31 | message: "Roles fetched successfully (from cache)",
32 | role: cachedRoles,
33 | });
34 | }
35 |
36 | const query = {};
37 | if (name) query.name = { $regex: name, $options: "i" };
38 |
39 | const roles = await Role.find(query).lean();
40 |
41 | if (!roles || roles.length === 0) {
42 | throw new Error("No roles found");
43 | }
44 |
45 | myCache.set(cacheKey, roles);
46 |
47 | return res.status(200).json({
48 | success: true,
49 | message: "Roles fetched successfully",
50 | role: roles,
51 | });
52 | });
53 |
54 | const getRoleById = catchErrors(async (req, res) => {
55 | const { id } = req.params;
56 |
57 | if (!id) throw new Error("Please provide role Id");
58 |
59 | const role = await Role.findById(id).populate({
60 | path: "department",
61 | select: "name",
62 | populate: {
63 | path: "head",
64 | select: "name",
65 | },
66 | });
67 |
68 | return res.status(201).json({
69 | success: true,
70 | message: "role fetched successfuly",
71 | role,
72 | });
73 | });
74 |
75 | const deleteRole = catchErrors(async (req, res) => {
76 | const { id } = req.params;
77 |
78 | if (!id) throw new Error("Please provide role Id");
79 |
80 | await Role.findByIdAndDelete(id);
81 |
82 | myCache.del("insights");
83 | myCache.del("department");
84 |
85 | return res.status(201).json({
86 | success: true,
87 | message: "Role deleted successfuly",
88 | });
89 | });
90 |
91 | const updateRole = catchErrors(async (req, res) => {
92 | const { id } = req.params;
93 | const { name, description } = req.body;
94 |
95 | if (!id) throw new Error("Please provide role Id");
96 |
97 | const role = await Role.findByIdAndUpdate(
98 | id,
99 | { name, description },
100 | { new: true }
101 | );
102 |
103 | myCache.del("insights");
104 | myCache.del("department");
105 |
106 | return res.status(201).json({
107 | success: true,
108 | message: "Department updated successfuly",
109 | role,
110 | });
111 | });
112 |
113 | export { createRole, getAllRoles, getRoleById, deleteRole, updateRole };
114 |
--------------------------------------------------------------------------------
/server/src/controllers/update.controller.js:
--------------------------------------------------------------------------------
1 | import Leave from "../models/leave.model.js";
2 | import { catchErrors } from "../utils/index.js";
3 | import Complaint from "../models/complaint.model.js";
4 | import Update from "../models/update.model.js";
5 |
6 | const createUpdate = async ({ employee, type, status, remarks }) => {
7 | await Update.create({ employee, type, status, remarks });
8 | };
9 |
10 | const getUpdates = catchErrors(async (req, res) => {
11 | const employee = req.user.id;
12 |
13 | const updates = await Update.find({ employee })
14 | .populate({
15 | path: "employee",
16 | select: "name employeeId department role",
17 | populate: [
18 | {
19 | path: "department",
20 | select: "name",
21 | },
22 | {
23 | path: "role",
24 | select: "name",
25 | },
26 | ],
27 | })
28 | .sort({ createdAt: -1 });
29 |
30 | return res.status(200).json({
31 | success: true,
32 | message: "Updates fetched successfully",
33 | updates,
34 | });
35 | });
36 |
37 | const getDate = catchErrors(async (req, res) => {
38 | const datetime = new Date().toISOString();
39 |
40 | return res.status(200).json({
41 | success: true,
42 | message: "Date fetched successfully",
43 | datetime,
44 | });
45 | });
46 |
47 | export { getUpdates, createUpdate, getDate };
48 |
--------------------------------------------------------------------------------
/server/src/doc/index.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv"
2 | dotenv.config();
3 |
4 | import swaggerJsdoc from "swagger-jsdoc";
5 | import swaggerUi from "swagger-ui-express";
6 |
7 |
8 | const options = {
9 | definition: {
10 | openapi: "3.0.0",
11 | info: {
12 | title: "HRMS",
13 | version: "1.0.0",
14 | description: "AI Driven Human Resource Management System",
15 | },
16 | servers: [
17 | {
18 | url: process.env.SERVER_URL,
19 | },
20 | ],
21 | },
22 | apis: ["./routes/*.js"],
23 | };
24 |
25 | const swaggerSpec = swaggerJsdoc(options);
26 |
27 | export { swaggerUi, swaggerSpec };
28 |
--------------------------------------------------------------------------------
/server/src/gemini/index.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | dotenv.config();
3 |
4 | import { GoogleGenerativeAI } from "@google/generative-ai";
5 |
6 | const genAI = new GoogleGenerativeAI(process.env.GEMINI);
7 |
8 | async function getPredictionFromGeminiAI(input) {
9 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
10 | try {
11 | const result = await model.generateContent(input);
12 | const response = result.response;
13 | const text = await response.text();
14 | return text;
15 | } catch (error) {
16 | console.log(error.message);
17 | }
18 | }
19 |
20 | export default getPredictionFromGeminiAI;
21 |
--------------------------------------------------------------------------------
/server/src/index.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | dotenv.config();
3 |
4 | import cors from "cors";
5 | import path from "path";
6 | import express from "express";
7 | import { fileURLToPath } from "url";
8 | import cloudinary from "cloudinary";
9 | import bodyParser from "body-parser";
10 | import { connectDB } from "./config/index.js";
11 | import {
12 | role,
13 | leave,
14 | payroll,
15 | employee,
16 | feedback,
17 | inshight,
18 | complaint,
19 | attendance,
20 | department,
21 | performance,
22 | recruitment,
23 | authentication,
24 | } from "./routes/index.routes.js";
25 | import { swaggerUi, swaggerSpec } from "./doc/index.js";
26 | // import {
27 | // deleteAllPayrollRecords,
28 | // generatePayrollDataForYear,
29 | // generatePayrollDataForMonths,
30 | // deleteTodayAttendanceRecords
31 | // } from "./seeders/index.js";
32 |
33 | const app = express();
34 |
35 | app.use(bodyParser.json());
36 | app.use(bodyParser.urlencoded({ extended: true }));
37 |
38 | const __filename = fileURLToPath(import.meta.url);
39 | const __dirname = path.dirname(__filename);
40 |
41 | app.use(express.static(path.join(__dirname, "public")));
42 |
43 | const allowedOrigins = [process.env.CLIENT_URL];
44 |
45 | app.use(
46 | cors({
47 | origin: (origin, callback) => {
48 | if (!origin || allowedOrigins.includes(origin)) {
49 | callback(null, true);
50 | } else {
51 | callback(new Error("Not allowed by CORS"));
52 | }
53 | },
54 | methods: "GET, POST, PUT, DELETE, PATCH, OPTIONS",
55 | credentials: true,
56 | allowedHeaders: ["Content-Type", "Authorization"],
57 | exposedHeaders: ["Set-Cookie"],
58 | })
59 | );
60 |
61 | cloudinary.v2.config({
62 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
63 | api_key: process.env.CLOUDINARY_API_KEY,
64 | api_secret: process.env.CLOUDINARY_API_SECRET,
65 | secure: true,
66 | });
67 |
68 | // deleteAllPayrollRecords()
69 | // deleteTodayAttendanceRecords()
70 | // generatePayrollDataForMonths(8)
71 | // generatePayrollDataForYear(2025)
72 |
73 | app.use("/api/roles", role);
74 | app.use("/api/leaves", leave);
75 | app.use("/api/payrolls", payroll);
76 | app.use("/api/insights", inshight);
77 | app.use("/api/employees", employee);
78 | app.use("/api/feedbacks", feedback);
79 | app.use("/api/auth", authentication);
80 | app.use("/api/complaints", complaint);
81 | app.use("/api/attendance", attendance);
82 | app.use("/api/departments", department);
83 | app.use("/api/performance", performance);
84 | app.use("/api/recruitment", recruitment);
85 |
86 | app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
87 |
88 | app.get("/", (req, res) => {
89 | res.sendFile(path.join(__dirname, "public", "welcome.html"));
90 | });
91 |
92 | const port = process.env.PORT || 3000;
93 | connectDB()
94 | .then(() => {
95 | app.listen(port, () => {
96 | console.log(`Express running → On http://localhost:${port} 🚀`);
97 | });
98 | })
99 | .catch((err) => {
100 | console.error(err.message);
101 | });
102 |
103 | app.use((req, res, next) => {
104 | const error = new Error("404 Endpoint Not Found");
105 | error.status = 404;
106 | next(error.message);
107 | });
108 |
109 | app.use((err, req, res, next) => {
110 | const message = err || "Internal server error";
111 | res.status(500).json({
112 | success: false,
113 | message,
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/server/src/middlewares/index.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import { catchErrors } from "../utils/index.js";
3 | import Session from "../models/session.model.js";
4 | import Employee from "../models/employee.model.js";
5 | import rateLimit, { ipKeyGenerator } from "express-rate-limit";
6 |
7 | const loginLimiter = rateLimit({
8 | windowMs: 1 * 60 * 1000,
9 | max: 4,
10 | keyGenerator: (req) => {
11 | const employeeId = req.body.employeeId || "guest";
12 | return ipKeyGenerator(req) + ":" + employeeId;
13 | },
14 | standardHeaders: true,
15 | legacyHeaders: false,
16 | handler: async (req, res, next, options) => {
17 | const key = options.keyGenerator(req, res);
18 | const data = await options.store.get(key);
19 |
20 | let retryAfterSec = Math.ceil(options.windowMs / 1000);
21 | if (data && data.resetTime) {
22 | retryAfterSec = Math.ceil((data.resetTime.getTime() - Date.now()) / 1000);
23 | }
24 |
25 | const minutes = Math.floor(retryAfterSec / 60);
26 | const seconds = retryAfterSec % 60;
27 |
28 | res.status(options.statusCode).json({
29 | message: `Too many login attempts. Try again after ${minutes}m ${seconds}s.`,
30 | });
31 | },
32 | });
33 |
34 | const verifyEmployeeToken = catchErrors(async (req, res, next) => {
35 | const token = req.headers.authorization?.split(" ")[1];
36 |
37 | if (!token) throw new Error("Unauthorized access");
38 |
39 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
40 |
41 | if (!decoded.employeeId) throw new Error("Unauthorized access");
42 |
43 | const session = await Session.findOne({
44 | userId: decoded.employeeId,
45 | authority: decoded.authority,
46 | token,
47 | });
48 |
49 | if (!session) throw new Error("Session expired, please login again");
50 |
51 | req.user = {
52 | id: decoded.employeeId,
53 | authority: decoded.authority,
54 | token,
55 | };
56 |
57 | next();
58 | });
59 |
60 | const verifyAdminToken = catchErrors(async (req, res, next) => {
61 | const token = req.headers.authorization?.split(" ")[1];
62 |
63 | if (!token) throw new Error("Unauthorized access");
64 |
65 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
66 |
67 | const session = await Session.findOne({
68 | userId: decoded.employeeId,
69 | authority: decoded.authority,
70 | token,
71 | });
72 |
73 | if (!session) throw new Error("Session expired, please login again");
74 |
75 | const user = await Employee.findById(decoded.employeeId);
76 |
77 | if (!user || !user.admin) throw new Error("Unauthorized access");
78 |
79 | req.user = {
80 | id: decoded.employeeId,
81 | authority: decoded.authority,
82 | token,
83 | };
84 |
85 | next();
86 | });
87 |
88 | const verifyCornJob = catchErrors(async (req, res, next) => {
89 | const token = req.headers.secret.trim();
90 |
91 | if (!token || token !== process.env.CRON_SECRET)
92 | throw new Error("Unauthorized access");
93 |
94 | next();
95 | });
96 |
97 | export { verifyEmployeeToken, verifyAdminToken, loginLimiter, verifyCornJob };
98 |
--------------------------------------------------------------------------------
/server/src/models/attendance.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const attendanceSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | required: true,
9 | },
10 | date: {
11 | type: Date,
12 | required: true,
13 | default: Date.now,
14 | },
15 | status: {
16 | type: String,
17 | enum: ["Present", "Absent"],
18 | required: true,
19 | },
20 | },
21 | {
22 | timestamps: true,
23 | }
24 | );
25 |
26 | const Attendance = mongoose.model("Attendance", attendanceSchema);
27 |
28 | export default Attendance;
29 |
--------------------------------------------------------------------------------
/server/src/models/complaint.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const complaintSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | },
9 | complainType: {
10 | type: String,
11 | required: true,
12 | enum: [
13 | "Leave",
14 | "Workplace",
15 | "Payroll",
16 | "Harassment",
17 | "Scheduling",
18 | "Misconduct",
19 | ],
20 | },
21 | complainSubject: {
22 | type: String,
23 | required: true,
24 | },
25 | complaintDetails: {
26 | type: String,
27 | required: true,
28 | },
29 | remarks: {
30 | type: String,
31 | },
32 | status: {
33 | type: String,
34 | default: "Pending",
35 | enum: ["Pending", "Resolved", "Closed"],
36 | },
37 | assignComplaint: {
38 | type: mongoose.Schema.Types.ObjectId,
39 | ref: "Employee",
40 | },
41 | },
42 | {
43 | timestamps: true,
44 | }
45 | );
46 |
47 | const Complaint = mongoose.model("Complaint", complaintSchema);
48 |
49 | export default Complaint;
50 |
--------------------------------------------------------------------------------
/server/src/models/department.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const departmentSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | unique: true,
8 | required: true,
9 | },
10 | head: {
11 | type: mongoose.Schema.Types.ObjectId,
12 | ref: "Employee",
13 | required: true,
14 | },
15 | description: {
16 | type: String,
17 | required: true,
18 | },
19 | },
20 | {
21 | timestamps: true,
22 | }
23 | );
24 |
25 | const Department = mongoose.model("Department", departmentSchema);
26 |
27 | export default Department;
28 |
--------------------------------------------------------------------------------
/server/src/models/employee.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const employeeSchema = new mongoose.Schema(
4 | {
5 | employeeId: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | },
10 | name: {
11 | type: String,
12 | required: true,
13 | },
14 | dob: {
15 | type: Date,
16 | required: true,
17 | },
18 | email: {
19 | type: String,
20 | required: true,
21 | unique: true,
22 | },
23 | password: {
24 | type: String,
25 | required: true,
26 | },
27 | profilePicture: {
28 | type: String,
29 | default: "https://metrohrms.netlify.app/unknown.jpeg",
30 | },
31 | qrCode: String,
32 | phoneNumber: {
33 | type: String,
34 | required: true,
35 | unique: true,
36 | },
37 | address: {
38 | street: String,
39 | city: String,
40 | state: String,
41 | postalCode: String,
42 | country: String,
43 | },
44 | department: {
45 | type: mongoose.Schema.Types.ObjectId,
46 | ref: "Department",
47 | required: true,
48 | },
49 | role: {
50 | type: mongoose.Schema.Types.ObjectId,
51 | ref: "Role",
52 | required: true,
53 | },
54 | dateOfJoining: {
55 | type: Date,
56 | },
57 | gender: {
58 | type: String,
59 | enum: ["Male", "Female"],
60 | required: true,
61 | },
62 | martialStatus: {
63 | type: String,
64 | enum: ["Single", "Married"],
65 | required: true,
66 | },
67 | employmentType: {
68 | type: String,
69 | enum: ["Full-Time", "Part-Time", "Contract"],
70 | required: true,
71 | },
72 | shift: {
73 | type: String,
74 | enum: ["Morning", "Evening", "Night"],
75 | required: true,
76 | },
77 | status: {
78 | type: String,
79 | enum: ["Active", "Inactive", "Leave"],
80 | default: "Active",
81 | },
82 | salary: {
83 | type: Number,
84 | required: true,
85 | },
86 | bankDetails: {
87 | accountNumber: String,
88 | bankName: String,
89 | },
90 | emergencyContact: {
91 | name: String,
92 | relationship: String,
93 | phoneNumber: String,
94 | },
95 | leaveBalance: {
96 | type: Number,
97 | default: 0,
98 | },
99 | admin: {
100 | type: Boolean,
101 | default: false,
102 | },
103 | forgetPasswordToken: String,
104 | },
105 | {
106 | timestamps: true,
107 | }
108 | );
109 |
110 | const Employee = mongoose.model("Employee", employeeSchema);
111 |
112 | export default Employee;
113 |
--------------------------------------------------------------------------------
/server/src/models/feedback.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const feedbackSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | },
9 | review: {
10 | type: String,
11 | required: true,
12 | },
13 | rating: {
14 | type: Number,
15 | required: true,
16 | },
17 | description: {
18 | type: String,
19 | required: true,
20 | },
21 | suggestion: {
22 | type: String,
23 | required: true,
24 | },
25 | },
26 | {
27 | timestamps: true,
28 | }
29 | );
30 |
31 | const Feedback = mongoose.model("Feedback", feedbackSchema);
32 |
33 | export default Feedback;
34 |
--------------------------------------------------------------------------------
/server/src/models/leave.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const leaveSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | },
9 | leaveType: {
10 | type: String,
11 | required: true,
12 | enum: ["Sick", "Casual", "Vacation", "Unpaid"],
13 | },
14 | remarks: {
15 | type: String,
16 | default: "",
17 | },
18 | substitute: {
19 | type: mongoose.Schema.Types.ObjectId,
20 | ref: "Employee",
21 | },
22 | fromDate: {
23 | type: Date,
24 | required: true,
25 | },
26 | toDate: {
27 | type: Date,
28 | required: true,
29 | },
30 | duration: {
31 | type: Number,
32 | required: true,
33 | },
34 | description: String,
35 | status: {
36 | type: String,
37 | enum: ["Pending", "Approved", "Rejected"],
38 | required: true,
39 | },
40 | },
41 | {
42 | timestamps: true,
43 | }
44 | );
45 |
46 | const Leave = mongoose.model("Leave", leaveSchema);
47 |
48 | export default Leave;
49 |
--------------------------------------------------------------------------------
/server/src/models/payroll.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const PayrollSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | required: true,
9 | },
10 | month: {
11 | type: Number,
12 | required: true,
13 | min: 1,
14 | max: 12,
15 | },
16 | year: {
17 | type: Number,
18 | required: true,
19 | },
20 | baseSalary: {
21 | type: Number,
22 | required: true,
23 | },
24 | allowances: {
25 | type: Number,
26 | default: 0,
27 | },
28 | deductions: {
29 | type: Number,
30 | default: 0,
31 | },
32 | bonuses: {
33 | type: Number,
34 | default: 0,
35 | },
36 | netSalary: {
37 | type: Number,
38 | required: true,
39 | },
40 | isPaid: {
41 | type: Boolean,
42 | default: false,
43 | },
44 | paymentDate: {
45 | type: Date,
46 | default: null,
47 | },
48 | },
49 | {
50 | timestamps: true,
51 | }
52 | );
53 |
54 | PayrollSchema.pre("save", function (next) {
55 | this.netSalary =
56 | this.baseSalary + this.allowances + this.bonuses - this.deductions;
57 | next();
58 | });
59 |
60 | const Payroll = mongoose.model("Payroll", PayrollSchema);
61 |
62 | export default Payroll;
63 |
--------------------------------------------------------------------------------
/server/src/models/performance.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const performanceSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | required: true,
9 | },
10 | kpis: {
11 | attendance: {
12 | type: Number,
13 | required: true,
14 | },
15 | },
16 | kpiScore: {
17 | type: Number,
18 | default: 0,
19 | },
20 | feedback: {
21 | type: String,
22 | },
23 | rating: {
24 | type: Number,
25 | default: 0,
26 | },
27 | lastUpdated: {
28 | type: Date,
29 | default: Date.now,
30 | },
31 | },
32 | {
33 | timestamps: true,
34 | }
35 | );
36 |
37 | const Performance = mongoose.model("Performance", performanceSchema);
38 |
39 | export default Performance;
40 |
--------------------------------------------------------------------------------
/server/src/models/recruitment.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const applicantSchema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true,
7 | },
8 | email: {
9 | type: String,
10 | required: true,
11 | },
12 | phone: String,
13 | resume: String,
14 | coverLetter: String,
15 | appliedAt: {
16 | type: Date,
17 | default: Date.now,
18 | },
19 | status: {
20 | type: String,
21 | enum: ["Applied", "Under Review", "Interview", "Rejected", "Hired"],
22 | default: "Applied",
23 | },
24 | });
25 |
26 | const jobSchema = new mongoose.Schema({
27 | title: {
28 | type: String,
29 | required: true,
30 | },
31 | department: {
32 | type: mongoose.Schema.Types.ObjectId,
33 | ref: "Department",
34 | },
35 | role: {
36 | type: mongoose.Schema.Types.ObjectId,
37 | ref: "Role",
38 | },
39 | location: String,
40 | minSalary: String,
41 | maxSalary: String,
42 | type: {
43 | type: String,
44 | enum: ["Full-time", "Part-time", "Contract"],
45 | default: "full-time",
46 | },
47 | description: String,
48 | postedBy: {
49 | type: mongoose.Schema.Types.ObjectId,
50 | ref: "Employee",
51 | },
52 | postedAt: {
53 | type: Date,
54 | default: Date.now,
55 | },
56 | deadline: Date,
57 | applicants: [applicantSchema],
58 | status: {
59 | type: String,
60 | enum: ["Open", "Closed", "Paused"],
61 | default: "Open",
62 | },
63 | });
64 |
65 | const Recruitment = mongoose.model("Job", jobSchema);
66 |
67 | export default Recruitment;
68 |
--------------------------------------------------------------------------------
/server/src/models/role.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const roleSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | },
10 | description: {
11 | type: String,
12 | required: true,
13 | },
14 | },
15 | {
16 | timestamps: true,
17 | }
18 | );
19 |
20 | const Role = mongoose.model("Role", roleSchema);
21 |
22 | export default Role;
23 |
--------------------------------------------------------------------------------
/server/src/models/session.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const sessionSchema = new mongoose.Schema({
4 | userId: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | ref: "Employee",
7 | required: true,
8 | },
9 | token: { type: String, required: true },
10 | authority: { type: String, required: true },
11 | createdAt: { type: Date, default: Date.now, expires: "7d" },
12 | });
13 |
14 | const Session = mongoose.model("Session", sessionSchema);
15 | export default Session;
16 |
--------------------------------------------------------------------------------
/server/src/models/update.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const updateSchema = new mongoose.Schema(
4 | {
5 | employee: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "Employee",
8 | required: true,
9 | },
10 | type: { type: String, required: true },
11 | status: { type: String, required: true },
12 | remarks: { type: String, required: true },
13 | },
14 | { timestamps: true }
15 | );
16 |
17 | const Update = mongoose.model("Update", updateSchema);
18 | export default Update;
19 |
--------------------------------------------------------------------------------
/server/src/predictions/index.js:
--------------------------------------------------------------------------------
1 | import getSubstitute from "./substitute.js";
2 | import getAnswerFromChatbot from "./chatbot.js";
3 | import getSentimentAnalysis from "./sentiment.js";
4 |
5 | export { getSentimentAnalysis, getAnswerFromChatbot, getSubstitute };
6 |
--------------------------------------------------------------------------------
/server/src/predictions/sentiment.js:
--------------------------------------------------------------------------------
1 | import getPredictionFromGeminiAI from "../gemini/index.js";
2 |
3 | /** Fallback sentiment analysis */
4 | function fallbackSentimentAnalysis(rating) {
5 | if (rating >= 4) return "Positive";
6 | if (rating === 3) return "Neutral";
7 | return "Negative";
8 | }
9 |
10 | async function getSentimentAnalysis(description, rating) {
11 | const prompt = `
12 | Given user feedback: "${description}"
13 | Classify sentiment as one of "Positive", "Negative", or "Neutral" (single word only).
14 | `;
15 |
16 | try {
17 | const response = await getPredictionFromGeminiAI(prompt);
18 | if (response) return response;
19 | } catch (error) {
20 | console.error("Gemini AI sentiment error:", error.message);
21 | }
22 | return fallbackSentimentAnalysis(rating);
23 | }
24 |
25 | export default getSentimentAnalysis;
26 |
--------------------------------------------------------------------------------
/server/src/predictions/substitute.js:
--------------------------------------------------------------------------------
1 | import Employee from "../models/employee.model.js";
2 | import getPredictionFromGeminiAI from "../gemini/index.js";
3 |
4 | async function getSubstitute({ department, shift }) {
5 | const shiftMapping = {
6 | Morning: ["Evening", "Night"],
7 | Evening: ["Morning", "Night"],
8 | Night: ["Morning", "Evening"],
9 | };
10 |
11 | const requiredShifts = shiftMapping[shift] || ["Morning", "Evening", "Night"];
12 |
13 | const employees = await Employee.find({
14 | status: "Active",
15 | department,
16 | shift: { $in: requiredShifts },
17 | }).sort({ leaveBalance: -1 });
18 |
19 | if (!employees.length) {
20 | return { availability: false, message: "No suitable substitute found." };
21 | }
22 |
23 | const prompt = `
24 | You are an AI assistant helping to assign a substitute employee for a shift in the department "${department}".
25 | The following employees are available, and their details are as follows:
26 | ${employees
27 | .map(
28 | (emp, index) =>
29 | `${index + 1}. Name: ${emp.name}, Email: ${
30 | emp.email
31 | }, Current Shift: ${emp.shift}, Leave Balance: ${emp.leaveBalance}`
32 | )
33 | .join("\n")}
34 |
35 | The current shift requiring a substitute is "${shift}".
36 | Your task is to suggest the most suitable substitute based on leave balance and shift compatibility.
37 | If no perfect match is found, return the least possible match.
38 |
39 | Please respond with **only** a valid JSON object in the following format:
40 | {
41 | "name": "Employee Name",
42 | "email": "employee@example.com"
43 | }`;
44 |
45 | let suggestedEmployee;
46 |
47 | try {
48 | const response = await getPredictionFromGeminiAI(prompt);
49 |
50 | const cleanedResponse = response.replace(/```json|```|\n/g, "").trim();
51 |
52 | suggestedEmployee = JSON.parse(cleanedResponse);
53 |
54 | if (!suggestedEmployee.name || !suggestedEmployee.email) {
55 | throw new Error("Incomplete JSON data from AI response.");
56 | }
57 |
58 | console.log("AI Suggested sustitute employee", suggestedEmployee);
59 | } catch (error) {
60 | console.error("Error with AI prediction or parsing:", error.message);
61 |
62 | suggestedEmployee = {
63 | name: employees[0].name,
64 | email: employees[0].email,
65 | };
66 | }
67 |
68 | return {
69 | availability: true,
70 | id: employees.find((e) => e.email === suggestedEmployee.email)?._id,
71 | email: suggestedEmployee.email,
72 | name: suggestedEmployee.name,
73 | };
74 | }
75 |
76 | export default getSubstitute;
77 |
--------------------------------------------------------------------------------
/server/src/public/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Metro Hrms Server
7 |
8 |
9 |
11 |
13 |
Metro Cash & Carry
14 |
Welcome to Metro HRMS
15 |
16 | Manage employees, track attendance, automate payroll, streamlime recruitment process and improve workforce efficiency with AI-powered insights.
17 |
18 |
20 | Visit Metro HRMS Portal
21 |
22 |
23 |
Metro HRMS © 2024. All Rights Reserved.
24 |
`;
25 |
26 |
--------------------------------------------------------------------------------
/server/src/routes/attendance.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | markAttendance,
4 | getAttendanceList,
5 | markAbsentAtEndOfDay,
6 | getEmployeeAttendance,
7 | markAttendanceByQrCode,
8 | genrateQrCodeForAttendance,
9 | getMonthlyAttendancePercentage,
10 | getEmployeeAttendanceByDepartment,
11 | getEmployeeMonthAttendanceByDepartment,
12 | } from "../controllers/attendance.controller.js";
13 | import {
14 | verifyAdminToken,
15 | verifyCornJob,
16 | verifyEmployeeToken,
17 | } from "../middlewares/index.js";
18 |
19 | const router = express.Router();
20 |
21 | router.get("/", verifyAdminToken, getAttendanceList);
22 | router.post("/mark", verifyAdminToken, markAttendance);
23 | router.post("/mark-absent", verifyCornJob, markAbsentAtEndOfDay);
24 | router.get("/employee", verifyEmployeeToken, getEmployeeAttendance);
25 | router.post("/mark/qr", verifyEmployeeToken, markAttendanceByQrCode);
26 | router.get("/month", verifyAdminToken, getMonthlyAttendancePercentage);
27 | router.post("/generate", verifyEmployeeToken, genrateQrCodeForAttendance);
28 | router.get("/department", verifyAdminToken, getEmployeeAttendanceByDepartment);
29 | router.get(
30 | "/month/department",
31 | verifyAdminToken,
32 | getEmployeeMonthAttendanceByDepartment
33 | );
34 |
35 | export default router;
36 |
--------------------------------------------------------------------------------
/server/src/routes/authentication.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | logout,
4 | login,
5 | logoutAll,
6 | resetPassword,
7 | forgetPassword,
8 | updatePassword,
9 | checkResetPasswordValidity,
10 | } from "../controllers/authentication.controller.js";
11 | import {
12 | loginLimiter,
13 | verifyAdminToken,
14 | verifyEmployeeToken,
15 | } from "../middlewares/index.js";
16 |
17 | const router = express.Router();
18 |
19 | router.post("/login", loginLimiter, login);
20 | router.patch("/reset/password", resetPassword);
21 | router.post("/forget/password", forgetPassword);
22 | router.get("/logout", verifyEmployeeToken, logout);
23 | router.get("/logout/all", verifyAdminToken, logoutAll);
24 | router.get("/reset/password/validate", checkResetPasswordValidity);
25 | router.patch("/password/update", verifyEmployeeToken, updatePassword);
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/server/src/routes/complaint.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getComplaints,
4 | createComplaint,
5 | respondComplaint,
6 | assignComplaintForResolution,
7 | } from "../controllers/complaint.controller.js";
8 | import { verifyAdminToken, verifyEmployeeToken } from "../middlewares/index.js";
9 |
10 | const router = express.Router();
11 |
12 | router.get("/", verifyAdminToken, getComplaints);
13 | router.post("/", verifyEmployeeToken, createComplaint);
14 | router.patch("/:id", verifyAdminToken, respondComplaint);
15 | router.patch(
16 | "/:id/assign",
17 | verifyAdminToken,
18 | assignComplaintForResolution
19 | );
20 |
21 | export default router;
22 |
--------------------------------------------------------------------------------
/server/src/routes/department.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | createDepartment,
4 | getAllDepartments,
5 | getDepartmentById,
6 | deleteDepartment,
7 | updateDepartment,
8 | getAllEmployeesForHead,
9 | getDepartmentEmployees,
10 | } from "../controllers/department.controller.js";
11 | import { verifyAdminToken } from "../middlewares/index.js";
12 |
13 | const router = express.Router();
14 |
15 | router.get("/head", verifyAdminToken, getAllEmployeesForHead);
16 | router.post("/", verifyAdminToken, createDepartment);
17 | router.get("/:id/employees", verifyAdminToken, getDepartmentEmployees);
18 | router.get("/", verifyAdminToken, getAllDepartments);
19 | router.get("/:id", verifyAdminToken, getDepartmentById);
20 | router.delete("/:id", verifyAdminToken, deleteDepartment);
21 | router.patch("/:id", verifyAdminToken, updateDepartment);
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/server/src/routes/employee.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | updateProfile,
4 | updateEmployee,
5 | deleteEmployee,
6 | createEmployee,
7 | getAllEmployees,
8 | getEmployeeById,
9 | bulkCreateEmployees,
10 | } from "../controllers/employee.controller.js";
11 | import { upload } from "../config/index.js";
12 | import { verifyAdminToken, verifyEmployeeToken } from "../middlewares/index.js";
13 |
14 | const router = express.Router();
15 |
16 | router.post("/", verifyAdminToken, createEmployee);
17 | router.get("/", verifyAdminToken, getAllEmployees);
18 | router.post("/bulk", verifyAdminToken, bulkCreateEmployees);
19 | router.patch(
20 | "/profile",
21 | verifyEmployeeToken,
22 | upload.single("profilePicture"),
23 | updateProfile
24 | );
25 | router.get("/:id", verifyEmployeeToken, getEmployeeById);
26 | router.delete("/:id", verifyAdminToken, deleteEmployee);
27 | router.patch("/:id", verifyEmployeeToken, updateEmployee);
28 |
29 | export default router;
30 |
--------------------------------------------------------------------------------
/server/src/routes/feedback.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { verifyAdminToken, verifyEmployeeToken } from "../middlewares/index.js";
3 | import {
4 | getFeedbacks,
5 | createFeedback,
6 | } from "../controllers/feedback.controller.js";
7 |
8 | const router = express.Router();
9 |
10 | router.get("/", verifyAdminToken, getFeedbacks);
11 | router.post("/", verifyEmployeeToken, createFeedback);
12 |
13 | export default router;
14 |
--------------------------------------------------------------------------------
/server/src/routes/index.routes.js:
--------------------------------------------------------------------------------
1 | import role from "./role.routes.js";
2 | import leave from "./leave.routes.js";
3 | import payroll from "./payroll.routes.js";
4 | import employee from "./employee.routes.js";
5 | import feedback from "./feedback.routes.js";
6 | import inshight from "./insights.routes.js";
7 | import complaint from "./complaint.routes.js";
8 | import attendance from "./attendance.routes.js";
9 | import department from "./department.routes.js";
10 | import performance from "./performance.routes.js";
11 | import recruitment from "./recruitment.routes.js";
12 | import authentication from "./authentication.routes.js";
13 |
14 | export {
15 | role,
16 | leave,
17 | payroll,
18 | employee,
19 | feedback,
20 | inshight,
21 | complaint,
22 | attendance,
23 | department,
24 | performance,
25 | recruitment,
26 | authentication,
27 | };
28 |
--------------------------------------------------------------------------------
/server/src/routes/insights.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getDate, getUpdates } from "../controllers/update.controller.js";
3 | import answerAdminQuery from "../controllers/chatbot.controller.js";
4 | import {
5 | getAdminInsights,
6 | getEmployeeInsights,
7 | } from "../controllers/insights.controller.js";
8 | import { verifyAdminToken, verifyEmployeeToken } from "../middlewares/index.js";
9 |
10 | const router = express.Router();
11 |
12 | router.get("/date", verifyEmployeeToken, getDate);
13 | router.get("/", verifyAdminToken, getAdminInsights);
14 | router.get("/updates", verifyEmployeeToken, getUpdates);
15 | router.post("/chat", verifyAdminToken, answerAdminQuery);
16 | router.get("/employee", verifyEmployeeToken, getEmployeeInsights);
17 |
18 | export default router;
19 |
--------------------------------------------------------------------------------
/server/src/routes/leave.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getLeaves,
4 | applyLeave,
5 | respondLeave,
6 | assignSustitute,
7 | getEmployeesOnLeave,
8 | } from "../controllers/leave.controller.js";
9 | import { verifyAdminToken, verifyEmployeeToken } from "../middlewares/index.js";
10 |
11 | const router = express.Router();
12 |
13 | router.get("/", verifyAdminToken, getLeaves);
14 | router.post("/", verifyEmployeeToken, applyLeave);
15 | router.patch("/:id", verifyAdminToken, respondLeave);
16 | router.get("/employee", verifyAdminToken, getEmployeesOnLeave);
17 | router.patch("/:id/substitute", verifyAdminToken, assignSustitute);
18 |
19 | export default router;
20 |
--------------------------------------------------------------------------------
/server/src/routes/payroll.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | markAsPaid,
4 | createPayroll,
5 | updatePayroll,
6 | getAllPayrolls,
7 | getPayrollByEmployee,
8 | getEmployeePayrollHistory,
9 | generatePayrollForNextYear,
10 | } from "../controllers/payroll.controller.js";
11 | import { verifyAdminToken, verifyCornJob } from "../middlewares/index.js";
12 |
13 | const router = express.Router();
14 |
15 | router.get("/", verifyAdminToken, getAllPayrolls);
16 | router.post("/", verifyAdminToken, createPayroll);
17 | router.patch("/:payrollId", verifyAdminToken, updatePayroll);
18 | router.patch("/:payrollId/pay", verifyAdminToken, markAsPaid);
19 | router.get("/employee", verifyAdminToken, getPayrollByEmployee);
20 | router.post("/generate", verifyCornJob, generatePayrollForNextYear);
21 | router.get("/history/:employee", verifyAdminToken, getEmployeePayrollHistory);
22 |
23 | export default router;
24 |
--------------------------------------------------------------------------------
/server/src/routes/performance.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | updatePerformance,
4 | getAllPerformances,
5 | getPerformanceMetricsById,
6 | } from "../controllers/performance.controller.js";
7 | import { verifyAdminToken } from "../middlewares/index.js";
8 |
9 | const router = express.Router();
10 |
11 | router.get("/", verifyAdminToken, getAllPerformances);
12 | router.patch("/:employeeID", verifyAdminToken, updatePerformance);
13 | router.get("/:employeeID", verifyAdminToken, getPerformanceMetricsById);
14 |
15 | export default router;
16 |
--------------------------------------------------------------------------------
/server/src/routes/recruitment.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | createJob,
4 | getAllJobs,
5 | getJobById,
6 | createApplicant,
7 | updateJobStatus,
8 | getJobApplications,
9 | inviteForInterview,
10 | updateApplicationStatus,
11 | } from "../controllers/recruitment.controller.js";
12 | import { verifyAdminToken } from "../middlewares/index.js";
13 | import { uploadResume } from "../config/index.js";
14 |
15 | const router = express.Router();
16 |
17 | router.get("/", getAllJobs);
18 | router.get("/:id", getJobById);
19 | router.post("/", verifyAdminToken, createJob);
20 | router.patch("/:id/status", verifyAdminToken, updateJobStatus);
21 | router.get("/:id/applicants", verifyAdminToken, getJobApplications);
22 | router.post("/:id/apply", uploadResume.single("resume"), createApplicant);
23 | router.patch(
24 | "/:jobId/applicants/:applicantId/status",
25 | verifyAdminToken,
26 | updateApplicationStatus
27 | );
28 | router.post(
29 | "/:jobId/applicants/:applicantId/invite",
30 | verifyAdminToken,
31 | inviteForInterview
32 | );
33 |
34 | export default router;
35 |
--------------------------------------------------------------------------------
/server/src/routes/role.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | createRole,
4 | getAllRoles,
5 | getRoleById,
6 | deleteRole,
7 | updateRole,
8 | } from "../controllers/role.controller.js";
9 | import { verifyAdminToken } from "../middlewares/index.js";
10 |
11 | const router = express.Router();
12 |
13 | router.post("/", verifyAdminToken, createRole);
14 | router.get("/", verifyAdminToken, getAllRoles);
15 | router.get("/:id", verifyAdminToken, getRoleById);
16 | router.delete("/:id", verifyAdminToken, deleteRole);
17 | router.patch("/:id", verifyAdminToken, updateRole);
18 |
19 | export default router;
20 |
--------------------------------------------------------------------------------
/server/src/setup/index.js:
--------------------------------------------------------------------------------
1 | import Employee from "../models/employee.model.js";
2 | import { connectDB, disConnectDB } from "../config/index.js";
3 | import { startHrmsApplication } from "../seeders/index.js";
4 |
5 | async function setUpHrmsApplication() {
6 | try {
7 | await connectDB();
8 | const count = await Employee.countDocuments();
9 | if (count === 0) {
10 | await startHrmsApplication();
11 | console.log("Project setup successfully! 🎉");
12 | } else console.log("Project already setup. Skipping setup.");
13 | } catch (error) {
14 | console.log("Error in setup", error.message);
15 | } finally {
16 | await disConnectDB();
17 | }
18 | }
19 |
20 | setUpHrmsApplication();
21 |
--------------------------------------------------------------------------------
/server/test.yaml:
--------------------------------------------------------------------------------
1 | config:
2 | target: "http://localhost:4000"
3 | phases:
4 | - duration: 60
5 | arrivalRate: 100
6 | scenarios:
7 | - flow:
8 | - get:
9 | url: "/api/employees"
10 |
--------------------------------------------------------------------------------
/server/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "src/index.js",
6 | "use": "@vercel/node"
7 | }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/(.*)",
12 | "dest": "src/index.js"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------