Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@
"lint-staged": {
"*.{js,jsx}": [
"prettier --write",
"eslint --fix",
"git add"
"eslint --fix"
]
}
}
9 changes: 9 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Examination from "./Modules/Examination/examination";
import Database from "./Modules/Database/database";
import ProgrammeCurriculumRoutes from "./Modules/Program_curriculum/programmCurriculum";
import NotFoundPage from "./components/NotFoundPage";
import HR from "./Modules/HR";

const theme = createTheme({
breakpoints: {
Expand Down Expand Up @@ -81,6 +82,14 @@ export default function App() {
<Route path="/accounts/login" element={<LoginPage />} />
<Route path="/reset-password" element={<ForgotPassword />} />
<Route path="/examination/*" element={<Examination />} />
<Route
path="/hr/*"
element={
<Layout>
<HR />
</Layout>
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/hr/* route has no RequireAuth guard. All other protected routes in this file (dashboard, academics, profile, examination, etc.) are wrapped in <RequireAuth>. The HR route is not. An unauthenticated user who navigates directly to /hr will hit the HR module instead of being redirected to login.

/>
<Route path="/database/*" element={<Database />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
Expand Down
Binary file added src/Modules/HR.zip
Binary file not shown.
100 changes: 100 additions & 0 deletions src/Modules/HR/components/common/CPDABalanceButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState, useEffect } from "react";
import { Modal, Button, ScrollArea, Loader, Text, Group } from "@mantine/core";
import { Wallet } from "@phosphor-icons/react";
import { getLeaveBalance } from "../../services/api";

function CPDABalanceButton() {
const [opened, setOpened] = useState(false);
const [cpdaBalance, setCpdaBalance] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const fetchCpdaBalance = async () => {
setLoading(true);
setError(null);

try {
const data = await getLeaveBalance(); // Backend returns both leave and cpda balance
setCpdaBalance(data.cpda_balance);
} catch (err) {
console.error(err);
setError("Failed to fetch CPDA balance.");
} finally {
setLoading(false);
}
};

useEffect(() => {
if (opened) {
fetchCpdaBalance();
}
}, [opened]);

return (
<>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "-50px",
position: "relative",
top: "50px",
zIndex: 10
}}
>
<Button
onClick={() => setOpened(true)}
leftSection={<Wallet size={20} />}
color="green"
variant="light"
>
Check CPDA Balance
</Button>
</div>

<Modal
opened={opened}
onClose={() => setOpened(false)}
title={
<span style={{ fontWeight: "bold", fontSize: "20px" }}>
Cumulative Professional Development Allowance (CPDA)
</span>
}
centered
size="md"
>
{loading ? (
<div style={{ display: "flex", justifyContent: "center", padding: "40px" }}>
<Loader size="lg" />
</div>
) : error ? (
<Text color="red" ta="center" fw={700} p="md">{error}</Text>
) : cpdaBalance ? (
<div style={{ padding: "10px" }}>
<Group position="apart" mb="md">
<Text fw={500}>Total Allotted:</Text>
<Text fw={700} color="blue">Rs. {cpdaBalance.allotted.toLocaleString()}</Text>
</Group>
<Group position="apart" mb="md">
<Text fw={500}>Total Used:</Text>
<Text fw={700} color="orange">Rs. {cpdaBalance.taken.toLocaleString()}</Text>
</Group>
<Group position="apart" pt="md" style={{ borderTop: "2px solid #eee" }}>
<Text fw={700} size="lg">Current Balance:</Text>
<Text fw={800} size="xl" color={cpdaBalance.balance > 0 ? "green" : "red"}>
Rs. {cpdaBalance.balance.toLocaleString()}
</Text>
</Group>
<Text size="xs" color="dimmed" mt="xl" ta="center">
*This balance represents your cumulative eligibility for the current block period.
</Text>
</div>
) : (
<Text ta="center" fw={500} p="md">No CPDA balance data available.</Text>
)}
</Modal>
</>
);
}

export default CPDABalanceButton;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import PropTypes from "prop-types";

export function Dots({ size = 185, radius = 2.5, ...others }) {
export function Dots({ size = 185, radius = 2.5, className, style }) {
return (
<svg
aria-hidden
Expand All @@ -9,7 +10,8 @@ export function Dots({ size = 185, radius = 2.5, ...others }) {
viewBox="0 0 185 185"
width={size}
height={size}
{...others}
className={className}
style={style}
>
<rect width="5" height="5" rx={radius} />
<rect width="5" height="5" x="60" rx={radius} />
Expand Down Expand Up @@ -114,3 +116,10 @@ export function Dots({ size = 185, radius = 2.5, ...others }) {
</svg>
);
}

Dots.propTypes = {
size: PropTypes.number,
radius: PropTypes.number,
className: PropTypes.string,
style: PropTypes.shape({}),
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { Container, Title, Text, Button } from "@mantine/core";
import { Container, Title, Text } from "@mantine/core";
import { Dots } from "./Dots";
import classes from "./HeroBanner.module.css";
import classes from "../../styles/HeroBanner.module.css";

export function HeroBanner() {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { Breadcrumbs as MantineBreadcrumbs, Anchor } from "@mantine/core";
import { CaretRight } from "@phosphor-icons/react";
import classes from "./HrBreadcrumbs.module.css";
import classes from "../../styles/HrBreadcrumbs.module.css";

const HrBreadcrumbs = ({ items }) => {
function HrBreadcrumbs({ items }) {
const navigate = useNavigate();

const handleClick = (path) => {
Expand All @@ -27,6 +28,15 @@ const HrBreadcrumbs = ({ items }) => {
))}
</MantineBreadcrumbs>
);
};
}

export default HrBreadcrumbs;

HrBreadcrumbs.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
}),
).isRequired,
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import { Loader, Center, Text } from "@mantine/core";
import PropTypes from "prop-types";

const LoadingComponent = ({ loadingMsg = "fetching data..." }) => {
function LoadingComponent({ loadingMsg = "fetching data..." }) {
return (
<Center
style={{ height: "50vh", flexDirection: "column", textAlign: "center" }}
Expand All @@ -16,6 +17,10 @@ const LoadingComponent = ({ loadingMsg = "fetching data..." }) => {
</Text>
</Center>
);
};
}

export default LoadingComponent;

LoadingComponent.propTypes = {
loadingMsg: PropTypes.string,
};
17 changes: 17 additions & 0 deletions src/Modules/HR/components/common/LoadingSpinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";
import PropTypes from "prop-types";

function LoadingSpinner({ message = "Loading..." }) {
return (
<div className="loading-spinner">
<div className="spinner" />
<p>{message}</p>
</div>
);
}

export default LoadingSpinner;

LoadingSpinner.propTypes = {
message: PropTypes.string,
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { Select } from "@mantine/core";
import { search_employees } from "../../../routes/hr/index";
import PropTypes from "prop-types";
import { search_employees } from "../../../../routes/hr/index";

const SearchAndSelectUser = ({ onUserSelect }) => {
function SearchAndSelectUser({ onUserSelect }) {
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const [error, setError] = useState(null);

const token = localStorage.getItem("authToken");
Expand Down Expand Up @@ -57,8 +57,6 @@ const SearchAndSelectUser = ({ onUserSelect }) => {

const handleUserSelection = (selectedValue) => {
const user = searchResults.find((result) => result.value === selectedValue);
setSelectedUser(user?.details || null);

// Pass selected user to the parent via the callback
if (onUserSelect && user?.details) {
onUserSelect(user.details);
Expand All @@ -79,6 +77,10 @@ const SearchAndSelectUser = ({ onUserSelect }) => {
/>
</div>
);
};
}

export default SearchAndSelectUser;

SearchAndSelectUser.propTypes = {
onUserSelect: PropTypes.func,
};
Original file line number Diff line number Diff line change
@@ -1,46 +1,25 @@
import React, { useState, useEffect, useRef } from "react";
import { Select } from "@mantine/core";
import PropTypes from "prop-types";
import { search_employees } from "../../../routes/hr/index";
import { searchEmployees } from "../../services/api";

function SearchEmployee({ onEmployeeSelect, initialSearch }) {
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
const [selectedEmployee, setSelectedEmployee] = useState(null);
const [error, setError] = useState(null);
const [searchText, setSearchText] = useState(initialSearch || "");

const hasAutoSearched = useRef(false);
const token = localStorage.getItem("authToken");

const fetchEmployees = async (text) => {
if (text.length < 3) {
setSearchResults([]);
return;
}

if (!token) {
setError("Authentication token is missing.");
return;
}

setLoading(true);
setError(null);

try {
const response = await fetch(`${search_employees}?search_text=${text}`, {
headers: {
Authorization: `Token ${token}`,
},
});

if (!response.ok) {
throw new Error(`Failed to fetch employees: ${response.statusText}`);
}

const data = await response.json();

const uniqueEmployees = data.employees.reduce((acc, employee) => {
const results = await searchEmployees(text);
const uniqueEmployees = results.reduce((acc, employee) => {
if (!acc[employee.id]) {
acc[employee.id] = {
value: `${employee.id}-${employee.username}`,
Expand All @@ -50,14 +29,10 @@ function SearchEmployee({ onEmployeeSelect, initialSearch }) {
}
return acc;
}, {});

const formattedResults = Object.values(uniqueEmployees);
setSearchResults(formattedResults);

return formattedResults;
setSearchResults(Object.values(uniqueEmployees));
} catch (err) {
setError("Unable to fetch employees.");
return [];
setSearchResults([]);
} finally {
setLoading(false);
}
Expand All @@ -67,9 +42,6 @@ function SearchEmployee({ onEmployeeSelect, initialSearch }) {
const employee = searchResults.find(
(result) => result.value === selectedValue,
);
setSelectedEmployee(employee?.details || null);
console.log(selectedEmployee);

if (onEmployeeSelect && employee?.details) {
onEmployeeSelect(employee.details);
}
Expand All @@ -79,14 +51,7 @@ function SearchEmployee({ onEmployeeSelect, initialSearch }) {
const autoSearch = async () => {
if (initialSearch && !hasAutoSearched.current) {
hasAutoSearched.current = true;
setSearchText(initialSearch);
console.log(searchText);
const results = await fetchEmployees(initialSearch);
if (results.length > 0) {
const firstEmployee = results[0];
setSelectedEmployee(firstEmployee.details);
onEmployeeSelect?.(firstEmployee.details);
}
await fetchEmployees(initialSearch);
}
};
autoSearch();
Expand All @@ -101,7 +66,6 @@ function SearchEmployee({ onEmployeeSelect, initialSearch }) {
nothingFound={error || "No employees found"}
data={searchResults}
onSearchChange={(val) => {
setSearchText(val);
fetchEmployees(val);
}}
onChange={handleEmployeeSelection}
Expand All @@ -111,7 +75,6 @@ function SearchEmployee({ onEmployeeSelect, initialSearch }) {
);
}

// ✅ PropTypes validation
SearchEmployee.propTypes = {
onEmployeeSelect: PropTypes.func,
initialSearch: PropTypes.string,
Expand Down
Loading