diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 2755f79f..7eb4d28a 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -10,25 +10,18 @@ import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import ConfirmModal from "../common/ConfirmModal"; import { useProfile } from "../../hooks/useProfile"; -const ExpenseList = () => { +const ExpenseList = ({filters}) => { const [deletingId, setDeletingId] = useState(null); const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { setViewExpense, setManageExpenseModal } = useExpenseContext(); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; const { profile } = useProfile(); - const filter = { - projectIds: [], - statusIds: [], - createdByIds: [], - paidById: [], - startDate: null, - endDate: null, - }; + const { mutate: DeleteExpense, isPending } = useDeleteExpense(); const { data, isLoading, isError, isInitialLoading, error, isFetching } = - useExpenseList(10, currentPage, filter); + useExpenseList(10, currentPage, filters); const handleDelete = (id) => { setDeletingId(id); @@ -121,7 +114,7 @@ const ExpenseList = () => { className="dataTables_wrapper no-footer px-2" > @@ -150,7 +143,7 @@ const ExpenseList = () => {
Expense Type
- + {!isInitialLoading && groupExpensesByDateAndStatus(items).map( ({ date, expenses }) => ( <> - + - - - {expenses.map((expense) => ( - - {expenses.map((expense) => ( + + - - - diff --git a/src/components/Expenses/ExpenseSchema.js b/src/components/Expenses/ExpenseSchema.js index 705efe6b..dfa5ffec 100644 --- a/src/components/Expenses/ExpenseSchema.js +++ b/src/components/Expenses/ExpenseSchema.js @@ -88,4 +88,24 @@ export const defaultExpense = { export const ActionSchema = z.object({ comment : z.string().min(1,{message:"Please leave comment"}), selectedStatus: z.string().min(1, { message: "Please select a status" }), -}) \ No newline at end of file +}) + + +export const SearchSchema = z.object({ + projectIds: z.array(z.string()).optional(), + statusIds: z.array(z.string()).optional(), + createdByIds: z.array(z.string()).optional(), + paidById: z.array(z.string()).optional(), +startDate: z.string().optional(), +endDate: z.string().optional(), + +}); + +export const defaultFilter = { + projectIds:[], + statusIds:[], + createdByIds:[], + paidById:[], + startDate:"", + endDate:"" +} \ No newline at end of file diff --git a/src/components/Expenses/ExpenseSkeleton.jsx b/src/components/Expenses/ExpenseSkeleton.jsx index 59ae1792..85aa2b1a 100644 --- a/src/components/Expenses/ExpenseSkeleton.jsx +++ b/src/components/Expenses/ExpenseSkeleton.jsx @@ -135,7 +135,7 @@ const SkeletonCell = ({ width = "100%", height = 20, className = "", style = {} /> ); -export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 6 }) => { +export const ExpenseTableSkeleton = ({ groups = 3, rowsPerGroup = 3 }) => { return (
{ aria-label="Payment Mode: activate to sort column ascending" aria-sort="descending" > -
Payment Mode
+
Payment Mode
{
Paid By
{
{formatUTCToLocalTime(date)}
+
{expense.expensesType?.name || "N/A"} + {expense.paymentMode?.name || "N/A"} +
{
+ {expense?.amount}
{ - const [startDateString, endDateString] = dateStr.split(" to "); + const [startDateString, endDateString] = dateStr.split(" To "); onRangeChange?.({ startDate: startDateString, endDate: endDateString }); }, }); @@ -45,13 +48,29 @@ const DateRangePicker = ({ }, [onRangeChange, DateDifference, endDateMode]); return ( - +
+ + + +
+ ); }; diff --git a/src/components/common/SelectMultiple.jsx b/src/components/common/SelectMultiple.jsx index 272f8944..0873637e 100644 --- a/src/components/common/SelectMultiple.jsx +++ b/src/components/common/SelectMultiple.jsx @@ -1,12 +1,13 @@ import React, { useState, useEffect, useRef } from "react"; import { useFormContext } from "react-hook-form"; +import { createPortal } from "react-dom"; import "./MultiSelectDropdown.css"; const SelectMultiple = ({ name, options = [], label = "Select options", - labelKey = "name", + labelKey = "name", // Can now be a function or a string valueKey = "id", placeholder = "Please select...", IsLoading = false, @@ -16,11 +17,18 @@ const SelectMultiple = ({ const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(""); + const containerRef = useRef(null); const dropdownRef = useRef(null); + const [dropdownStyles, setDropdownStyles] = useState({ top: 0, left: 0, width: 0 }); + useEffect(() => { const handleClickOutside = (e) => { - if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + if ( + containerRef.current && + !containerRef.current.contains(e.target) && + (!dropdownRef.current || !dropdownRef.current.contains(e.target)) + ) { setIsOpen(false); } }; @@ -28,6 +36,21 @@ const SelectMultiple = ({ return () => document.removeEventListener("mousedown", handleClickOutside); }, []); + useEffect(() => { + if (isOpen && containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setDropdownStyles({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + width: rect.width, + }); + } + }, [isOpen]); + + const getLabel = (item) => { + return typeof labelKey === "function" ? labelKey(item) : item[labelKey]; + }; + const handleCheckboxChange = (value) => { const updated = selectedValues.includes(value) ? selectedValues.filter((v) => v !== value) @@ -36,96 +59,113 @@ const SelectMultiple = ({ setValue(name, updated, { shouldValidate: true }); }; - const filteredOptions = options.filter((item) => - item[labelKey]?.toLowerCase().includes(searchText.toLowerCase()) - ); + const filteredOptions = options.filter((item) => { + const label = getLabel(item); + return label?.toLowerCase().includes(searchText.toLowerCase()); + }); - return ( -
- - -
setIsOpen((prev) => !prev)} - > - 0 - ? "placeholder-style-selected" - : "placeholder-style" - } - > -
- {selectedValues.length > 0 ? ( - selectedValues.map((val) => { - const found = options.find((opt) => opt[valueKey] === val); - return ( - - {found ? found[labelKey] : ""} - - ); - }) - ) : ( - {placeholder} - )} -
-
- + const dropdownElement = ( +
+
+ setSearchText(e.target.value)} + className="multi-select-dropdown-search-input" + style={{ width: "100%", padding: 4 }} + />
- {isOpen && ( -
-
+ {filteredOptions.map((item) => { + const labelVal = getLabel(item); + const valueVal = item[valueKey]; + const isChecked = selectedValues.includes(valueVal); + + return ( +
setSearchText(e.target.value)} - className="multi-select-dropdown-search-input" + type="checkbox" + className="custom-checkbox form-check-input" + checked={isChecked} + onChange={() => handleCheckboxChange(valueVal)} + style={{ marginRight: 8 }} /> +
+ ); + })} - {filteredOptions.map((item) => { - const labelVal = item[labelKey]; - const valueVal = item[valueKey]; - const isChecked = selectedValues.includes(valueVal); - - return ( -
- handleCheckboxChange(valueVal)} - /> - -
- ); - })} - {!IsLoading && filteredOptions.length === 0 && ( -
- -
- )} - {IsLoading && filteredOptions.length === 0 && ( -
- -
- )} + {!IsLoading && filteredOptions.length === 0 && ( +
+ +
+ )} + {IsLoading && filteredOptions.length === 0 && ( +
+
)}
); + + return ( + <> +
+ + +
setIsOpen((prev) => !prev)} + style={{ cursor: "pointer" }} + > + 0 ? "placeholder-style-selected" : "placeholder-style" + } + > +
+ {selectedValues.length > 0 ? ( + selectedValues.map((val) => { + const found = options.find((opt) => opt[valueKey] === val); + const label = found ? getLabel(found) : ""; + return ( + + {label} + + ); + }) + ) : ( + {placeholder} + )} +
+
+ +
+
+ + {isOpen && createPortal(dropdownElement, document.body)} + + ); }; export default SelectMultiple; diff --git a/src/pages/Expense/ExpensePage.jsx b/src/pages/Expense/ExpensePage.jsx index b405f1c3..90624d61 100644 --- a/src/pages/Expense/ExpensePage.jsx +++ b/src/pages/Expense/ExpensePage.jsx @@ -1,5 +1,17 @@ -import React, { createContext, useContext, useState } from "react"; - +import React, { + createContext, + useContext, + useState, + useRef, + useEffect, +} from "react"; +import { + useForm, + useFieldArray, + FormProvider, + useFormContext, + Controller, +} from "react-hook-form"; import ExpenseList from "../../components/Expenses/ExpenseList"; import ViewExpense from "../../components/Expenses/ViewExpense"; import Breadcrumb from "../../components/common/Breadcrumb"; @@ -8,11 +20,88 @@ import PreviewDocument from "../../components/Expenses/PreviewDocument"; import ManageExpense from "../../components/Expenses/ManageExpense"; import { useProjectName } from "../../hooks/useProjects"; import { useExpenseStatus } from "../../hooks/masterHook/useMaster"; +import { + useEmployees, + useEmployeesAllOrByProjectId, +} from "../../hooks/useEmployees"; +import { useSelector } from "react-redux"; +import DateRangePicker from "../../components/common/DateRangePicker"; +import SelectMultiple from "../../components/common/SelectMultiple"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + defaultFilter, + SearchSchema, +} from "../../components/Expenses/ExpenseSchema"; + +const SelectDropdown = ({ + label, + options = [], + loading = false, + placeholder = "Select...", + valueKey = "id", + labelKey = "name", + selectedValues = [], + onChange, + isMulti = false, +}) => { + const handleChange = (e) => { + const selected = Array.from( + e.target.selectedOptions, + (option) => option.value + ); + onChange && onChange(selected); + }; + + return ( +
+ +
+ {options.map((option) => { + const checked = selectedValues.includes(option[valueKey]); + return ( +
+ { + let newSelected; + if (checked) { + newSelected = selectedValues.filter( + (val) => val !== option[valueKey] + ); + } else { + newSelected = [...selectedValues, option[valueKey]]; + } + onChange(newSelected); + }} + /> + +
+ ); + })} +
+
+ ); +}; export const ExpenseContext = createContext(); export const useExpenseContext = () => useContext(ExpenseContext); const ExpensePage = () => { + const [isOpen, setIsOpen] = useState(false); + const [filters,setFilter] = useState() + const dropdownRef = useRef(null); + const shouldCloseOnOutsideClick = useRef(false); + const selectedProjectId = useSelector( + (store) => store.localVariables.projectId + ); const [ManageExpenseModal, setManageExpenseModal] = useState({ IsOpen: null, expenseId: null, @@ -32,8 +121,81 @@ const ExpensePage = () => { setDocumentView, }; - const { projectNames } = useProjectName(); - const {} = useExpenseStatus(); + const methods = useForm({ + resolver: zodResolver(SearchSchema), + defaultValues: defaultFilter, + }); + const { + register, + handleSubmit, + control, + getValues, + trigger, + setValue, + watch, + reset, + formState: { errors }, + } = methods; + + const { projectNames, loading: projectLoading } = useProjectName(); + const { ExpenseStatus, loading: statusLoading, error } = useExpenseStatus(); + const { employees, loading: empLoading } = useEmployeesAllOrByProjectId( + true, + selectedProjectId, + true + ); + + const onSubmit = (data) => { + setFilter(data) + }; + const setDateRange = ({ startDate, endDate }) => { + setValue( + "startDate", + startDate ? new Date(startDate).toISOString().split("T")[0] : null + ); + setValue( + "endDate", + endDate ? new Date(endDate).toISOString().split("T")[0] : null + ); + }; + + const toggleDropdown = () => { + setIsOpen((prev) => { + shouldCloseOnOutsideClick.current = !prev; + return !prev; + }); + }; + + useEffect(() => { + function handleClickOutside(event) { + if ( + shouldCloseOnOutsideClick.current && + dropdownRef.current && + dropdownRef.current.contains(event.target) + ) { + setIsOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + const clearFilter =()=>{ + setFilter( + { + projectIds: [], + statusIds: [], + createdByIds: [], + paidById: [], + startDate: null, + endDate: null, + }) + reset() + + } + return (
@@ -46,50 +208,135 @@ const ExpensePage = () => {
-
- -
+
+
+ {isOpen && ( +
+ +
{ + onSubmit(data); + setIsOpen(false); + })} + > +
+ +
-
-
-
- - -
+
+
+ +
+ {ExpenseStatus.map((status) => ( + ( +
+ { + if (e.target.checked) { + onChange([...value, status.id]); + } else { + onChange( + value.filter( + (v) => v !== status.id + ) + ); + } + }} + /> + +
+ )} + /> + ))} +
+
+
-
- - -
+
+ + + `${item.firstName} ${item.lastName}` + } + valueKey="id" + IsLoading={empLoading} + /> + + `${item.firstName} ${item.lastName}` + } + valueKey="id" + IsLoading={empLoading} + /> +
+
+ + +
+ +
-
+ )}
@@ -115,7 +362,7 @@ const ExpensePage = () => {
- + {ManageExpenseModal.IsOpen && (