Commit 4a3dab02 authored by =sankoju Ravi's avatar =sankoju Ravi

Merge branch 'master' of...

Merge branch 'master' of https://gitlab.mynisum.com/vsingamchetty/nisum-scorecard into feture/get-activities
parents a38e9755 60298025
......@@ -52,6 +52,7 @@ Data In body Example :
"searchText":"eng"
}*/
3. POST method to add activity added by manager to his reportees. Id should contain all the fields given below in the example or else it throws an error.
Note: Users can add a custom aName field , from frontend a unique id will be generated and added as aID.
......@@ -59,18 +60,20 @@ https://nisumscorecardservertesting.netlify.app/.netlify/functions/api/createAct
Data In body Example :
//Example of post Data
/*
{
"empId":41689,
"empId":10000,
"data":{
"aName":"Approval of timesheet",
"aId":"D001",
"type":"duties",
"ratedBy":"Name",
"score":3,
"comments":""
}
}
Actual Activities Data this to be used when trying to create a activity to a employee:
[{"_id":"65f19252ecd2b756fab896b8","type":"duties","aId":"D001","aName":"Submission timesheet"},
......@@ -91,8 +94,29 @@ Data In body Example :
{
"empId":41689,
"fromDate":"2024-03-10",
"toDate":"2024-03-14"
"toDate":"2024-03-14",
page:0
perPage:10,
getAll:true //send only for get all records
}
*/
5. POST method to get Average scores of individual activities.
https://nisumscorecardservertesting.netlify.app/.netlify/functions/api/getActivities-avg
//sending filtered activities avg score data
/*Example post data
{
"empId":41689,
"fromDate":"2024-03-10",
"toDate":"2024-03-14",
"types":["duties","initiative"]
}
*/
API's for Deployment Team Usage
......@@ -134,6 +158,7 @@ Data In body Example :
"searchText":"eng"
}*/
3. POST method to add activity added by manager to his reportees. Id should contain all the fields given below in the example or else it throws an error.
Note: Users can add a custom aName field , from frontend a unique id will be generated and added as aID.
......@@ -141,18 +166,19 @@ https://nisumscorecardserverdev.netlify.app/.netlify/functions/api/createActivit
Data In body Example :
//Example of post Data
/*
{
"empId":41689,
"empId":10000,
"data":{
"aName":"Approval of timesheet",
"aId":"D001",
"type":"duties",
"ratedBy":"Name",
"score":3,
"comments":""
}
}
Actual Activities Data this to be used when trying to create a activity to a employee:
[{"_id":"65f19252ecd2b756fab896b8","type":"duties","aId":"D001","aName":"Submission timesheet"},
......@@ -168,12 +194,30 @@ Actual Activities Data this to be used when trying to create a activity to a emp
https://nisumscorecardserverdev.netlify.app/.netlify/functions/api/getActivities
Data In body Example :
/*Example post data
{
"empId":41689,
"fromDate":"2024-03-10",
"toDate":"2024-03-14"
"toDate":"2024-03-14",
page:0
perPage:10,
getAll:true //send only for get all records
}
*/
5. POST method to get Average scores of individual activities.
https://nisumscorecardserverdev.netlify.app/.netlify/functions/api/getActivities-avg
//sending filtered activities avg score data
/*Example post data
{
"empId":41689,
"fromDate":"2024-03-10",
"toDate":"2024-03-14",
"types":["duties","initiative"]
}
*/
This diff is collapsed.
......@@ -6,10 +6,12 @@
"@reduxjs/toolkit": "^2.2.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.6.7",
"cors": "^2.8.5",
"express": "^4.18.3",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.8.2",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"mongodb": "^6.5.0",
......@@ -50,6 +52,7 @@
]
},
"devDependencies": {
"@babel/preset-env": "^7.24.3",
"tailwindcss": "^3.4.1"
}
}
public/favicon.PNG

1.25 KB | W: | H:

public/favicon.PNG

2.38 KB | W: | H:

public/favicon.PNG
public/favicon.PNG
public/favicon.PNG
public/favicon.PNG
  • 2-up
  • Swipe
  • Onion skin
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Scorecard",
"name": "Nisum ScoreCard",
"icons": [
{
"src": "favicon.ico",
"src": "favicon1.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
"type": "image/png"
},
{
"src": "logo192.png",
......
{
"presets": ["@babel/preset-env"]
}
\ No newline at end of file
......@@ -6,6 +6,7 @@ import Viewreportee from './pages/viewReportee';
import './App.css';
import PageNotFound from './pages/pagenotfound/PageNotFound';
import Exporttable from './pages/reportexport'
function App() {
return (
<BrowserRouter>
......
......@@ -3,6 +3,6 @@ import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
// const linkElement = screen.getByText(/learn react/i);
// expect(linkElement).toBeInTheDocument();
});
import React from "react";
function DashboardIcon() {
return (
<svg
className="size-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
);
}
export default DashboardIcon;
import React from 'react'
function DownloadIcon() {
return (
<svg width="25px" height="25px" viewBox="0 0 24 24" fill="none" className='ml-2'>
<path d="M12 7L12 14M12 14L15 11M12 14L9 11" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 17H12H8" stroke="white" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12Z" stroke="white" strokeWidth="1.5"/>
</svg>
)
}
export default DownloadIcon
import React from "react";
function ReportsIcon() {
return (
<svg
className="size-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
</svg>
);
}
export default ReportsIcon;
......@@ -12,7 +12,7 @@ const PaginationComponent = ({ currentPage, totalPages, onPageChange }) => {
return (
<nav className="flex justify-center my-2">
<ul className="pagination flex">
<button disabled={currentPage === 1} onClick={() => onPageChange(currentPage-1)} className='mr-2 hover:bg-blue-400 border rounded disabled:bg-gray-200'><LeftIcon /></button>
<button disabled={currentPage === 1} onClick={() => onPageChange(currentPage-1)} className={`ml-2 hover:bg-blue-400 border-0 bg-transparent rounded ${currentPage===1 ? "hidden":null}`}><LeftIcon /></button>
{pageNumbers.map(number => (
<li key={number} className={`page-item ${number === currentPage ? 'active' : ''}` }>
<button onClick={() => onPageChange(number)} className=" w-[22px] font-bold h-[22px] ">
......@@ -20,7 +20,7 @@ const PaginationComponent = ({ currentPage, totalPages, onPageChange }) => {
</button>
</li>
))}
<button disabled={currentPage === totalPages} onClick={() => onPageChange(currentPage+1)} className='ml-2 hover:bg-blue-400 border rounded disabled:bg-gray-200'><RightIcon/></button>
<button disabled={currentPage === totalPages} onClick={() => onPageChange(currentPage+1)} className={`ml-2 hover:bg-blue-400 border-0 bg-transparent rounded ${currentPage===totalPages ? "hidden":null}`}><RightIcon/></button>
</ul>
</nav>
);
......
......@@ -29,12 +29,12 @@ function Accordion({ title, data ,handleAddActivity,open,handleAccordian}) {
// }
// },[userDetails,id])
useEffect(()=>{
if(reports?.length !==null){
dispatch(calculateDefaultScore(reports))
dispatch(calculateInitiativeScore(reports))
}
},[reports,id])
// useEffect(()=>{
// if(reports?.length !==null){
// dispatch(calculateDefaultScore(reports))
// dispatch(calculateInitiativeScore(reports))
// }
// },[reports,id])
function handleClick(){
handleAccordian(title)
......@@ -46,7 +46,7 @@ function Accordion({ title, data ,handleAddActivity,open,handleAccordian}) {
{ title: "Score", id: "score", render: (value) => <div className="w-[35px] px-3 bg-blue-400 rounded-full text-white font-bold text-center p-[4px]">{value}</div> },
{ title: "Comments", id: "comments", render:(value)=><span className="listData" title={value}>{value}</span>},
];
if(loading && title =="Duties")return <Loading/>
if(!loading){
......@@ -59,7 +59,7 @@ function Accordion({ title, data ,handleAddActivity,open,handleAccordian}) {
className="flex items-center rounded-lg w-full py-2 px-2 mt-4 font-medium rtl:text-right bg-white text-gray-500 border border-[#B7B7B7] focus:ring-4 hover:bg-gray-100 gap-3" data-accordion-target="#accordion-collapse-body-2" aria-expanded="false" aria-controls="accordion-collapse-body-2" >
<span className="w-1/2 text-start ms-2">{title}</span>
<span className="w-1/2 flex justify-between">
Average Score :{title === "Duties" ? defaultAvgScore : initiativeAvgScore}
Score: {title === "Duties" ? defaultAvgScore : initiativeAvgScore}
<ModalButton type={`${title === "Duties" ? "duties" : "initiative"}`} handleAddActivity={handleAddActivity}/>
</span>
<svg data-accordion-icon className="w-3 h-3 rotate-180 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
......
......@@ -37,7 +37,7 @@ function Header({ isOpen }) {
}, []);
return (
<div className="flex items-center justify-between py-5 px-10" >
<div className="flex items-center justify-between py-5 px-10 fixed bg-white w-full" >
<Link to={"/dashboard"}><img src="/logo.png" /></Link>
<div className="flex items-center relative">
<button ref={logoutRef} className=" -mt-1 text-2xl flex" onClick={() => setOpen(!open)}>
......
import React, { useEffect, useState } from "react";
import { fetchReportees, setViewReportee, setCurrPage, setPagesCount } from "../../redux/reducers/reporteesSlice";
import { fetchReportees, setCurrPage, setPagesCount, setReporteeId } from "../../redux/reducers/reporteesSlice";
import { Link } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
......@@ -15,20 +15,19 @@ function LeftSidebar() {
useEffect(() => {
if(inputValue!==null){
const debounceTimeout = setTimeout(() => {
const data = {
reportees: userDetails.user.reportees,
page: currPage,
perPage: 10,
searchText:inputValue
};
dispatch(fetchReportees(data));
}, 1000);
if (inputValue !== null) {
const debounceTimeout = setTimeout(() => {
const data = {
reportees: userDetails.user.reportees,
page: (inputValue === "") ? currPage : 1,
perPage: 10,
searchText: inputValue
};
dispatch(fetchReportees(data));
}, 1000);
return () => clearTimeout(debounceTimeout);
}
return () => clearTimeout(debounceTimeout);
}
}, [inputValue]);
const handleChange = (event) => {
......@@ -52,44 +51,50 @@ function LeftSidebar() {
return (
<div className=" w-[33%] flex flex-col px-[5px]">
<div className=" flex mt-3 items-center justify-between">
<div className=" flex mt-3 items-center">
<p className="text-xl text-blue-400 font-semibold pl-4">
Reportees
Reportees
</p>
<input
<input
placeholder="Search"
type="text"
className="p-2 border rounded w-[160px] placeholder:text-[14px]"
value={inputValue}
onChange={handleChange}
type="text"
className="p-2 border rounded ml-[16px] placeholder:text-[14px]"
value={inputValue}
onChange={handleChange}
/>
</div>
{
(loading) ? <Loading /> :
<div className="p-2 bg-[#E9EDEE] mt-4 max-h-[70vh] overflow-auto">
{reportees?.map(({ empName, score, empId }) => (
<button onClick={() => dispatch(setViewReportee(empId))}
// to={`/viewreportee`}
className={`flex items-center hover:bg-blue-400 hover:text-white bg-${viewReportee?.empId == empId ? "blue-400 text-white" : "white"
{(reportees.length) ? reportees?.map(({ empName, score, empId }) => (
<button onClick={() => dispatch(setReporteeId(empId))}
className={`flex rounded-lg items-center hover:bg-blue-400 hover:text-white bg-${viewReportee?.empId == empId ? "blue-400 text-white" : "white"
} p-2 justify-between mb-1 w-full`}
key={empId}
>
<p className="w-[80%] text-left">{empName}</p>
<p className={`w-[30px] h-[30px] rounded-full flex items-center text-white justify-center ${scoreColor(score)}`}>
{score}
</p>
</button>
))}
<p className="w-[80%] text-left">{empName}</p>
<p className={`w-[30px] h-[30px] rounded-full flex items-center text-white justify-center ${scoreColor(score)}`}>
{score}
</p>
</button>
)) : <div className="w-full h-full">
<p className="text-center align-middle text-blue-500 font-bold">No Records Found</p>
</div>
}
</div>
}
<div>
<PaginationComponent
currentPage={currPage}
totalPages={pagesCount}
onPageChange={handlePageChange}
/>
{
reportees.length > 0 && pagesCount > 1 && (
<PaginationComponent
currentPage={currPage}
totalPages={pagesCount}
onPageChange={handlePageChange}
/>
)
}
</div>
</div>
);
......
......@@ -7,16 +7,16 @@ import { v4 as uuidv4 } from 'uuid';
export default function MyModal({ visible, onClose, type, handleAddActivity }) {
const {user} = useSelector((state) => state.userDetails)
const { user } = useSelector((state) => state.userDetails)
const [activitiesList, setActivitiesList] = useState([])
const [enableSubmit, setEnableSubmit] = useState(false)
const [activityData, setActivityData] = useState({ aName: "",ratedBy:"", aId: "", type: type, score: 0, comments: "" })
const [activityData, setActivityData] = useState({ aName: "", ratedBy: "", aId: "", type: type, score: 0, comments: "" })
const [activityType, setActivtyType] = useState("")
const [showCustActivity, setShowActivity] = useState(false);
const [modalLoading, setModalLoading] = useState(true)
const [scoreRender, setScoreRender] = useState([]);
const [showScore, setShowScore] = useState(false)
const [disableAppreciate,setDisableAppreciate]=useState(false)
const [disableAppreciate, setDisableAppreciate] = useState(false)
const getActivitysList = async (type) => {
const activities = await axios.get(`${base_url}/activities`)
......@@ -26,7 +26,14 @@ export default function MyModal({ visible, onClose, type, handleAddActivity }) {
}
const handleActivityName = (e) => {
setActivityData({ ...activityData, aName: e.target.value, aId: e.target.options[e.target.selectedIndex].id })
if (e.target.value === "custom") {
setActivityData({ ...activityData, aName: "", aId: "" })
setShowActivity(true)
}
else {
setShowActivity(false)
setActivityData({ ...activityData, aName: e.target.value, aId: e.target.options[e.target.selectedIndex].id })
}
}
const handleCustumActivity = (e) => {
......@@ -39,9 +46,9 @@ export default function MyModal({ visible, onClose, type, handleAddActivity }) {
setActivityData({ ...activityData, score: Number(value) })
}
const handlePerformance=(value)=> {
const handlePerformance = (value) => {
let appreciateScores = [1, 2, 3, 4, 5]
let depreciateScores = [ -1, -2, -3, -4, -5]
let depreciateScores = [-1, -2, -3, -4, -5]
if (value == 1) {
setActivityData({ ...activityData, score: 0 })
setScoreRender(appreciateScores)
......@@ -55,10 +62,11 @@ export default function MyModal({ visible, onClose, type, handleAddActivity }) {
}
const handleComments = (e) => {
setActivityData({ ...activityData, comments:e.target.value.trim() })
setActivityData({ ...activityData, comments: e.target.value.trim() })
}
const handleSubmit = (e) => {
e.preventDefault()
onClose()
setShowActivity(false)
handleAddActivity(activityData)
......@@ -79,16 +87,16 @@ export default function MyModal({ visible, onClose, type, handleAddActivity }) {
setActivtyType(str.charAt(0).toUpperCase() + str.slice(1).toLowerCase())
}
useEffect(() => {
if(type==="duties"){
if (type === "duties") {
setDisableAppreciate(true);
}else{
} else {
setDisableAppreciate(false);
}
SentenceCase(type)
if (visible === false) {
setActivityData({ aName: "",ratedBy:"", aId: "", type: type, score: 0, comments: "" })
setActivityData({ aName: "", ratedBy: "", aId: "", type: type, score: 0, comments: "" })
} else {
setActivityData({...activityData,ratedBy:user.empName})
setActivityData({ ...activityData, ratedBy: user.empName })
getActivitysList(type);
setModalLoading(true)
}
......@@ -114,25 +122,19 @@ export default function MyModal({ visible, onClose, type, handleAddActivity }) {
<div>
<form className=" p-2 max-w-sm mx-auto text-[12px]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center my-5">
<label htmlFor="countries">SELECT ACTIVITY<span className="text-[15px]">*</span>: </label>
<select disabled={showCustActivity} className="bg-gray-50 ml-2 w-6/12 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 " onChange={(e) => handleActivityName(e)} value={activityData.aName}>
<select className="bg-gray-50 ml-2 w-6/12 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 " onChange={(e) => handleActivityName(e)} >
<option id="" value="">Select</option>
{
activitiesList && activitiesList.map((activity) => <option className=" w-7/12" key={activity.aId} id={activity.aId} value={activity.aName}>{activity.aName}</option>)
}
<option value="custom" className={`${showCustActivity || type == "duties" && 'hidden'}`}>Add Activity</option>
</select>
<button onClick={(e) => { handleCustBtn(e) }} className={`${showCustActivity && 'hidden'} bg-blue-400 ml-2 w-2/12 text-white py-1 rounded hover:scale-95 transition text-sm`}>Custom</button>
</div>
<div className={`flex items-center ${!showCustActivity && 'hidden'}`}>
<label className={`font-medium mr-2`}>Custom Activity<span className="text-[15px]">*</span>:</label>
<label className={`font-medium mr-2`}>Activity<span className="text-[15px]">*</span>:</label>
<input type="text" value={activityData.aName} placeholder="Enter Activity name" name="performance" className={`border border-gray-300 rounded p-2 `} onChange={(e) => handleCustumActivity(e)} />
<button onClick={(e) => { handleCustBtn(e) }} className={`${!showCustActivity && 'hidden'} bg-blue-400 ml-2 w-2/12 text-white py-1 rounded hover:scale-95 transition text-sm`}>Close</button>
</div>
<div className="flex items-center mb-4 ">
<label htmlFor="appreciate" className="font-medium">APPRECIATION<span className="text-[15px]">*</span>:</label>
<input id="appreciate" disabled={disableAppreciate} type="radio" value="appreciate" name="performance" className="w-4 h-4 m-3 text-blue-600 bg-gray-100 border-gray-300 " onChange={() => handlePerformance(1)} />
......
import React from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import SetWindowSize from '../../utils/SetWindowSize';
import DashboardIcon from '../../assets/icons/dashboardIcon';
import ReportsIcon from '../../assets/icons/reportsIcon';
const menus = [
{title: "Dashboard", path: '/dashboard', selectPaths: ['dashboard', 'viewreportee'], icon: <DashboardIcon/> },
{title: "Reports", path: '/reportees', selectPaths:['reportees'], icon: <ReportsIcon />}
]
function Sidebar() {
const user = useSelector((state) => state.userDetails.user);
const url = window.location.href;
const [windowWidth, windowHeight] = SetWindowSize();
const selected = url.split('/').at(-1)
return (
<div className="w-[20%] flex items-center flex-col">
<div className="w-[20%] flex items-center flex-col overflow-auto" style={{ height: `calc(${windowHeight}px - 87px)` }}>
<nav
className="hs-accordion-group p-6 w-full flex flex-col flex-wrap"
data-hs-accordion-always-open
>
<ul className="space-y-1.5">
<li>
<Link
className={`flex items-center gap-x-3.5 py-2 px-2.5 bg-gray-100 text-sm text-slate-700 rounded-lg hover:bg-gray-100 `}
to={`/dashboard`}
>
<svg
className="size-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
Dashboard
</Link>
</li>
<li>
<Link
className={`flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-slate-700 rounded-lg hover:bg-gray-100 `} to={`/reportees`}
>
<svg
className="size-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{
menus.map((menu) => (
<li key={menu.path}>
<Link
className={`flex items-center ${menu.selectPaths.includes(selected) && 'bg-gray-100'} gap-x-3.5 py-2 px-2.5 text-sm text-slate-700 rounded-lg hover:bg-gray-100 `}
to={menu.path}
>
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
</svg>
Reports
</Link>
</li>
<span>{menu.icon}</span>
{menu.title}
</Link>
</li>
))
}
</ul>
</nav>
</div>
......
import React, {useState, useEffect} from "react";
import { useSelector, useDispatch} from "react-redux";
import {setSortKey, setSortOrder} from '../../redux/reducers/reporteesSlice';
import Loading from "../loading Component/Loading";
import SortButton from "../sortButton";
function Table({headers, data,loading, maxHeight}) {
const [sortedData, setSortedData] = useState([]);
const [sortKey, setSortKey] = useState(null);
const [sortOrder, setSortOrder] = useState('asc');
function Table({headers, data,loading, handleSorting }) {
const dispatch = useDispatch();
const {sortKey, sortOrder} = useSelector((state) => state.reportees);
// const [sortedData, setSortedData] = useState([]);
// const [sortKey, setSortKey] = useState(null);
// const [sortOrder, setSortOrder] = useState('asc');
useEffect(() => {
if(sortKey) {
setSortKey(null);
}
setSortedData(data);
}, [data]);
// useEffect(() => {
// if(sortKey) {
// setSortKey(null);
// }
// setSortedData(data);
// }, [data]);
// useEffect(() => {
// if(sortKey) {
// }
// }, [sortKey, sortOrder])
const handleSort = (key) => {
const order = key === sortKey ? (sortOrder === 'asc' ? 'desc' : 'asc') : 'asc';
const order = key === sortKey ? (sortOrder === 'asc' ? 'desc' : 'asc') : 'asc';
if(sortOrder === 'desc' && key === sortKey) {
setSortedData(data);
setSortKey(null);
setSortOrder('asc');
dispatch(setSortKey(null))
dispatch(setSortOrder('asc'));
handleSorting(null, order)
} else {
const sorted = [...data].sort((a, b) => {
if (typeof a[key] === 'string') {
return order === 'asc' ? a[key].localeCompare(b[key]) : b[key].localeCompare(a[key]);
}
return order === 'asc' ? a[key] - b[key] : b[key] - a[key];
});
setSortedData(sorted);
setSortKey(key);
setSortOrder(order);
dispatch(setSortKey(key));
dispatch(setSortOrder(order));
handleSorting(key, order)
}
// if(sortOrder === 'desc' && key === sortKey) {
// setSortedData(data);
// setSortKey(null);
// setSortOrder('asc');
// } else {
// const sorted = [...data].sort((a, b) => {
// if (typeof a[key] === 'string') {
// return order === 'asc' ? a[key].localeCompare(b[key]) : b[key].localeCompare(a[key]);
// }
// return order === 'asc' ? a[key] - b[key] : b[key] - a[key];
// });
// setSortedData(sorted);
// setSortKey(key);
// setSortOrder(order);
// }
};
......@@ -58,7 +79,7 @@ function Table({headers, data,loading, maxHeight}) {
{
(data?.length)?<tbody >
{
sortedData?.map((item, index) => (
data?.map((item, index) => (
<tr key={item.id} className="bg-white hover:bg-gray-300 " >
{
headers?.map(({render, id}) => (
......@@ -75,15 +96,11 @@ function Table({headers, data,loading, maxHeight}) {
</table>
{
(!data?.length)?<div className="w-full h-full">
<p className="text-center align-middle pt-14 pb-14 text-blue-500 font-bold">No records to display</p>
<p className="text-center align-middle pt-14 pb-14 text-blue-500 font-bold">No Records Found</p>
</div>:null
}
</div>
);
// else
// return <div className="w-full h-full">
// <p className="text-center align-middle pt-14 pb-14 text-blue-500 font-bold">No records to display</p>
// </div>
}
export default Table;
......@@ -23,6 +23,23 @@ code {
display: inline-block;
}
.childDiv {
max-height: calc(100vh - 85px);
.loading {
width: 100%;
height: 100%;
content: '';
top: 50%;
left: 50%;
width: 20px;
height: 20px;
border: 3px solid #fff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
\ No newline at end of file
import React, { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { fetchReportees, setViewReportee, setCurrPage, setPagesCount } from "../../redux/reducers/reporteesSlice";
import { fetchReportees, setViewReportee, setCurrPage, setPagesCount, setReporteeId } from "../../redux/reducers/reporteesSlice";
import Table from '../../components/table';
import RightArrowIcon from '../../assets/icons/rightArrowIcon';
import { scoreColor } from '../../utils/commonFunctions';
......@@ -10,32 +10,42 @@ import PaginationComponent from "../../components/Pagenation/Pagenation";
function Dashboard() {
const dispatch = useDispatch();
const navigate = useNavigate();
const { reportees, loading, totalCount, currPage, pagesCount} = useSelector((state) => state.reportees);
const { reportees, loading, totalCount, currPage, pagesCount, sortKey, sortOrder} = useSelector((state) => state.reportees);
const userDetails = useSelector((state) => state.userDetails);
const [reporteIds, setReporteIds] = useState([]);
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState(null);
// userDetails.user.reportees || [];
const handlePageChange = (currPage) => {
let data = {
reportees: userDetails.user.reportees,
page: currPage,
perPage: 10
perPage: 10,
sort: sortKey ? {type:sortKey, order: sortOrder === "asc" ? 1 : -1} : {}
}
dispatch(setCurrPage(currPage))
dispatch(fetchReportees(data))
}
const handleSort = (key, order) => {
let data = {
reportees: userDetails.user.reportees,
page: currPage,
perPage: 10,
sort: key ? {type:key, order: order === "asc" ? 1 : -1} : {}
}
dispatch(fetchReportees(data))
}
useEffect(() => {
dispatch(setPagesCount(Math.ceil((totalCount) / (10))))
}, [totalCount])
useEffect(() => {
if (reporteIds.length > 0) {
if (reporteIds.length > 0 ) {
const data = {
reportees: userDetails.user.reportees,
// sort: { type: "empId", order: 1 },
page: currPage,
perPage: 10
};
......@@ -46,24 +56,27 @@ function Dashboard() {
useEffect(() => {
if (userDetails.user) {
setReporteIds(userDetails.user.reportees)
navigate("/dashboard")
// navigate("/dashboard")
} else {
navigate("/")
}
}, [userDetails]);
useEffect(() => {
if(inputValue!==null){
const debounceTimeout = setTimeout(() => {
const data = {
reportees: userDetails.user.reportees,
page: currPage,
page: 1,
perPage: 10,
searchText:inputValue
};
dispatch(fetchReportees(data));
}, 1000);
return () => clearTimeout(debounceTimeout);
}
// return () => clearTimeout(debounceTimeout);
}, [inputValue]);
const handleChange = (event) => {
......@@ -104,7 +117,7 @@ function Dashboard() {
title: "Action",
id: "empId",
render: (value) => <Link to={`/viewreportee`}>
<button className="bg-blue-400 text-white rounded-md px-2 py-1 flex items-center justify-center w-[40px]" onClick={()=>dispatch(setViewReportee(value))}>
<button className="bg-blue-400 text-white rounded-md px-2 py-1 flex items-center justify-center w-[40px]" onClick={()=>dispatch(setReporteeId(value))}>
<RightArrowIcon />
</button>
</Link>
......@@ -119,12 +132,11 @@ function Dashboard() {
<label>Search :</label>
<input placeholder="Name/Id/Designation/Role" value={inputValue} onChange={handleChange} type="text" className="p-1 px-2 border rounded ml-2 placeholder:text-[14px]"/>
</div>
<Table headers={headers} data={reportees} loading={loading} maxHeight={88} />
<Table headers={headers} data={reportees} loading={loading} handleSorting={handleSort}/>
<div className="">
{reportees && (
{reportees.length>0 && pagesCount>1 && (
<div className="flex justify-center mt-2">
{/* <div className="text-blue-500">Total Results: {pagesCount}</div> */}
{pagesCount >= 1 && (
<PaginationComponent
currentPage={currPage}
......
......@@ -4,7 +4,6 @@ import axios from 'axios';
import { base_url } from "../../utils/constants";
import { loginUser } from "../../redux/reducers/userSlice";
import {useDispatch,useSelector} from 'react-redux'
import Loading from '../../components/loading Component/Loading'
function Home() {
const inputRef = useRef(null);
......
......@@ -2,9 +2,12 @@ import React, { useState } from 'react'
import Header from '../../components/header';
import Sidebar from '../../components/sidebar';
import LeftSidebar from '../../components/leftSidebar';
import SetWindowSize from '../../utils/SetWindowSize';
function Layout({children}) {
const [isOpen, setIsOpen] = useState(false)
const [isOpen, setIsOpen] = useState(false);
const [windowWidth, windowHeight] = SetWindowSize();
const handleLogoutOpen=()=>{
setIsOpen(false)
}
......@@ -12,9 +15,9 @@ function Layout({children}) {
return (
<div className='max-h-[84vh]' onClick={handleLogoutOpen}>
<Header isOpen={isOpen} />
<div className="flex">
<div className="flex pt-[85px]">
<Sidebar/>
<div className="bg-[#E9EDEE] w-full" style={{height:"88vh"}}>
<div className="bg-[#E9EDEE] w-full overflow-auto" style={{ height: `calc(${windowHeight}px - 87px)` }}>
{children}
</div>
{url.includes('/viewreportee') && <LeftSidebar/>}
......
This diff is collapsed.
export const styles = {
genarateReportContainer: "overflow-auto sm:rounded-lg p-4 bg-[#E9EDEE] ",
textBlueHeading: "text-blue-800 py-3 pl-2 text-center fond-bold text-2xl",
formContainer: "p-2 text-[12px] mb-4",
flexContainer: "flex items-center justify-between",
flexItemsCenter: "flex items-center",
selectEmployeeDropdown:
"bg-gray-50 text-balance rounded-lg rounded-md ml-2 border border-gray-300 text-gray-900 text-sm focus:ring-blue-500 focus:border-blue-500 block p-2.5 ",
selectDropdown:"bg-gray-50 text-balance rounded-lg ml-2 border border-gray-300 text-gray-900 text-sm focus:ring-blue-500 focus:border-blue-500 block p-2.5 w-[235px]",
downloadButton:
"px-3 py-2 ml-5 min-w-[100px] disabled:bg-gray-400 h-[40px] bg-blue-500 font-semibold text-white rounded-md flex items-center justify-center",
Norecords: "text-center align-middle pt-14 pb-14 text-blue-500 font-bold",
};
......@@ -4,7 +4,7 @@ import { useParams, useNavigate } from "react-router";
import { base_url } from "../../utils/constants";
import axios from 'axios';
import { fetchReportees,setViewReportee } from "../../redux/reducers/reporteesSlice";
import {fetchReporteeActivities} from '../../redux/reducers/viewreporteeSlice'
import {fetchReporteeActivities, fetchActivitiesAvg, } from '../../redux/reducers/viewreporteeSlice'
import Accordion from "../../components/accordion";
import {scoreColor} from '../../utils/commonFunctions';
......@@ -12,35 +12,31 @@ import {scoreColor} from '../../utils/commonFunctions';
function Viewreportee() {
const dispatch = useDispatch();
const navigate = useNavigate();
const {reportees, viewReportee } = useSelector((state) => state.reportees);
const {reportees, viewReportee,currPage, reporteeId } = useSelector((state) => state.reportees);
const user = useSelector((state) => state.userDetails.user)
const { reports, loading, error } = useSelector((state) => state.reports);
const { reports, loading, error, dutiesReports, initiativeReports } = useSelector((state) => state.reports);
const [open, setOpen] = useState({ "accordianOne": false, "accordianTwo": false });
/*Example post data
{
"empId":41689,
"fromDate":"2024-03-10",
"toDate":"2024-03-11"
}
*/
const activities = useMemo(() => {
if (reports) {
const filtered = Object.groupBy(reports, ({ type }) => type);
return filtered;
const fetchActivities = (type) => {
const data ={
empId:viewReportee?.empId,
types:[type],
page: 1,
perPage: 5
}
}, [reports, viewReportee]);
dispatch(fetchReporteeActivities(data))
}
const handleAccordian = (value) => {
switch (value) {
case "Duties":
setOpen({ ...open, "accordianOne": !open["accordianOne"], "accordianTwo": false });
fetchActivities('duties')
break;
case "Initiatives":
setOpen({ ...open, "accordianOne": false, "accordianTwo": !open["accordianTwo"] });
fetchActivities('initiative')
break;
default:
setOpen({ "accordianOne": false, "accordianTwo": false });
......@@ -51,13 +47,17 @@ function Viewreportee() {
if (user) {
const data = {
reportees: user.reportees,
sort: { type: "empId", order: 1 },
page: 1,
page: currPage,
perPage: 10,
};
dispatch(fetchReportees(data))
}
}
const fetchViewReporteeData=async(empId)=>{
const response= await axios.get(`${base_url}/employee/${empId}`)
const data=await response.data
dispatch(setViewReportee(data))
}
const handleAddActivity = async (activityData) => {
......@@ -68,66 +68,82 @@ function Viewreportee() {
}
await axios.post(`${base_url}/createActivity`, newData)
.then(async (result) => {
fetchLatestReporteesData()
fetchLatestReporteesData();
fetchActivities(activityData?.type)
fetchViewReporteeData(reporteeId)
dispatch(fetchActivitiesAvg({empId:reporteeId, types:["duties", "initiative"]}))
})
} else {
alert("Please login")
}
}
useEffect(()=>{
if(reportees.length>0 && viewReportee !== null)
dispatch(fetchReporteeActivities({empId:viewReportee?.empId}))
},[reportees,viewReportee])
// useEffect(()=>{
// if(reportees.length){
// dispatch(setViewReportee(viewReportee?.empId))
// }
// },[reportees])
useEffect(() => {
if(reporteeId) {
fetchViewReporteeData(reporteeId)
dispatch(fetchActivitiesAvg({empId:reporteeId, types:["duties", "initiative"]}))
}
}, [reporteeId])
useEffect(() => {
if (user) {
navigate(`/viewreportee`)
// navigate(`/viewreportee`)
setOpen({ "accordianOne": false, "accordianTwo": false })
} else {
navigate("/")
}
}, []);
if ( reportees.length && viewReportee)
if (viewReportee!==null)
return (
<div className="p-4" >
<div className="bg-white p-3 rounded-md">
<div className="flex justify-between">
{/* <img src="/generic-male-avatar-rectangular.jpg" width="100px" height="100px" /> */}
<div className="my-1">
{/* <div className="my-1">
<p>
<span className="font-medium">Employee Name : </span> {viewReportee?.empName}
<span className="font-medium">Employee Name: </span> {viewReportee?.empName}
</p>
<p>
<span className="font-medium">Designation : </span> {viewReportee?.designation}
<span className="font-medium">Designation: </span> {viewReportee?.designation}
</p>
{/* <p>
<span className="font-medium">Email Id: </span> {viewReportee?.empEmail}
</p> */}
</div> */}
<div className="flex items-center">
<div>
<p className="font-medium mb-2">
Employee Name
</p>
<p className="font-medium">
Employee Id
</p>
</div>
<div>
<p className="mb-2"><span className="font-medium">:</span> {viewReportee?.empName}</p>
<p><span className="font-medium">:</span> {viewReportee?.empId}</p>
</div>
</div>
<div className="my-1">
{/* <div className="my-1">
<p>
<span className="font-medium">Role : </span> {viewReportee?.techStack}
<span className="font-medium">Role: </span> {viewReportee?.techStack}
</p>
<p>
<span className="font-medium">Employee Id: </span> {viewReportee?.empId}
</p>
{/* <p>
<span className="font-medium">Total Score : </span> {viewReportee?.score}
</p> */}
{/* <p>
<span className="font-medium">Allocated To : </span> {viewReportee?.project}
</p> */}
</div> */}
<div className="flex items-center">
<div>
<p className="font-medium mb-2">
Designation
</p>
<p className="font-medium">
Role
</p>
</div>
<div>
<p className="mb-2"><span className="font-medium">:</span> {viewReportee?.designation}</p>
<p><span className="font-medium">:</span> {viewReportee?.techStack}</p>
</div>
</div>
<div className="flex flex-col justify-center items-center">
<div className={`w-[40px] h-[40px] rounded-full flex items-center text-white justify-center ${scoreColor(viewReportee?.score)}`}>
......@@ -137,15 +153,14 @@ function Viewreportee() {
<span className="text-blue-400 font-semibold">Total Score</span>
</div>
</div>
</div>
</div>
<div className="">
<div className="">
<Accordion title="Duties" open={open.accordianOne} handleAccordian={handleAccordian} data={activities?.duties} handleAddActivity={handleAddActivity} />
<Accordion title="Duties" open={open.accordianOne} handleAccordian={handleAccordian} data={dutiesReports} handleAddActivity={handleAddActivity} />
</div>
<div className="">
<Accordion title="Initiatives" open={open.accordianTwo} handleAccordian={handleAccordian} data={activities?.initiative} handleAddActivity={handleAddActivity} />
<Accordion title="Initiatives" open={open.accordianTwo} handleAccordian={handleAccordian} data={initiativeReports} handleAddActivity={handleAddActivity} />
</div>
</div>
</div>
......
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { base_url } from "../../utils/constants";
import axios from "axios";
const initialState = {
totalReporteesData: [],
activitiesData:null,
// totalReporteesData: [],
activitiesData:[],
loading: false,
error: null,
fromDate: null,
toDate: null,
};
export const fetchReportstableData = createAsyncThunk("getreportees", async (data) => {
return await axios.post(`${base_url}/getreportees`, data)
export const fetchReportesActivitiesData = createAsyncThunk("gettotalactivities", async (data) => {
return await axios.post(`${base_url}/getActivities`, data)
.then((response) => response.data);
});
const exporttableSlice = createSlice({
name: "totalReportees",
initialState,
reducers: {
resetActivitiesData: (state) => {
state.activitiesData = []
},
resetReporteesTableData:() => {
return initialState
},
setPastMonth: (state) => {
const toDate = new Date();
const fromDate = new Date();
fromDate.setMonth(fromDate.getMonth() - 1);
state.fromDate = fromDate;
state.toDate = toDate;
console.log(fromDate,toDate)
},
setPastTwoMonths: (state) => {
const toDate = new Date();
const fromDate = new Date();
fromDate.setMonth(fromDate.getMonth() - 3);
state.fromDate = fromDate;
state.toDate = toDate;
console.log(fromDate,toDate)
},
setPastsixMonths: (state) => {
const toDate = new Date();
const fromDate = new Date();
fromDate.setMonth(fromDate.getMonth() - 6);
state.fromDate = fromDate;
state.toDate = toDate;
console.log(fromDate,toDate)
},
setPasttwelvemonths: (state) => {
const toDate = new Date();
const fromDate = new Date();
fromDate.setMonth(fromDate.getMonth() - 12);
state.fromDate = fromDate;
state.toDate = toDate;
console.log(fromDate,toDate)
},
},
extraReducers: (builder) => {
builder.addCase(fetchReportstableData.pending, (state) => {
builder.addCase(fetchReportesActivitiesData.pending, (state) => {
state.loading = true;
state.error = "pending";
});
builder.addCase(fetchReportstableData.fulfilled, (state, action) => {
builder.addCase(fetchReportesActivitiesData.fulfilled, (state, action) => {
// console.log(action.payload.activities)
state.loading = false;
state.totalReporteesData = action.payload.data;
state.activitiesData = action.payload.activities ;
state.error = "";
});
builder.addCase(fetchReportstableData.rejected, (state, action) => {
builder.addCase(fetchReportesActivitiesData.rejected, (state, action) => {
state.loading = false;
state.totalReporteesData = null;
state.activitiesData = null;
state.error = action.error || "Something went wrong!";
});
},
});
export const {resetReporteesTableData,setPastMonth,setPastTwoMonths,setPastsixMonths,setPasttwelvemonths} = exporttableSlice.actions;
export const {resetReporteesTableData, resetActivitiesData} = exporttableSlice.actions;
export default exporttableSlice.reducer;
......@@ -4,12 +4,15 @@ import axios from "axios";
const initialState = {
reportees: [],
reporteeId: null,
viewReportee:null,
totalCount:0,
loading: false,
error: null,
currPage:1,
pagesCount:1
pagesCount:1,
sortKey: null,
sortOrder: 'asc'
};
export const fetchReportees = createAsyncThunk("getreportees", async (data) => {
......@@ -25,24 +28,25 @@ const reporteesSlice = createSlice({
return initialState
},
setViewReportee:(state,action)=>{
const reportee=state.reportees.find((reportee)=>reportee.empId==action.payload)
if(!reportee){
return {
...state,
viewReportee: null
viewReportee: action.payload
}
} else {
return {
...state,
viewReportee: reportee
}
}
},
setReporteeId: (state, action) => {
state.reporteeId = action.payload
},
setCurrPage: (state, action) => {
state.currPage = action.payload
},
setPagesCount: (state, action) => {
state.pagesCount = action.payload
},
setSortKey: (state, action) => {
state.sortKey = action.payload
},
setSortOrder: (state, action) => {
state.sortOrder = action.payload
}
},
extraReducers: (builder) => {
......@@ -64,6 +68,6 @@ const reporteesSlice = createSlice({
},
});
export const {resetReportees,setViewReportee, setCurrPage, setPagesCount} = reporteesSlice.actions;
export const {resetReportees,setViewReportee, setCurrPage, setPagesCount, setSortKey, setSortOrder, setReporteeId} = reporteesSlice.actions;
export default reporteesSlice.reducer;
......@@ -4,6 +4,8 @@ import axios from "axios";
const initialState = {
reports: null,
dutiesReports: null,
initiativeReports: null,
defaultAvgScore:0,
initiativeAvgScore:0,
loading: false,
......@@ -13,6 +15,12 @@ const initialState = {
export const fetchReporteeActivities = createAsyncThunk("getReports", async (data) => {
return await axios
.post(`${base_url}/getActivities`, data)
.then((response) => {return {data:response.data?.activities, type:data.types}});
});
export const fetchActivitiesAvg = createAsyncThunk("getActivities-avg", async (data) => {
return await axios
.post(`${base_url}/getActivities-avg`, data)
.then((response) => response.data);
});
......@@ -23,39 +31,32 @@ const reportSlice = createSlice({
resetReports:() => {
return initialState
},
calculateDefaultScore:(state, action)=>{
if(action.payload===undefined){
return {...state,defaultAvgScore :0}
}else{
const dutiesItems = action.payload?.filter(item => item.type === "duties");
const totalDutiesScore =dutiesItems?.length? dutiesItems?.reduce((acc, curr) => acc+ Number(curr.score), 0):0;
const defaultAvgScore =totalDutiesScore===0?0: Number(totalDutiesScore) / Number(dutiesItems?.length);
return {...state,defaultAvgScore :Number(defaultAvgScore).toFixed(1)}
}
},
calculateInitiativeScore:(state,action)=>{
if(action.payload===undefined){
return {...state,initiativeAvgScore:0}
}
else{
const initiatiesItems = action.payload?.filter(item => item.type === "initiative");
const totalInitiateScore =initiatiesItems?.length? (initiatiesItems?.reduce((acc, curr) => acc+ Number(curr.score), 0)):0;
const initialAvgScore =totalInitiateScore===0?0: Number(totalInitiateScore) / Number(initiatiesItems?.length) ;
return {...state,initiativeAvgScore :Number(initialAvgScore).toFixed(1)}
}
},
},
extraReducers: (builder) => {
builder.addCase(fetchReporteeActivities.pending, (state) => {
return {...state,loading :true,error :"loading"}
});
builder.addCase(fetchReporteeActivities.fulfilled, (state, action) => {
return {...state,loading :false,error :"",reports:action.payload?.activities}
const {data, type} = action.payload;
return {...state,loading :false,error :"", [`${type[0]}Reports`]: data}
});
builder.addCase(fetchReporteeActivities.rejected, (state, action) => {
return {...state,loading :false,error :action.error || "Something went wrong!",reports:null}
});
// getActivities Api
builder.addCase(fetchActivitiesAvg.pending, (state) => {
return {...state,loading :true,error :"loading"}
});
builder.addCase(fetchActivitiesAvg.fulfilled, (state, action) => {
const avgScores = action.payload;
const dutiesAvg = avgScores.find(({type}) => type === "duties")
const initiatieAvg = avgScores.find(({type}) => type === "initiative")
return {...state,loading :false,error :"", defaultAvgScore: dutiesAvg?.avgScore.toFixed(1) || 0, initiativeAvgScore: initiatieAvg?.avgScore.toFixed(1) || 0}
});
builder.addCase(fetchActivitiesAvg.rejected, (state, action) => {
return {...state,loading :false,error :action.error || "Something went wrong!",reports:null}
});
},
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment