Merge pull request 'filter_offcanvas : Added Offcanvas Filter Support for All Pages' (#310) from filter_offcanvas into Feature_Expense

Reviewed-on: #310
Merged
This commit is contained in:
pramod.mahajan 2025-07-30 06:48:56 +00:00
commit cc2710abdc
9 changed files with 292 additions and 77 deletions

View File

@ -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>
);

View File

@ -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">

View 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;

View File

@ -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>

View 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;

View 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;

View File

@ -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>
);

View File

@ -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}

View File

@ -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" }}