filter_offcanvas : Added Offcanvas Filter Support for All Pages #310
@ -4,9 +4,30 @@ const FabContext = createContext();
|
||||
|
||||
export const FabProvider = ({ children }) => {
|
||||
const [actions, setActions] = useState([]);
|
||||
const [showTrigger, setShowTrigger] = useState(true);
|
||||
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
|
||||
const [offcanvas, setOffcanvas] = useState({
|
||||
isOpen: false,
|
||||
title: "",
|
||||
content: null,
|
||||
});
|
||||
|
||||
const openOffcanvas = (title, content) => {
|
||||
setOffcanvas({ isOpen: true, title, content });
|
||||
setTimeout(() => {
|
||||
const offcanvasElement = document.getElementById("globalOffcanvas");
|
||||
if (offcanvasElement) {
|
||||
const bsOffcanvas = new window.bootstrap.Offcanvas(offcanvasElement);
|
||||
bsOffcanvas.show();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
const setOffcanvasContent = (title, content) => {
|
||||
setOffcanvas(prev => ({ ...prev, title, content }));
|
||||
};
|
||||
|
||||
return (
|
||||
<FabContext.Provider value={{ actions, setActions }}>
|
||||
<FabContext.Provider value={{ actions, setActions, offcanvas, openOffcanvas, showTrigger, setShowTrigger,isOffcanvasOpen, setIsOffcanvasOpen, setOffcanvasContent, }}>
|
||||
{children}
|
||||
</FabContext.Provider>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React,{useEffect} from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
useDashboardProjectsCardData,
|
||||
@ -14,16 +14,25 @@ import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||
import ProjectProgressChart from "./ProjectProgressChart";
|
||||
import ProjectOverview from "../Project/ProjectOverview";
|
||||
import AttendanceOverview from "./AttendanceChart";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
|
||||
const Dashboard = () => {
|
||||
const { projectsCardData } = useDashboardProjectsCardData();
|
||||
const { teamsCardData } = useDashboardTeamsCardData();
|
||||
const { tasksCardData } = useDashboardTasksCardData();
|
||||
const {setShowTrigger} = useFab()
|
||||
|
||||
// Get the selected project ID from Redux store
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isAllProjectsSelected = projectId === null;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setShowTrigger(false);
|
||||
console.log("OffCanvas")
|
||||
return () => setShowTrigger(true);
|
||||
}, [setShowTrigger])
|
||||
|
||||
return (
|
||||
<div className="container-fluid mt-5">
|
||||
<div className="row gy-4">
|
||||
|
152
src/components/Expenses/ExpenseFilterPanel.jsx
Normal file
152
src/components/Expenses/ExpenseFilterPanel.jsx
Normal file
@ -0,0 +1,152 @@
|
||||
// components/Expense/ExpenseFilterPanel.jsx
|
||||
import React, { useEffect } from "react";
|
||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
defaultFilter,
|
||||
SearchSchema,
|
||||
} from "./ExpenseSchema";
|
||||
|
||||
import DateRangePicker from "../common/DateRangePicker";
|
||||
import SelectMultiple from "../common/SelectMultiple";
|
||||
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import { useExpenseStatus } from "../../hooks/masterHook/useMaster";
|
||||
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const ExpenseFilterPanel = ({ onApply }) => {
|
||||
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
||||
|
||||
const { projectNames, loading: projectLoading } = useProjectName();
|
||||
const { ExpenseStatus = [] } = useExpenseStatus();
|
||||
const { employees, loading: empLoading } = useEmployeesAllOrByProjectId(
|
||||
true,
|
||||
selectedProjectId,
|
||||
true
|
||||
);
|
||||
|
||||
const methods = useForm({
|
||||
resolver: zodResolver(SearchSchema),
|
||||
defaultValues: defaultFilter,
|
||||
});
|
||||
|
||||
const { control, handleSubmit, setValue, reset } = methods;
|
||||
|
||||
const isValidDate = (date) => date instanceof Date && !isNaN(date);
|
||||
const setDateRange = ({ startDate, endDate }) => {
|
||||
const parsedStart = new Date(startDate);
|
||||
const parsedEnd = new Date(endDate);
|
||||
setValue(
|
||||
"startDate",
|
||||
isValidDate(parsedStart) ? parsedStart.toISOString().split("T")[0] : null
|
||||
);
|
||||
setValue(
|
||||
"endDate",
|
||||
isValidDate(parsedEnd) ? parsedEnd.toISOString().split("T")[0] : null
|
||||
);
|
||||
};
|
||||
|
||||
const closePanel = () => {
|
||||
document.querySelector(".offcanvas.show .btn-close")?.click();
|
||||
};
|
||||
|
||||
const onSubmit = (data) => {
|
||||
onApply(data);
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
reset(defaultFilter);
|
||||
onApply(defaultFilter);
|
||||
closePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||
|
||||
<div className="mb-3 w-100">
|
||||
<DateRangePicker
|
||||
onRangeChange={setDateRange}
|
||||
endDateMode="today"
|
||||
DateDifference="6"
|
||||
dateFormat="DD-MM-YYYY"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="row g-2">
|
||||
<SelectMultiple
|
||||
name="projectIds"
|
||||
label="Select Projects"
|
||||
options={projectNames}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
IsLoading={projectLoading}
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="createdByIds"
|
||||
label="Select Creator"
|
||||
options={employees}
|
||||
labelKey={(item) => `${item.firstName} ${item.lastName}`}
|
||||
valueKey="id"
|
||||
IsLoading={empLoading}
|
||||
/>
|
||||
<SelectMultiple
|
||||
name="paidById"
|
||||
label="Select Paid By"
|
||||
options={employees}
|
||||
labelKey={(item) => `${item.firstName} ${item.lastName}`}
|
||||
valueKey="id"
|
||||
IsLoading={empLoading}
|
||||
/>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label ">Select Status</label>
|
||||
<div className="d-flex flex-wrap">
|
||||
{ExpenseStatus.map((status) => (
|
||||
<Controller
|
||||
key={status.id}
|
||||
control={control}
|
||||
name="statusIds"
|
||||
render={({ field: { value = [], onChange } }) => (
|
||||
<div className="d-flex align-items-center me-3 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
value={status.id}
|
||||
checked={value.includes(status.id)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
onChange(
|
||||
checked
|
||||
? [...value, status.id]
|
||||
: value.filter((v) => v !== status.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label className="ms-2 mb-0">{status.displayName}</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end py-3 gap-2">
|
||||
<button type="button" className="btn btn-secondary btn-xs" onClick={onClear}>
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary btn-xs">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseFilterPanel;
|
@ -28,9 +28,9 @@ const DateRangePicker = ({
|
||||
altFormat: "d-m-Y",
|
||||
defaultDate: [startDate, endDate],
|
||||
static: false,
|
||||
appendTo: document.body,
|
||||
// appendTo: document.body,
|
||||
clickOpens: true,
|
||||
maxDate: endDate, // ✅ Disable future dates
|
||||
maxDate: endDate,
|
||||
onChange: (selectedDates, dateStr) => {
|
||||
const [startDateString, endDateString] = dateStr.split(" To ");
|
||||
onRangeChange?.({ startDate: startDateString, endDate: endDateString });
|
||||
@ -51,23 +51,15 @@ const DateRangePicker = ({
|
||||
<div className={`col-${sm} col-sm-${md} px-1 position-relative`}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm ps-2 pe-5 "
|
||||
className="form-control form-control-sm ps-2 pe-5 me-4"
|
||||
placeholder="From to End"
|
||||
id="flatpickr-range"
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
<i
|
||||
className="bx bx-calendar calendar-icon cursor-pointer"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
right: "12px",
|
||||
transform: "translateY(-50%)",
|
||||
color: "#6c757d",
|
||||
fontSize: "1.1rem",
|
||||
|
||||
}}
|
||||
className="bx bx-calendar calendar-icon cursor-pointer position-absolute top-50 translate-middle-y "
|
||||
style={{right:"12px"}}
|
||||
></i>
|
||||
</div>
|
||||
|
||||
|
52
src/components/common/GlobalOffcanvas .jsx
Normal file
52
src/components/common/GlobalOffcanvas .jsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
|
||||
const GlobalOffcanvas = () => {
|
||||
const { offcanvas, setIsOffcanvasOpen } = useFab();
|
||||
const offcanvasRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const el = offcanvasRef.current;
|
||||
const bsOffcanvas = new bootstrap.Offcanvas(el);
|
||||
|
||||
const handleShow = () => setIsOffcanvasOpen(true);
|
||||
const handleHide = () => setIsOffcanvasOpen(false);
|
||||
|
||||
el.addEventListener("show.bs.offcanvas", handleShow);
|
||||
el.addEventListener("hidden.bs.offcanvas", handleHide);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener("show.bs.offcanvas", handleShow);
|
||||
el.removeEventListener("hidden.bs.offcanvas", handleHide);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="offcanvas offcanvas-end"
|
||||
tabIndex="-1"
|
||||
id="globalOffcanvas"
|
||||
ref={offcanvasRef}
|
||||
aria-labelledby="offcanvasLabel"
|
||||
data-bs-backdrop="false"
|
||||
data-bs-scroll="true"
|
||||
>
|
||||
<div className="offcanvas-header">
|
||||
<h5 className="offcanvas-title" id="offcanvasLabel">
|
||||
{offcanvas.title}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close text-reset"
|
||||
data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div className="offcanvas-body mx-0 flex-grow-0">
|
||||
{offcanvas.content || <p>No content</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalOffcanvas;
|
26
src/components/common/OffcanvasTrigger.jsx
Normal file
26
src/components/common/OffcanvasTrigger.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
|
||||
const OffcanvasTrigger = () => {
|
||||
const { openOffcanvas, offcanvas, showTrigger } = useFab();
|
||||
|
||||
if (!showTrigger || !offcanvas.content) return null;
|
||||
|
||||
const btn = (
|
||||
<i
|
||||
className="bx bx-slider-alt position-fixed p-2 bg-primary text-white rounded-start"
|
||||
onClick={() => openOffcanvas(offcanvas.title, offcanvas.content)}
|
||||
role="button"
|
||||
style={{
|
||||
top: "25%",
|
||||
right: "0%",
|
||||
cursor: "pointer",
|
||||
zIndex: 1056,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return createPortal(btn, document.body);
|
||||
};
|
||||
|
||||
export default OffcanvasTrigger;
|
@ -7,6 +7,8 @@ import Footer from "../components/Layout/Footer";
|
||||
import FloatingMenu from "../components/common/FloatingMenu";
|
||||
import { FabProvider } from "../Context/FabContext";
|
||||
import { useSelector } from "react-redux";
|
||||
import OffcanvasTrigger from "../components/common/OffcanvasTrigger";
|
||||
import GlobalOffcanvas from "../components/common/GlobalOffcanvas ";
|
||||
|
||||
const HomeLayout = () => {
|
||||
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
||||
@ -34,9 +36,11 @@ const HomeLayout = () => {
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<OffcanvasTrigger />
|
||||
<FloatingMenu />
|
||||
<div className="layout-overlay layout-menu-toggle"></div>
|
||||
</div>
|
||||
<GlobalOffcanvas />
|
||||
</div>
|
||||
</FabProvider>
|
||||
);
|
||||
|
@ -8,24 +8,19 @@ import {
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import AttendanceLog from "../../components/Activities/AttendcesLogs";
|
||||
import Attendance from "../../components/Activities/Attendance";
|
||||
// import AttendanceModel from "../../components/Activities/AttendanceModel";
|
||||
import showToast from "../../services/toastService";
|
||||
// import { useProjects } from "../../hooks/useProjects";
|
||||
import Regularization from "../../components/Activities/Regularization";
|
||||
import { useAttendance } from "../../hooks/useAttendance";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||
// import { markCurrentAttendance } from "../../slices/apiSlice/attendanceAllSlice";
|
||||
import { hasUserPermission } from "../../utils/authUtils";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { REGULARIZE_ATTENDANCE } from "../../utils/constants";
|
||||
import eventBus from "../../services/eventBus";
|
||||
// import AttendanceRepository from "../../repositories/AttendanceRepository";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
import CheckCheckOutmodel from "../../components/Activities/CheckCheckOutForm";
|
||||
import AttendLogs from "../../components/Activities/AttendLogs";
|
||||
// import Confirmation from "../../components/Activities/Confirmation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
const AttendancePage = () => {
|
||||
@ -35,11 +30,7 @@ const AttendancePage = () => {
|
||||
const loginUser = getCachedProfileData();
|
||||
var selectedProject = useSelector((store) => store.localVariables.projectId);
|
||||
const dispatch = useDispatch();
|
||||
// const {
|
||||
// attendance,
|
||||
// loading: attLoading,
|
||||
// recall: attrecall,
|
||||
// } = useAttendance(selectedProject);
|
||||
|
||||
const [attendances, setAttendances] = useState();
|
||||
const [empRoles, setEmpRoles] = useState(null);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
@ -53,33 +44,6 @@ const AttendancePage = () => {
|
||||
date: new Date().toLocaleDateString(),
|
||||
});
|
||||
|
||||
// const handler = useCallback(
|
||||
// (msg) => {
|
||||
// if (selectedProject == msg.projectId) {
|
||||
// const updatedAttendance = attendances.map((item) =>
|
||||
// item.employeeId === msg.response.employeeId
|
||||
// ? { ...item, ...msg.response }
|
||||
// : item
|
||||
// );
|
||||
// queryClient.setQueryData(["attendance", selectedProject], (oldData) => {
|
||||
// if (!oldData) return oldData;
|
||||
// return oldData.map((emp) =>
|
||||
// emp.employeeId === data.employeeId ? { ...emp, ...data } : emp
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// [selectedProject, attrecall]
|
||||
// );
|
||||
|
||||
// const employeeHandler = useCallback(
|
||||
// (msg) => {
|
||||
// if (attendances.some((item) => item.employeeId == msg.employeeId)) {
|
||||
// attrecall();
|
||||
// }
|
||||
// },
|
||||
// [selectedProject, attendances]
|
||||
// );
|
||||
useEffect(() => {
|
||||
if (selectedProject == null) {
|
||||
dispatch(setProjectId(projectNames[0]?.id));
|
||||
@ -117,32 +81,9 @@ const AttendancePage = () => {
|
||||
}
|
||||
}, [modelConfig, isCreateModalOpen]);
|
||||
|
||||
// useEffect(() => {
|
||||
// eventBus.on("attendance", handler);
|
||||
// return () => eventBus.off("attendance", handler);
|
||||
// }, [handler]);
|
||||
|
||||
// useEffect(() => {
|
||||
// eventBus.on("employee", employeeHandler);
|
||||
// return () => eventBus.off("employee", employeeHandler);
|
||||
// }, [employeeHandler]);
|
||||
return (
|
||||
<>
|
||||
{/* {isCreateModalOpen && modelConfig && (
|
||||
<div
|
||||
className="modal fade show"
|
||||
style={{ display: "block" }}
|
||||
id="check-Out-modalg"
|
||||
tabIndex="-1"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<AttendanceModel
|
||||
modelConfig={modelConfig}
|
||||
closeModal={closeModal}
|
||||
handleSubmitForm={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{isCreateModalOpen && modelConfig && (
|
||||
<GlobalModel
|
||||
isOpen={isCreateModalOpen}
|
||||
|
@ -38,6 +38,8 @@ import {
|
||||
VIEW_ALL_EXPNESE,
|
||||
VIEW_SELF_EXPENSE,
|
||||
} from "../../utils/constants";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
import ExpenseFilterPanel from "../../components/Expenses/ExpenseFilterPanel";
|
||||
|
||||
const SelectDropdown = ({
|
||||
label,
|
||||
@ -215,6 +217,22 @@ const ExpensePage = () => {
|
||||
reset();
|
||||
};
|
||||
|
||||
const { setOffcanvasContent, setShowTrigger } = useFab();
|
||||
useEffect(() => {
|
||||
setShowTrigger(true);
|
||||
|
||||
setOffcanvasContent(
|
||||
"Expense Filters",
|
||||
<ExpenseFilterPanel
|
||||
onApply={(data) => setFilter(data)}
|
||||
/>
|
||||
);
|
||||
return () => {
|
||||
setOffcanvasContent("", null);
|
||||
setShowTrigger(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExpenseContext.Provider value={contextValue}>
|
||||
<div className="container-fluid">
|
||||
@ -235,7 +253,7 @@ const ExpensePage = () => {
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<i
|
||||
className="bx bx-slider-alt ms-2"
|
||||
className="bx bx-slider-alt ms-2 d-none"
|
||||
role="button"
|
||||
aria-expanded={isOpen}
|
||||
style={{ cursor: "pointer" }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user