Compare commits

..

5 Commits

20 changed files with 298 additions and 323 deletions

View File

@ -4,6 +4,7 @@ import { ToastContainer } from "react-toastify";
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from "./layouts/AuthLayout";
import ModalProvider from "./ModalProvider";
@ -11,6 +12,7 @@ const App = () => {
return (
<div className="app">
<QueryClientProvider client={queryClient}>
<ModalProvider/>
<DireProvider>
<AppRoutes />
</DireProvider>

View File

@ -1,112 +0,0 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
} from "react";
const ModalContext = createContext();
export const useModal = () => useContext(ModalContext);
// ModalProvider to manage modal state and expose functionality to the rest of the app
export const ModalProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [modalContent, setModalContent] = useState(null);
const [onSubmit, setOnSubmit] = useState(null);
const [modalSize, setModalSize] = useState("lg");
// Ref to track the modal content element
const modalRef = useRef(null);
const openModal = (content, onSubmitCallback, size = "lg") => {
setModalContent(content); // Set modal content dynamically
setOnSubmit(() => onSubmitCallback); // Set the submit handler dynamically
setIsOpen(true); // Open the modal
setModalSize(size); // Set the modal size
};
// Function to close the modal
const closeModal = () => {
setIsOpen(false); // Close the modal
setModalContent(null); // Clear modal content
setOnSubmit(null); // Clear the submit callback
setModalSize("lg"); // Reset modal size
};
useEffect(() => {
const handleEscape = (event) => {
if (event.key === "Escape") {
closeModal();
}
};
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, []);
useEffect(() => {
const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
closeModal();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<ModalContext.Provider
value={{
isOpen,
openModal,
closeModal,
modalContent,
modalSize,
onSubmit,
}}
>
{children}
{isOpen && (
<div style={overlayStyles}>
<div
ref={modalRef}
style={{
...modalStyles,
maxWidth: modalSize === "sm" ? "400px" : "800px",
}}
>
<div>{modalContent}</div>
</div>
</div>
)}
</ModalContext.Provider>
);
};
const overlayStyles = {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1050,
};
const modalStyles = {
backgroundColor: "white",
padding: "20px",
borderRadius: "5px",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
width: "90%",
maxWidth: "800px",
};

14
src/ModalProvider.jsx Normal file
View File

@ -0,0 +1,14 @@
import React from 'react'
import Modal from './components/common/Modal'
import ManageOrganization from './components/Organization/ManageOrganization'
const ModalProvider = () => {
return (
<>
<ManageOrganization/>
</>
)
}
export default ModalProvider

View File

@ -4,7 +4,6 @@ import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus";
import { useSelector, useDispatch } from "react-redux";
import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice";
import DateRangePicker from "../common/DateRangePicker";
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
import eventBus from "../../services/eventBus";

View File

@ -5,7 +5,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import TimePicker from "../common/TimePicker";
import { usePositionTracker } from "../../hooks/usePositionTracker";
import { useDispatch, useSelector } from "react-redux";
import { markAttendance } from "../../slices/apiSlice/attedanceLogsSlice";
import showToast from "../../services/toastService";
import { checkIfCurrentDate } from "../../utils/dateUtils";
import { useMarkAttendance } from "../../hooks/useAttendance";

View File

@ -1,9 +1,7 @@
import React, { act, useEffect, useState } from 'react'
import useAttendanceStatus, { ACTIONS } from '../../hooks/useAttendanceStatus';
// import AttendanceRepository from '../../repositories/AttendanceRepository';
import { useDispatch, useSelector } from 'react-redux';
import { usePositionTracker } from '../../hooks/usePositionTracker';
import {markCurrentAttendance} from '../../slices/apiSlice/attendanceAllSlice';
import {cacheData, getCachedData, useSelectedProject} from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
import { useMarkAttendance } from '../../hooks/useAttendance';

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react";
import moment from "moment";
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
import { useDispatch, useSelector } from "react-redux";
import { fetchEmployeeAttendanceData } from "../../slices/apiSlice/employeeAttendanceSlice";
import usePagination from "../../hooks/usePagination";
import Avatar from "../common/Avatar";
import { convertShortTime } from "../../utils/dateUtils";

View File

@ -0,0 +1,85 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { FormProvider, useForm } from "react-hook-form";
import {
defaultOrganizationValues,
organizationSchema,
} from "./OrganizationSchema";
import Modal from "../common/Modal";
import { useOrganization } from "../../hooks/useDirectory";
import { useOrganizationModal } from "../../hooks/useOrganization";
import Label from "../common/Label";
import SelectMultiple from "../common/SelectMultiple";
const ManageOrganization = () => {
const orgModal = useOrganizationModal();
const method = useForm({
resolver: zodResolver(organizationSchema),
defaultValues: defaultOrganizationValues,
});
const { handleSubmit, watch, register } = method;
const onSubmit = () => {};
const contentBody = (
<FormProvider {...method}>
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-1 text-start">
<Label htmlFor="organization" required>
Organization Name
</Label>
<input className="form-control form-control-sm" />
</div>
<div className="mb-1 text-start">
<Label htmlFor="organization" required>
Contact Person
</Label>
<input className="form-control form-control-sm" />
</div>
<div className="mb-1 text-start">
<Label htmlFor="organization" required>
Contact Number
</Label>
<input className="form-control form-control-sm" />
</div>
<div className="mb-1 text-start">
<Label htmlFor="organization" required>
Email Address
</Label>
<input className="form-control form-control-sm" />
</div>
<div className="mb-1 text-start">
<SelectMultiple
name="serviceIds"
label="Services"
required={true}
valueKey="id"
/>
</div>
<div className="mb-1 text-start">
<Label htmlFor="organization" required>
Address
</Label>
<textarea className="form-control form-control-sm" rows={2} />
</div>
<div className="d-flex justify-content-end gap-2 my-2">
<button className="btn btn-sm btn-secondary">Cancel</button>
<button className="btn btn-sm btn-primary">Submit</button>
</div>
</form>
</FormProvider>
);
return (
<Modal
isOpen={orgModal.isOpen}
onClose={orgModal.onClose}
onSubmit={onSubmit}
title={"Manage Organization"}
actionLabel={"Submit"}
body={contentBody}
/>
);
};
export default ManageOrganization;

View File

@ -0,0 +1,32 @@
import { array, z } from "zod";
const phoneRegex = /^\+?[1-9]\d{6,14}$/;
export const organizationSchema = z.object({
organizationName: z.string().min(1, { message: "Name is required" }),
contactPhone: z
.string()
.trim()
.min(7)
.max(20)
.regex(phoneRegex, { message: "Invalid phone number" }),
contactPerson: z.string().min(1, { message: "Person name required" }),
address: z.string().min(1, { message: "Address is required!" }),
email: z
.string()
.min(1, { message: "Email is required" })
.email("Invalid email address"),
listOfServiceId: z
.array(z.string())
.min(1, { message: "Please insert service id" }),
});
export const defaultOrganizationValues = {
organizationSchema:"",
contactPhone:"",
contactPerson:"",
address:"",
email:"",
listOfServiceId:[]
}

View File

@ -0,0 +1,55 @@
import { useCallback } from "react";
const Modal = ({
isOpen,
onClose,
title,
body,
disabled,
size="md",
position="top",
}) => {
const handleClose = useCallback(() => {
if (disabled) return;
onClose();
}, [disabled, onClose]);
if (!isOpen) return null;
return (
<div
className="modal fade show"
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.6)" }}
tabIndex="-1"
role="dialog"
>
<div className={`modal-dialog modal-${size} modal-dialog-${position}`} role="document">
<div className="modal-content text-white shadow-lg">
{/* Header */}
<div className="modal-header pb-2 border-0">
<h5 className="modal-title">{title}</h5>
<button
type="button"
className="btn-close btn-close-white"
onClick={handleClose}
aria-label="Close"
>
</button>
</div>
{/* Body */}
<div className="modal-body pt-0">{body}</div>
</div>
</div>
</div>
);
};
export default Modal;

View File

@ -2,15 +2,17 @@ import React, { useState, useEffect, useRef } from "react";
import { useFormContext } from "react-hook-form";
import { createPortal } from "react-dom";
import "./MultiSelectDropdown.css";
import Label from "./Label";
const SelectMultiple = ({
name,
options = [],
label = "Select options",
labelKey = "name", // Can now be a function or a string
labelKey = "name",
valueKey = "id",
placeholder = "Please select...",
IsLoading = false,
required = false,
}) => {
const { setValue, watch } = useFormContext();
const selectedValues = watch(name) || [];
@ -20,7 +22,11 @@ const SelectMultiple = ({
const containerRef = useRef(null);
const dropdownRef = useRef(null);
const [dropdownStyles, setDropdownStyles] = useState({ top: 0, left: 0, width: 0 });
const [dropdownStyles, setDropdownStyles] = useState({
top: 0,
left: 0,
width: 0,
});
useEffect(() => {
const handleClickOutside = (e) => {
@ -100,8 +106,14 @@ const SelectMultiple = ({
return (
<div
key={valueVal}
className={`multi-select-dropdown-option ${isChecked ? "selected" : ""}`}
style={{ display: "flex", alignItems: "center", padding: "4px 8px" }}
className={`multi-select-dropdown-option ${
isChecked ? "selected" : ""
}`}
style={{
display: "flex",
alignItems: "center",
padding: "4px 8px",
}}
>
<input
type="checkbox"
@ -130,8 +142,14 @@ const SelectMultiple = ({
return (
<>
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
<label className="form-label mb-1">{label}</label>
<div
ref={containerRef}
className="multi-select-dropdown-container"
style={{ position: "relative" }}
>
<Label htmlFor={name} required={required}>
{label}
</Label>
<div
className="multi-select-dropdown-header"
@ -140,7 +158,9 @@ const SelectMultiple = ({
>
<span
className={
selectedValues.length > 0 ? "placeholder-style-selected" : "placeholder-style"
selectedValues.length > 0
? "placeholder-style-selected"
: "placeholder-style"
}
>
<div className="selected-badges-container">
@ -149,7 +169,10 @@ const SelectMultiple = ({
const found = options.find((opt) => opt[valueKey] === val);
const label = found ? getLabel(found) : "";
return (
<span key={val} className="badge badge-selected-item mx-1 mb-1">
<span
key={val}
className="badge badge-selected-item mx-1 mb-1"
>
{label}
</span>
);

View File

@ -0,0 +1,16 @@
import { useSelector, useDispatch } from "react-redux";
import { toggleOrgModal,openOrgModal,closeOrgModal } from "../slices/localVariablesSlice";
export const useOrganizationModal = () => {
const dispatch = useDispatch();
const isOpen = useSelector((state) => state.localVariables.OrganizationModal.isOpen);
return {
isOpen,
onOpen: () => dispatch(openOrgModal()),
onClose: () => dispatch(closeOrgModal()),
Togggle:()=> dispatch(toggleOrgModal(isOpen))
};
};

View File

@ -2,11 +2,9 @@
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
// import { MasterDataProvider } from "./provider/MasterDataContext";
import { Provider } from 'react-redux';
import { store } from './store/store';
import { ModalProvider } from './ModalContext.jsx';
import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx';
import { ModalProvider1 } from './pages/Gallary/ModalContext.jsx';
@ -15,15 +13,12 @@ createRoot(document.getElementById('root')!).render(
// <StrictMode>
// <MasterDataProvider>
<Provider store={ store }>
<ModalProvider>
<ChangePasswordProvider >
<ModalProvider1>
<App />
</ModalProvider1>
</ChangePasswordProvider>
</ModalProvider>
</Provider>
// </MasterDataProvider>
// </StrictMode>,
)

View File

@ -0,0 +1,43 @@
import React from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import { useOrganizationModal } from "../../hooks/useOrganization";
const OrganizationPage = () => {
const orgModal = useOrganizationModal()
return (
<div className="container-fluid">
<Breadcrumb
data={[{ label: "Home", link: "/" }, { label: "Organizations" }]}
/>
<div className="card my-3 px-sm-2 px-0">
<div className="card-body py-2 px-3">
<div className="row align-items-center">
<div className="col-6 ">
<div className="d-flex align-items-center">
<input
type="search"
className="form-control form-control-sm w-auto"
placeholder="Search Organization"
aria-describedby="search-label"
/>
</div>
</div>
<div className="col-6 text-end mt-2 mt-sm-0">
<button
type="button"
className="p-1 me-1 m-sm-0 bg-primary rounded-circle"
title="Add New Organization"
onClick={()=>orgModal.onOpen()}
>
<i className="bx bx-plus fs-4 text-white"></i>
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default OrganizationPage;

View File

@ -49,6 +49,8 @@ import MainResetPasswordPage from "../pages/authentication/MainResetPasswordPage
import TenantPage from "../pages/Tenant/TenantPage";
import { Navigate } from "react-router-dom";
import CreateTenant from "../pages/Tenant/CreateTenant";
;
import OrganizationPage from "../pages/Organization/OrganizationPage";
import LandingPage from "../pages/Home/LandingPage";
const router = createBrowserRouter(
[
@ -96,6 +98,7 @@ const router = createBrowserRouter(
{ path: "/tenants/new-tenant", element: <CreateTenant /> },
{ path: "/tenant/:tenantId", element: <SuperTenantDetails /> },
{ path: "/tenant/self", element: <SelfTenantDetails /> },
{ path: "/organizations", element: <OrganizationPage /> },
{ path: "/help/support", element: <Support /> },
{ path: "/help/docs", element: <Documentation /> },
{ path: "/help/connect", element: <Connect /> },

View File

@ -1,93 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AttendanceRepository from '../../repositories/AttendanceRepository';
import {clearCacheKey} from '../apiDataManager';
// Fetch attendance data
export const fetchAttendanceData = createAsyncThunk(
'attendanceLogs/fetchAttendanceData',
async ( {projectId, fromDate, toDate}, thunkAPI ) =>
{
try {
const response = await AttendanceRepository.getAttendanceFilteredByDate(projectId, fromDate, toDate);
return response?.data?.filter((log) => log.checkInTime !== null && log.activity !== 0);
} catch (error) {
return thunkAPI.rejectWithValue(error.message);
}
}
);
export const markAttendance = createAsyncThunk(
'attendanceLogs/markAttendance', // Updated action type prefix
async ( formData, thunkAPI ) =>
{
try {
let newRecordAttendance = {
Id: formData.id || null,
comment: formData.description,
employeeID: formData.employeeId,
projectID: formData.projectId,
date: new Date().toISOString(),
markTime: formData.markTime,
latitude: formData.latitude.toString(),
longitude: formData.longitude.toString(),
action: formData.action,
image: null,
};
const response = await AttendanceRepository.markAttendance( newRecordAttendance );
return response.data;
} catch ( error )
{
const message = error?.response?.data?.message || error.message || "Error Occured During Api Call";
return thunkAPI.rejectWithValue(message);
}
}
);
// Attendance Logs Slice
const attendanceLogsSlice = createSlice({
name: 'attendanceLogs', // Updated slice name
initialState: {
data: [],
loading: false,
error: null,
},
reducers: {
setAttendanceData: (state, action) => {
state.data = action.payload;
},
},
extraReducers: (builder) => {
builder
// Fetch attendance data
.addCase(fetchAttendanceData.pending, (state) => {
state.loading = true;
})
.addCase(fetchAttendanceData.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchAttendanceData.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// Mark attendance - log attenace data
.addCase(markAttendance.fulfilled, (state, action) => {
const updatedRecord = action.payload;
const index = state.data.findIndex(item => item.id === updatedRecord.id);
if (index !== -1) {
state.data[index] = { ...state.data[index], ...updatedRecord };
} else {
state.data.push(updatedRecord);
}
});
},
});
export const { setAttendanceData } = attendanceLogsSlice.actions;
export default attendanceLogsSlice.reducer;

View File

@ -1,38 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AttendanceRepository from '../../repositories/AttendanceRepository';
import {clearCacheKey} from '../apiDataManager';
export const markCurrentAttendance = createAsyncThunk(
'attendanceCurrentDate/markAttendance',
async ( formData, {getState, dispatch, rejectWithValue} ) =>
{
const { projectId } = getState().localVariables
try
{
// Create the new attendance record
const newRecordAttendance = {
Id: formData.id || null,
comment: formData.description,
employeeID: formData.employeeId,
projectId: projectId,
date: new Date().toISOString(),
markTime: formData.markTime,
latitude: formData.latitude.toString(),
longitude: formData.longitude.toString(),
action: formData.action,
image: null,
};
const response = await AttendanceRepository.markAttendance(newRecordAttendance);
const markedAttendance = response.data
clearCacheKey("AttendanceLogs")
return markedAttendance;
} catch (error) {
console.error('Error marking attendance:', error);
return rejectWithValue(error.message); // Reject with error message
}
}
);

View File

@ -1,56 +0,0 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AttendanceRepository from '../../repositories/AttendanceRepository';
import { markAttendance } from './attedanceLogsSlice';
export const fetchEmployeeAttendanceData = createAsyncThunk(
'employeeAttendance/fetchEmployeeAttendanceData',
async ( {employeeId, fromDate, toDate}, thunkAPI ) =>
{
try {
const response = await AttendanceRepository.getAttendanceByEmployee( employeeId, fromDate, toDate );
// return response?.data?.filter((log) => log.checkInTime !== null && log.activity !== 0);
return response.data
} catch (error) {
return thunkAPI.rejectWithValue(error.message);
}
}
);
const employeeAttendancesSlice = createSlice({
name: 'employeeAttendance', // Updated slice name
initialState: {
data: [],
loading: false,
error: null,
},
reducers: {
setEmployeeAttendanceData: (state, action) => {
state.data = action.payload;
},
},
extraReducers: (builder) => {
builder
// Fetch attendance data
.addCase(fetchEmployeeAttendanceData.pending, (state) => {
state.loading = true;
})
.addCase(fetchEmployeeAttendanceData.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchEmployeeAttendanceData.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
},
});
export const { setEmployeeAttendanceData } = employeeAttendancesSlice.actions;
export default employeeAttendancesSlice.reducer;

View File

@ -10,7 +10,11 @@ const localVariablesSlice = createSlice({
endDate: null,
},
projectId: null,
reload:false
reload:false,
OrganizationModal:{
isOpen:false
}
},
reducers: {
@ -32,8 +36,18 @@ const localVariablesSlice = createSlice({
setDefaultDateRange: (state, action) => {
state.defaultDateRange = action.payload;
},
openOrgModal: (state) => {
state.OrganizationModal.isOpen = true;
},
closeOrgModal: (state) => {
state.OrganizationModal.isOpen = false;
},
toggleOrgModal: (state) => {
state.OrganizationModal.isOpen = !state.OrganizationModal.isOpen;
},
},
});
export const { changeMaster ,updateRegularizationCount,setProjectId,refreshData,setDefaultDateRange} = localVariablesSlice.actions;
export const { changeMaster ,updateRegularizationCount,setProjectId,refreshData,setDefaultDateRange,openOrgModal,closeOrgModal,toggleOrgModal} = localVariablesSlice.actions;
export default localVariablesSlice.reducer;

View File

@ -2,15 +2,12 @@ import { configureStore } from "@reduxjs/toolkit";
import apiCacheReducer from "../slices/apiCacheSlice";
import globalVariablesReducer from "../slices/globalVariablesSlice";
import localVariableRducer from "../slices/localVariablesSlice"
import attendanceReducer from "../slices/apiSlice/attedanceLogsSlice"
import employeeAttendanceReducer from "../slices/apiSlice/employeeAttendanceSlice"
export const store = configureStore({
reducer: {
apiCache: apiCacheReducer,
globalVariables: globalVariablesReducer,
localVariables:localVariableRducer,
attendanceLogs: attendanceReducer,
employeeAttendance: employeeAttendanceReducer,
},
});