diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css index 1ecc882c..5b780c72 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -25,6 +25,12 @@ font-size: 2rem; .text-md-b { font-weight: normal; } +.cursor-wait{ + cursor:wait; +} +.cursor-notallowed { + cursor: not-allowed; +} .text-xxs { font-size: 0.55rem; } /* 8px */ .text-xs { font-size: 0.75rem; } /* 12px */ @@ -294,3 +300,7 @@ font-weight: normal; .w-8-xl{ width: 2rem; } .w-10-xl{ width: 2.5rem; } } + +.cursor-not-allowed{ + cursor: not-allowed; +} diff --git a/src/components/Activities/Attendance.jsx b/src/components/Activities/Attendance.jsx index 9a082976..b7ddfaff 100644 --- a/src/components/Activities/Attendance.jsx +++ b/src/components/Activities/Attendance.jsx @@ -126,7 +126,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat checked={ShowPending} onChange={(e) => setShowPending(e.target.checked)} /> - + {attLoading ? ( @@ -223,50 +223,6 @@ const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizat )} - - - {!loading && finalFilteredData.length > ITEMS_PER_PAGE && ( - - )} ) : (
)}
+ {!loading && finalFilteredData.length > ITEMS_PER_PAGE && ( + + )} ); }; diff --git a/src/components/Activities/AttendcesLogs.jsx b/src/components/Activities/AttendcesLogs.jsx index 8f8a55ac..4660f956 100644 --- a/src/components/Activities/AttendcesLogs.jsx +++ b/src/components/Activities/AttendcesLogs.jsx @@ -15,6 +15,7 @@ import AttendanceRepository from "../../repositories/AttendanceRepository"; import { useAttendancesLogs } from "../../hooks/useAttendance"; import { queryClient } from "../../layouts/AuthLayout"; import { ITEMS_PER_PAGE } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; const usePagination = (data, itemsPerPage) => { const [currentPage, setCurrentPage] = useState(1); @@ -38,162 +39,172 @@ const usePagination = (data, itemsPerPage) => { }; const AttendanceLog = ({ handleModalData, searchTerm, organizationId }) => { -const selectedProject = useSelectedProject(); -const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); -const dispatch = useDispatch(); -const [loading, setLoading] = useState(false); -const [showPending, setShowPending] = useState(false); -const [isRefreshing, setIsRefreshing] = useState(false); + const selectedProject = useSelectedProject(); + const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" }); + const dispatch = useDispatch(); + const [loading, setLoading] = useState(false); + const [showPending, setShowPending] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const navigate = useNavigate(); -const today = new Date(); -today.setHours(0, 0, 0, 0); + const today = new Date(); + today.setHours(0, 0, 0, 0); -const yesterday = new Date(); -yesterday.setDate(yesterday.getDate() - 1); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); -const isSameDay = (dateStr) => { - if (!dateStr) return false; - const d = new Date(dateStr); - d.setHours(0, 0, 0, 0); - return d.getTime() === today.getTime(); -}; + const isSameDay = (dateStr) => { + if (!dateStr) return false; + const d = new Date(dateStr); + d.setHours(0, 0, 0, 0); + return d.getTime() === today.getTime(); + }; -const isBeforeToday = (dateStr) => { - if (!dateStr) return false; - const d = new Date(dateStr); - d.setHours(0, 0, 0, 0); - return d.getTime() < today.getTime(); -}; + const isBeforeToday = (dateStr) => { + if (!dateStr) return false; + const d = new Date(dateStr); + d.setHours(0, 0, 0, 0); + return d.getTime() < today.getTime(); + }; -const sortByName = (a, b) => { - const nameA = (a.firstName + a.lastName).toLowerCase(); - const nameB = (b.firstName + b.lastName).toLowerCase(); - return nameA.localeCompare(nameB); -}; + const sortByName = (a, b) => { + const nameA = (a.firstName + a.lastName).toLowerCase(); + const nameB = (b.firstName + b.lastName).toLowerCase(); + return nameA.localeCompare(nameB); + }; -const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs( - selectedProject, - dateRange.startDate, - dateRange.endDate, - organizationId -); - -const processedData = useMemo(() => { - const filteredData = showPending - ? data.filter((item) => item.checkOutTime === null) - : data; - - const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName); - const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName); - const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName); - const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime)); - const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName); - const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName); - - const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6]; - - const groupedByDate = sortedList.reduce((acc, item) => { - const date = (item.checkInTime || item.checkOutTime)?.split("T")[0]; - if (date) { - acc[date] = acc[date] || []; - acc[date].push(item); - } - return acc; - }, {}); - - const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a)); - return sortedDates.flatMap((date) => groupedByDate[date]); -}, [data, showPending]); - -const filteredSearchData = useMemo(() => { - if (!searchTerm) return processedData; - - const lowercased = searchTerm.toLowerCase(); - return processedData.filter((item) => - `${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased) + const { data = [], isLoading, error, refetch, isFetching } = useAttendancesLogs( + selectedProject, + dateRange.startDate, + dateRange.endDate, + organizationId ); -}, [processedData, searchTerm]); -const { - currentPage, - totalPages, - currentItems: paginatedAttendances, - paginate, - resetPage, -} = usePagination(filteredSearchData, 20); + const processedData = useMemo(() => { + const filteredData = showPending + ? data.filter((item) => item.checkOutTime === null) + : data; -useEffect(() => { - resetPage(); -}, [filteredSearchData]); + const group1 = filteredData.filter((d) => d.activity === 1 && isSameDay(d.checkInTime)).sort(sortByName); + const group2 = filteredData.filter((d) => d.activity === 4 && isSameDay(d.checkOutTime)).sort(sortByName); + const group3 = filteredData.filter((d) => d.activity === 1 && isBeforeToday(d.checkInTime)).sort(sortByName); + const group4 = filteredData.filter((d) => d.activity === 4 && isBeforeToday(d.checkOutTime)); + const group5 = filteredData.filter((d) => d.activity === 2 && isBeforeToday(d.checkOutTime)).sort(sortByName); + const group6 = filteredData.filter((d) => d.activity === 5).sort(sortByName); -const handler = useCallback( - (msg) => { - const { startDate, endDate } = dateRange; - const checkIn = msg.response.checkInTime.substring(0, 10); + const sortedList = [...group1, ...group2, ...group3, ...group4, ...group5, ...group6]; - if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) { - queryClient.setQueriesData(["attendanceLogs"], (oldData) => { - if (!oldData) { - queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] }); - return; - } - return oldData.map((record) => - record.id === msg.response.id ? { ...record, ...msg.response } : record - ); - }); - resetPage(); - } - }, - [selectedProject, dateRange, resetPage] -); + const groupedByDate = sortedList.reduce((acc, item) => { + const date = (item.checkInTime || item.checkOutTime)?.split("T")[0]; + if (date) { + acc[date] = acc[date] || []; + acc[date].push(item); + } + return acc; + }, {}); -useEffect(() => { - eventBus.on("attendance_log", handler); - return () => eventBus.off("attendance_log", handler); -}, [handler]); + const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a)); + return sortedDates.flatMap((date) => groupedByDate[date]); + }, [data, showPending]); -const employeeHandler = useCallback( - (msg) => { - const { startDate, endDate } = dateRange; - if (data.some((item) => item.employeeId == msg.employeeId)) { - refetch(); - } - }, - [data, refetch] -); + const filteredSearchData = useMemo(() => { + if (!searchTerm) return processedData; -useEffect(() => { - eventBus.on("employee", employeeHandler); - return () => eventBus.off("employee", employeeHandler); -}, [employeeHandler]); + const lowercased = searchTerm.toLowerCase(); + return processedData.filter((item) => + `${item.firstName} ${item.lastName}`.toLowerCase().includes(lowercased) + ); + }, [processedData, searchTerm]); + + const { + currentPage, + totalPages, + currentItems: paginatedAttendances, + paginate, + resetPage, + } = usePagination(filteredSearchData, 20); + + useEffect(() => { + resetPage(); + }, [filteredSearchData]); + + const handler = useCallback( + (msg) => { + const { startDate, endDate } = dateRange; + const checkIn = msg.response.checkInTime.substring(0, 10); + + if (selectedProject === msg.projectId && startDate <= checkIn && checkIn <= endDate) { + queryClient.setQueriesData(["attendanceLogs"], (oldData) => { + if (!oldData) { + queryClient.invalidateQueries({ queryKey: ["attendanceLogs"] }); + return; + } + return oldData.map((record) => + record.id === msg.response.id ? { ...record, ...msg.response } : record + ); + }); + resetPage(); + } + }, + [selectedProject, dateRange, resetPage] + ); + + useEffect(() => { + eventBus.on("attendance_log", handler); + return () => eventBus.off("attendance_log", handler); + }, [handler]); + + const employeeHandler = useCallback( + (msg) => { + const { startDate, endDate } = dateRange; + if (data.some((item) => item.employeeId == msg.employeeId)) { + refetch(); + } + }, + [data, refetch] + ); + + useEffect(() => { + eventBus.on("employee", employeeHandler); + return () => eventBus.off("employee", employeeHandler); + }, [employeeHandler]); return ( <>
-
- -
+
+ {/* Date Range Picker */} +
+ +
+ + {/* Pending Attendance Switch */} +
setShowPending(e.target.checked)} /> - +
+
+
{ const previousAttendance = arr[index - 1]; const previousDate = previousAttendance ? moment( - previousAttendance.checkInTime || - previousAttendance.checkOutTime - ).format("YYYY-MM-DD") + previousAttendance.checkInTime || + previousAttendance.checkOutTime + ).format("YYYY-MM-DD") : null; if (!previousDate || currentDate !== previousDate) { @@ -260,7 +271,12 @@ useEffect(() => { lastName={attendance.lastName} />
- + + navigate(`/employee/${attendance.employeeId}?for=attendance`) + } + className="text-heading text-truncate cursor-pointer" + > {attendance.firstName} {attendance.lastName} @@ -297,8 +313,7 @@ useEffect(() => { ) : (
- No data available for the selected date range. Please Select - another date. + No attendance record found in selected date range.
)} @@ -326,9 +341,8 @@ useEffect(() => { (pageNumber) => (
  • + )} + {ApprovedTaskRights && task.reportedDate && !task.approvedBy && ( + + )} + +
  • + + + ))} + + ))} + + + +
    + { + data?.data?.length > 0 && ( + + ) + } +
    ); }; diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 311fa763..4c09056a 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -14,11 +14,11 @@ import ProjectCompletionChart from "./ProjectCompletionChart"; import ProjectProgressChart from "./ProjectProgressChart"; import ProjectOverview from "../Project/ProjectOverview"; import AttendanceOverview from "./AttendanceChart"; +import ExpenseAnalysis from "./ExpenseAnalysis"; +import ExpenseStatus from "./ExpenseStatus"; +import ExpenseByProject from "./ExpenseByProject"; const Dashboard = () => { - const { projectsCardData } = useDashboardProjectsCardData(); - const { teamsCardData } = useDashboardTeamsCardData(); - const { tasksCardData } = useDashboardTasksCardData(); // Get the selected project ID from Redux store const projectId = useSelector((store) => store.localVariables.projectId); @@ -29,16 +29,16 @@ const Dashboard = () => {
    {isAllProjectsSelected && (
    - +
    )}
    - +
    - +
    {isAllProjectsSelected && ( @@ -56,11 +56,25 @@ const Dashboard = () => {
    +
    +
    + +
    +
    + +
    +
    + +
    +
    {!isAllProjectsSelected && ( -
    - {/* ✅ Removed unnecessary projectId prop */} +
    +
    )} +
    + +
    ); diff --git a/src/components/Dashboard/DashboardSkeleton.jsx b/src/components/Dashboard/DashboardSkeleton.jsx new file mode 100644 index 00000000..89d25636 --- /dev/null +++ b/src/components/Dashboard/DashboardSkeleton.jsx @@ -0,0 +1,110 @@ +import React from "react"; + +const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => ( +
    +); + +const skeletonStyle = ` +@keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} +`; + +export const ProjectCardSkeleton = () => { + return ( + <> + {/* Inject animation CSS once */} + + +
    + {/* Header */} +
    +
    + {" "} + Projects +
    +
    + + {/* Skeleton body */} +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; + +export const TeamsSkeleton = () => { + return ( + <> + + +
    + {/* Header */} +
    +
    + Teams +
    +
    + + {/* Skeleton Body */} +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; +export const TasksSkeleton = () => { + return ( + <> + + +
    + {/* Header */} +
    +
    + Tasks +
    +
    + + {/* Skeleton Body */} +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; diff --git a/src/components/Dashboard/ExpenseAnalysis.jsx b/src/components/Dashboard/ExpenseAnalysis.jsx new file mode 100644 index 00000000..f398e8b5 --- /dev/null +++ b/src/components/Dashboard/ExpenseAnalysis.jsx @@ -0,0 +1,167 @@ +import React, { useEffect, useMemo, useState } from "react"; +import Chart from "react-apexcharts"; +import { useExpenseAnalysis } from "../../hooks/useDashboard_Data"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { DateRangePicker1 } from "../common/DateRangePicker"; +import { FormProvider, useForm } from "react-hook-form"; +import { formatCurrency, localToUtc } from "../../utils/appUtils"; +import { useProjectName } from "../../hooks/useProjects"; + +const ExpenseAnalysis = () => { + const projectId = useSelectedProject(); + const [projectName, setProjectName] = useState("All Project"); + const { projectNames, loading } = useProjectName(); + + const methods = useForm({ + defaultValues: { startDate: "", endDate: "" }, + }); + + useEffect(() => { + if (projectId && projectNames?.length) { + const project = projectNames.find((p) => p.id === projectId); + setProjectName(project?.name || "All Project"); + } else { + setProjectName("All Project"); + } + }, [projectNames, projectId]); + + const { watch } = methods; + const [startDate, endDate] = watch(["startDate", "endDate"]); + + const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis( + projectId, + startDate ? localToUtc(startDate) : null, + endDate ? localToUtc(endDate) : null + ); + + if (isError) return
    {error.message}
    ; + + const report = data?.report ?? []; + const { labels, series, total } = useMemo(() => { + const labels = report.map((item) => item.projectName); + const series = report.map((item) => item.totalApprovedAmount || 0); + const total = formatCurrency(data?.totalAmount || 0); + return { labels, series, total }; + }, [report, data?.totalAmount]); + + const donutOptions = { + chart: { type: "donut" }, + labels, + legend: { show: false }, + dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` }, + colors: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"], + plotOptions: { + pie: { + donut: { + size: "70%", + labels: { + show: true, + total: { + show: true, + label: "Total", + fontSize: "16px", + formatter: () => `${total}`, + }, + }, + }, + }, + }, + responsive: [ + { + breakpoint: 576, // mobile breakpoint + options: { + chart: { width: "100%" }, + }, + }, + ], + }; + + return ( + <> + +
    +
    +
    Expense Breakdown
    + {/*

    Category Wise Expense Breakdown

    */} +

    {projectName}

    +
    + +
    + + + +
    +
    + + {/* Card body */} +
    + {isLoading && ( +
    + Loading... +
    + )} + + {!isLoading && report.length === 0 && ( +
    No data found
    + )} + + {!isLoading && report.length > 0 && ( + <> + {isFetching && ( +
    + Loading... +
    + )} + +
    + +
    + +
    +
    + {report.map((item, idx) => ( +
    +
    + + + +
    +
    + {item.projectName} + + {formatCurrency(item.totalApprovedAmount)} + +
    +
    + ))} +
    +
    + + )} +
    + + {/* Header */} + + + ); +}; + +export default ExpenseAnalysis; diff --git a/src/components/Dashboard/ExpenseByProject.jsx b/src/components/Dashboard/ExpenseByProject.jsx new file mode 100644 index 00000000..96e3fb56 --- /dev/null +++ b/src/components/Dashboard/ExpenseByProject.jsx @@ -0,0 +1,178 @@ +import React, { useState, useEffect } from "react"; +import Chart from "react-apexcharts"; +import { useExpenseType } from "../../hooks/masterHook/useMaster"; +import { useSelector } from "react-redux"; +import { useExpenseDataByProject } from "../../hooks/useDashboard_Data"; +import { formatCurrency } from "../../utils/appUtils"; +import { formatDate_DayMonth } from "../../utils/dateUtils"; +import { useProjectName } from "../../hooks/useProjects"; +import { useSelectedProject } from "../../slices/apiDataManager"; + +const ExpenseByProject = () => { + const projectId = useSelector((store) => store.localVariables.projectId); + const [projectName, setProjectName] = useState("All Project"); + const [range, setRange] = useState("12M"); + const { projectNames, loading } = useProjectName(); + const [selectedType, setSelectedType] = useState(""); + const [viewMode, setViewMode] = useState("Category"); + const [chartData, setChartData] = useState({ categories: [], data: [] }); + const selectedProject = useSelectedProject(); + + const { ExpenseTypes, loading: typeLoading } = useExpenseType(); + + const { data: expenseApiData, isLoading } = useExpenseDataByProject( + projectId, + selectedType, + range === "All" ? null : parseInt(range) + ); + + useEffect(() => { + if (selectedProject && projectNames?.length) { + const project = projectNames.find((p) => p.id === selectedProject); + setProjectName(project?.name || "All Project"); + } else { + setProjectName("All Project"); + } + }, [projectNames, selectedProject]); + + useEffect(() => { + if (expenseApiData) { + const categories = expenseApiData.map((item) => + formatDate_DayMonth(item.monthName, item.year) + ); + const data = expenseApiData.map((item) => item.total); + setChartData({ categories, data }); + } else { + setChartData({ categories: [], data: [] }); + } + }, [expenseApiData]); + + const getSelectedTypeName = () => { + if (!selectedType) return "All Types"; + const found = ExpenseTypes.find((t) => t.id === selectedType); + return found ? found.name : "All Types"; + }; + + const options = { + chart: { type: "bar", toolbar: { show: false } }, + plotOptions: { + bar: { horizontal: false, columnWidth: "55%", borderRadius: 4 }, + }, + dataLabels: { enabled: true, formatter: (val) => formatCurrency(val) }, + xaxis: { + categories: chartData.categories, + labels: { style: { fontSize: "12px" }, rotate: -45 }, + }, + tooltip: { + y: { + formatter: (val) => `${formatCurrency(val)} (${getSelectedTypeName()})`, + }, + }, + + annotations: { xaxis: [{ x: 0, strokeDashArray: 0 }] }, + fill: { opacity: 1 }, + colors: ["#2196f3"], + }; + + const series = [ + { + name: getSelectedTypeName(), + data: chartData.data, + }, + ]; + + return ( +
    + {/* Header */} +
    +
    +
    +
    Monthly Expense -
    +

    {projectName}

    +
    +
    + +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    + + {/* Range Buttons + Expense Dropdown */} +
    + {["1M", "3M", "6M", "12M", "All"].map((item) => ( + + ))} + {viewMode === "Category" && ( + + )} +
    +
    + + {/* Chart */} +
    + {isLoading ? ( +

    Loading chart...

    + ) : !expenseApiData || expenseApiData.length === 0 ? ( +
    No data found
    + ) : ( + + )} +
    + +
    + ); +}; + +export default ExpenseByProject; diff --git a/src/components/Dashboard/ExpenseStatus.jsx b/src/components/Dashboard/ExpenseStatus.jsx new file mode 100644 index 00000000..d6fefe7d --- /dev/null +++ b/src/components/Dashboard/ExpenseStatus.jsx @@ -0,0 +1,157 @@ +import React, { useEffect, useState } from "react"; +import { useExpense } from "../../hooks/useExpense"; +import { useExpenseStatus } from "../../hooks/useDashboard_Data"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { useProjectName } from "../../hooks/useProjects"; +import { countDigit, formatCurrency } from "../../utils/appUtils"; +import { EXPENSE_MANAGE, EXPENSE_STATUS } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; +import { useHasUserPermission } from "../../hooks/useHasUserPermission"; + + +const ExpenseStatus = () => { + const [projectName, setProjectName] = useState("All Project"); + const selectedProject = useSelectedProject(); + const { projectNames, loading } = useProjectName(); + const { data, isPending, error } = useExpenseStatus(selectedProject); + const navigate = useNavigate(); + const isManageExpense = useHasUserPermission(EXPENSE_MANAGE) + + useEffect(() => { + if (selectedProject && projectNames?.length) { + const project = projectNames.find((p) => p.id === selectedProject); + setProjectName(project?.name || "All Project"); + } else { + setProjectName("All Project"); + } + }, [projectNames, selectedProject]); + + const handleNavigate = (status) => { + if (selectedProject) { + navigate(`/expenses/${status}/${selectedProject}`); + } else { + navigate(`/expenses/${status}`); + } + }; + return ( + <> +
    +
    +
    Expense - By Status
    +

    {projectName}

    +
    +
    + +
    + +
    + {[ + { + title: "Pending Payment", + count: data?.processPending?.count || 0, + amount: data?.processPending?.totalAmount || 0, + icon: "bx bx-rupee", + iconColor: "text-primary", + status: EXPENSE_STATUS.payment_pending, + }, + { + title: "Pending Approve", + count: data?.approvePending?.count || 0, + amount: data?.approvePending?.totalAmount || 0, + icon: "fa-solid fa-check", + iconColor: "text-warning", + status: EXPENSE_STATUS.approve_pending, + }, + { + title: "Pending Review", + count: data?.reviewPending?.count || 0, + amount: data?.reviewPending?.totalAmount || 0, + icon: "bx bx-search-alt-2", + iconColor: "text-secondary", + status: EXPENSE_STATUS.review_pending, + }, + { + title: "Draft", + count: data?.draft?.count || 0, + amount: data?.draft?.totalAmount || 0, + icon: "bx bx-file-blank", + iconColor: "text-info", + status: EXPENSE_STATUS.daft, + }, + ].map((item, idx) => ( +
    handleNavigate(item?.status)} + > +
    +
    + + + +
    +
    +
    + {item?.title} + {item?.amount ? ( + + {formatCurrency(item?.amount)} + + ) : ( + {formatCurrency(0)} + )} +
    +
    + = 3 ? "text-xl" : "text-2xl" + } text-gray-500`} + > + {item?.count || 0} + + + + +
    +
    +
    +
    + ))} +
    + +
    + {isManageExpense && ( +
    handleNavigate(EXPENSE_STATUS.process_pending)} + > +
    + 3 ? "text-base" : "text-lg" + }`} + > + Project Spendings: + {" "} + + (All Processed Payments) + +
    +
    + 3 ? "text-" : "text-3xl" + } text-md`} + > + {formatCurrency(data?.totalAmount || 0)} + + + + +
    +
    + )} +
    +
    + + ); +}; + +export default ExpenseStatus; diff --git a/src/components/Dashboard/ProjectCompletionChart.jsx b/src/components/Dashboard/ProjectCompletionChart.jsx index 8ce4b13a..decf7918 100644 --- a/src/components/Dashboard/ProjectCompletionChart.jsx +++ b/src/components/Dashboard/ProjectCompletionChart.jsx @@ -3,7 +3,8 @@ import HorizontalBarChart from "../Charts/HorizontalBarChart"; import { useProjects } from "../../hooks/useProjects"; const ProjectCompletionChart = () => { - const { projects, loading } = useProjects(); + const { data: projects = [], isLoading: loading, isError, error } = useProjects(); + // Bar chart logic const projectNames = projects?.map((p) => p.name) || []; @@ -11,7 +12,7 @@ const ProjectCompletionChart = () => { projects?.map((p) => { const completed = p.completedWork || 0; const planned = p.plannedWork || 1; - const percent = (completed / planned) * 100; + const percent = planned ? (completed / planned) * 100 : 0; return Math.min(Math.round(percent), 100); }) || []; diff --git a/src/components/Dashboard/Projects.jsx b/src/components/Dashboard/Projects.jsx index 86958aa5..06cba6ec 100644 --- a/src/components/Dashboard/Projects.jsx +++ b/src/components/Dashboard/Projects.jsx @@ -1,6 +1,9 @@ import React, { useEffect } from "react"; import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data"; import eventBus from "../../services/eventBus"; +import ProjectInfra from "../Project/ProjectInfra"; +import { ProjectCardSkeleton } from "./DashboardSkeleton"; +import { formatFigure } from "../../utils/appUtils"; const Projects = () => { const { @@ -8,6 +11,7 @@ const Projects = () => { isLoading, isError, error, + isFetching, refetch, } = useDashboardProjectsCardData(); @@ -23,7 +27,7 @@ const Projects = () => { const totalProjects = projectsCardData?.totalProjects ?? 0; const ongoingProjects = projectsCardData?.ongoingProjects ?? 0; - + if (isLoading) return ; return (
    @@ -33,24 +37,41 @@ const Projects = () => {
    - {isLoading ? ( -
    -
    - Loading... -
    -
    - ) : isError ? ( -
    - {error?.message || "Error loading data"} + {isError ? ( +
    + + + {error?.message || "Unable to load data at the moment."} + + + {" "} + Retry +
    ) : (
    -

    {totalProjects.toLocaleString()}

    +

    + {formatFigure(totalProjects ?? 0, { + notation: "compact", + })} +

    + Total
    -

    {ongoingProjects.toLocaleString()}

    +

    + {formatFigure(ongoingProjects ?? 0, { + notation: "compact", + })} +

    Ongoing
    diff --git a/src/components/Dashboard/Tasks.jsx b/src/components/Dashboard/Tasks.jsx index 56eb00f0..c386eb15 100644 --- a/src/components/Dashboard/Tasks.jsx +++ b/src/components/Dashboard/Tasks.jsx @@ -1,6 +1,8 @@ import React from "react"; import { useSelectedProject } from "../../slices/apiDataManager"; import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data"; +import { TasksSkeleton } from "./DashboardSkeleton"; +import { formatCurrency, formatFigure } from "../../utils/appUtils"; const TasksCard = () => { const projectId = useSelectedProject(); @@ -10,42 +12,57 @@ const TasksCard = () => { isLoading, isError, error, + isFetching, + refetch, } = useDashboardTasksCardData(projectId); - + if (isLoading) return ; return ( -
    +
    + {/* Header */}
    Tasks
    - {isLoading ? ( - // Loader while fetching -
    -
    - Loading... -
    -
    - ) : isError ? ( - // Show error -
    - {error?.message || "Error loading data"} + {isError ? ( +
    + + + {error?.message || "Unable to load data at the moment."} + + + + Retry +
    ) : ( - // Show data -
    -
    -

    - {tasksCardData?.totalTasks?.toLocaleString() ?? 0} +
    + {/* Total Tasks */} +
    +

    + {formatFigure(tasksCardData?.totalTasks ?? 0, { + notation: "compact", + })}

    - Total + Total
    -
    -

    - {tasksCardData?.completedTasks?.toLocaleString() ?? 0} + + {/* Completed Tasks */} +
    +

    + {formatFigure(tasksCardData?.completedTasks ?? 0, { + notation: "compact", + })}

    - Completed + Completed

    )} diff --git a/src/components/Dashboard/Teams.jsx b/src/components/Dashboard/Teams.jsx index 9e9d31f9..05888e77 100644 --- a/src/components/Dashboard/Teams.jsx +++ b/src/components/Dashboard/Teams.jsx @@ -4,16 +4,20 @@ import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data"; import eventBus from "../../services/eventBus"; import { useQueryClient } from "@tanstack/react-query"; import { useSelectedProject } from "../../slices/apiDataManager"; +import { TeamsSkeleton } from "./DashboardSkeleton"; +import { formatFigure } from "../../utils/appUtils"; const Teams = () => { const queryClient = useQueryClient(); - const projectId = useSelectedProject() + const projectId = useSelectedProject(); const { data: teamsCardData, isLoading, isError, error, + isFetching, + refetch, } = useDashboardTeamsCardData(projectId); // Handle real-time updates via eventBus @@ -40,6 +44,7 @@ const Teams = () => { const inToday = teamsCardData?.inToday ?? 0; const totalEmployees = teamsCardData?.totalEmployees ?? 0; + if (isLoading) return ; return (
    @@ -48,24 +53,41 @@ const Teams = () => {

    - {isLoading ? ( -
    -
    - Loading... -
    -
    - ) : isError ? ( -
    - {error?.message || "Error loading data"} + {isError ? ( +
    + + + + {error?.message || "Unable to load data at the moment."} + + + {" "} + Retry +
    ) : (
    -

    {totalEmployees.toLocaleString()}

    +

    + {formatFigure(totalEmployees ?? 0, { + notation: "compact", + })} +

    Total Employees
    -

    {inToday.toLocaleString()}

    +

    + {formatFigure(inToday ?? 0, { + notation: "compact", + })} +

    In Today
    diff --git a/src/components/Directory/ContactFilterChips.jsx b/src/components/Directory/ContactFilterChips.jsx new file mode 100644 index 00000000..d321afa3 --- /dev/null +++ b/src/components/Directory/ContactFilterChips.jsx @@ -0,0 +1,56 @@ +import React, { useMemo } from "react"; + +const ContactFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => { + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const addGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((i) => i.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + addGroup(filters.bucketIds, data.buckets, "Buckets", "bucketIds"); + addGroup(filters.categoryIds, data.contactCategories, "Category", "categoryIds"); + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
    + {filterChips.map((chipGroup) => ( +
    + {chipGroup.label}: + {chipGroup.items.map((item) => ( + + {item.name} +
    + ))} + +
    + ); +}; + +export default ContactFilterChips; \ No newline at end of file diff --git a/src/components/Directory/ListViewContact.jsx b/src/components/Directory/ListViewContact.jsx index 16fd9a14..f4f15531 100644 --- a/src/components/Directory/ListViewContact.jsx +++ b/src/components/Directory/ListViewContact.jsx @@ -160,8 +160,7 @@ const ListViewContact = ({ data, Pagination }) => {
    ) : ( { )} - {Pagination && ( -
    - {Pagination} -
    - )}
    + {Pagination && ( +
    + {Pagination} +
    + )}
    ); diff --git a/src/components/Directory/NoteFilterChips.jsx b/src/components/Directory/NoteFilterChips.jsx new file mode 100644 index 00000000..5569a460 --- /dev/null +++ b/src/components/Directory/NoteFilterChips.jsx @@ -0,0 +1,79 @@ +import React, { useMemo } from "react"; +import moment from "moment"; + +const NoteFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Normalize data (in case it’s wrapped in .data) + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + // Build chips dynamically + buildGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds"); + buildGroup(filters.organizations, data.organizations, "Organization", "organizations"); + + // Example: Add date range if you ever add in future + if (filters.startDate || filters.endDate) { + const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : ""; + const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
    +
    +
    + {filterChips.map((chip) => ( +
    + {chip.label}: +
    + {chip.items.map((item) => ( + + {item.name} +
    +
    + ))} +
    +
    +
    + ); +}; + +export default NoteFilterChips; \ No newline at end of file diff --git a/src/components/Documents/DocumentFilterChips.jsx b/src/components/Documents/DocumentFilterChips.jsx new file mode 100644 index 00000000..e6b56d7c --- /dev/null +++ b/src/components/Documents/DocumentFilterChips.jsx @@ -0,0 +1,94 @@ +import React, { useMemo } from "react"; +import moment from "moment"; + +const DocumentFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Normalize structure: handle both "filterData.data" and plain "filterData" + const data = filterData?.data || filterData || {}; + + const filterChips = useMemo(() => { + const chips = []; + + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list?.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + // Build chips using normalized data + buildGroup(filters.uploadedByIds, data.uploadedBy || [], "Uploaded By", "uploadedByIds"); + buildGroup(filters.documentCategoryIds, data.documentCategory || [], "Category", "documentCategoryIds"); + buildGroup(filters.documentTypeIds, data.documentType || [], "Type", "documentTypeIds"); + buildGroup(filters.documentTagIds, data.documentTag || [], "Tags", "documentTagIds"); + + if (filters.statusIds?.length) { + const items = filters.statusIds.map((status) => ({ + id: status, + name: + status === true + ? "Verified" + : status === false + ? "Rejected" + : "Pending", + })); + chips.push({ key: "statusIds", label: "Status", items }); + } + + if (filters.startDate || filters.endDate) { + const start = filters.startDate ? moment(filters.startDate).format("DD-MM-YYYY") : ""; + const end = filters.endDate ? moment(filters.endDate).format("DD-MM-YYYY") : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + + return ( +
    +
    +
    + {filterChips.map((chip) => ( +
    + {chip.label}: +
    + {chip.items.map((item) => ( + + {item.name} +
    +
    + ))} +
    +
    +
    + ); +}; + +export default DocumentFilterChips; diff --git a/src/components/Documents/DocumentFilterPanel.jsx b/src/components/Documents/DocumentFilterPanel.jsx index 15a2cbf1..edc613e1 100644 --- a/src/components/Documents/DocumentFilterPanel.jsx +++ b/src/components/Documents/DocumentFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState, useMemo, useImperativeHandle, forwardRef } from "react"; import { useDocumentFilterEntities } from "../../hooks/useDocument"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -9,16 +9,34 @@ import { import { DateRangePicker1 } from "../common/DateRangePicker"; import SelectMultiple from "../common/SelectMultiple"; import moment from "moment"; +import { useParams } from "react-router-dom"; -const DocumentFilterPanel = ({ entityTypeId, onApply }) => { +const DocumentFilterPanel = forwardRef( + ({ entityTypeId, onApply, setFilterdata }, ref) => { const [resetKey, setResetKey] = useState(0); + const { status } = useParams(); const { data, isError, isLoading, error } = useDocumentFilterEntities(entityTypeId); + //changes + + const dynamicDocumentFilterDefaultValues = useMemo(() => { + return { + ...DocumentFilterDefaultValues, + uploadedByIds: DocumentFilterDefaultValues.uploadedByIds || [], + documentCategoryIds: DocumentFilterDefaultValues.documentCategoryIds || [], + documentTypeIds: DocumentFilterDefaultValues.documentTypeIds || [], + documentTagIds: DocumentFilterDefaultValues.documentTagIds || [], + startDate: DocumentFilterDefaultValues.startDate, + endDate: DocumentFilterDefaultValues.endDate, + }; + + }, [status]); + const methods = useForm({ resolver: zodResolver(DocumentFilterSchema), - defaultValues: DocumentFilterDefaultValues, + defaultValues: dynamicDocumentFilterDefaultValues, }); const { handleSubmit, reset, setValue, watch } = methods; @@ -32,6 +50,24 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => { document.querySelector(".offcanvas.show .btn-close")?.click(); }; + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] }); + } + }, + getValues: methods.getValues, // optional, to read current filter state + })); + + //changes + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + const onSubmit = (values) => { onApply({ ...values, @@ -42,14 +78,14 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => { ? moment.utc(values.endDate, "DD-MM-YYYY").toISOString() : null, }); - closePanel(); + // closePanel(); }; const onClear = () => { reset(DocumentFilterDefaultValues); setResetKey((prev) => prev + 1); onApply(DocumentFilterDefaultValues); - closePanel(); + // closePanel(); }; if (isLoading) return
    Loading...
    ; @@ -63,6 +99,8 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => { documentTag = [], } = data?.data || {}; + + return (
    @@ -73,18 +111,16 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
    -
    ); -}; +}); export default DocumentFilterPanel; diff --git a/src/components/Documents/Documents.jsx b/src/components/Documents/Documents.jsx index 8210bf58..930ceb8c 100644 --- a/src/components/Documents/Documents.jsx +++ b/src/components/Documents/Documents.jsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; import GlobalModel from "../common/GlobalModel"; import NewDocument from "./ManageDocument"; import { DOCUMENTS_ENTITIES, UPLOAD_DOCUMENT } from "../../utils/constants"; @@ -17,6 +17,7 @@ import ViewDocument from "./ViewDocument"; import DocumentViewerModal from "./DocumentViewerModal"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { useProfile } from "../../hooks/useProfile"; +import DocumentFilterChips from "./DocumentFilterChips"; // Context export const DocumentContext = createContext(); @@ -51,12 +52,14 @@ const Documents = ({ Document_Entity, Entity }) => { const [isSelf, setIsSelf] = useState(false); const [searchText, setSearchText] = useState(""); const [isActive, setIsActive] = useState(true); - const [filters, setFilter] = useState(); + const [filters, setFilter] = useState(DocumentFilterDefaultValues); const [isRefetching, setIsRefetching] = useState(false); const [refetchFn, setRefetchFn] = useState(null); const [DocumentEntity, setDocumentEntity] = useState(Document_Entity); const { employeeId } = useParams(); const [OpenDocument, setOpenDocument] = useState(false); + const [filterData, setFilterdata] = useState(DocumentFilterDefaultValues); + const updatedRef = useRef(); const [ManageDoc, setManageDoc] = useState({ document: null, isOpen: false, @@ -92,7 +95,7 @@ const Documents = ({ Document_Entity, Entity }) => { setShowTrigger(true); setOffcanvasContent( "Document Filters", - + ); return () => { @@ -115,13 +118,35 @@ const Documents = ({ Document_Entity, Entity }) => { setDocumentEntity(Document_Entity); } }, [Document_Entity]); + + + const removeFilterChip = (key, id) => { + const updatedFilters = { ...filters }; + if (Array.isArray(updatedFilters[key])) { + updatedFilters[key] = updatedFilters[key].filter((v) => v !== id); + updatedRef.current?.resetFieldValue(key,updatedFilters[key]); + } + else if (key === "dateRange") { + updatedFilters.startDate = null; + updatedFilters.endDate = null; + updatedRef.current?.resetFieldValue("startDate",null); + updatedRef.current?.resetFieldValue("endDate",null); + } + else { + updatedFilters[key] = null; + } + setFilter(updatedFilters); + return updatedFilters; + }; + return ( -
    -
    +
    +
    +
    {/* Search */} -
    +
    {" "} {
    -
    +
    {(isSelf || canUploadDocument) && (
    ), getValue: (e) => - `${e.uploadedBy?.firstName ?? ""} ${ - e.uploadedBy?.lastName ?? "" - }`.trim() || "N/A", + `${e.uploadedBy?.firstName ?? ""} ${e.uploadedBy?.lastName ?? "" + }`.trim() || "N/A", }, { key: "uploadedAt", @@ -217,7 +215,7 @@ const DocumentsList = ({ } > - {(isSelf || canModifyDocument) && ( + {(isSelf || canModifyDocument) && ( @@ -226,7 +224,7 @@ const DocumentsList = ({ > )} - {(isSelf || canDeleteDocument) && ( + {(isSelf || canDeleteDocument) && ( { diff --git a/src/components/Employee/EmpAttendance.jsx b/src/components/Employee/EmpAttendance.jsx index b630ac7f..686d820c 100644 --- a/src/components/Employee/EmpAttendance.jsx +++ b/src/components/Employee/EmpAttendance.jsx @@ -51,7 +51,6 @@ const EmpAttendance = () => { new Date(b?.checkInTime).getTime() - new Date(a?.checkInTime).getTime() ); - console.log(sorted); const { currentPage, totalPages, currentItems, paginate } = usePagination( sorted, diff --git a/src/components/Employee/handleEmployeeExport.jsx b/src/components/Employee/handleEmployeeExport.jsx new file mode 100644 index 00000000..a4883d4f --- /dev/null +++ b/src/components/Employee/handleEmployeeExport.jsx @@ -0,0 +1,84 @@ +import moment from "moment"; +import { exportToExcel, exportToCSV, exportToPDF, printTable } from "../../utils/tableExportUtils"; + +/** + * Handles export operations for employee data. + * @param {string} type - Export type: 'csv', 'excel', 'pdf', or 'print' + * @param {Array} employeeList - Full employee data array + * @param {Array} filteredData - Filtered employee data (if search applied) + * @param {string} searchText - Current search text (used to decide dataset) + * @param {RefObject} tableRef - Table reference (used for print) + */ +const handleEmployeeExport = (type, employeeList, filteredData, searchText, tableRef) => { + // Export full list (filtered if search applied) + const dataToExport = searchText ? filteredData : employeeList; + + if (!dataToExport || dataToExport.length === 0) return; + + // Map and format employee data for export + const exportData = dataToExport.map((item) => ({ + "First Name": item.firstName || "", + "Middle Name": item.middleName || "", + "Last Name": item.lastName || "", + "Email": item.email || "", + "Gender": item.gender || "", + "Birth Date": item.birthdate + ? moment(item.birthdate).format("DD-MMM-YYYY") + : "", + "Joining Date": item.joiningDate + ? moment(item.joiningDate).format("DD-MMM-YYYY") + : "", + "Permanent Address": item.permanentAddress || "", + "Current Address": item.currentAddress || "", + "Phone Number": item.phoneNumber || "", + "Emergency Phone Number": item.emergencyPhoneNumber || "", + "Emergency Contact Person": item.emergencyContactPerson || "", + "Is Active": item.isActive ? "Active" : "Inactive", + "Job Role": item.jobRole || "", + })); + + switch (type) { + case "csv": + exportToCSV(exportData, "employees"); + break; + + case "excel": + exportToExcel(exportData, "employees"); + break; + + case "pdf": + exportToPDF( + dataToExport.map((item) => ({ + Name: `${item.firstName || ""} ${item.lastName || ""}`.trim(), + Email: item.email || "", + "Phone Number": item.phoneNumber || "", + "Job Role": item.jobRole || "", + "Joining Date": item.joiningDate + ? moment(item.joiningDate).format("DD-MMM-YYYY") + : "", + Gender: item.gender || "", + Status: item.isActive ? "Active" : "Inactive", + })), + "employees", + [ + "Name", + "Email", + "Phone Number", + "Job Role", + "Joining Date", + "Gender", + "Status", + ] + ); + break; + + case "print": + printTable(tableRef.current); + break; + + default: + break; + } +}; + +export default handleEmployeeExport; diff --git a/src/components/Expenses/ExpenseFilterChips.jsx b/src/components/Expenses/ExpenseFilterChips.jsx new file mode 100644 index 00000000..66c8a376 --- /dev/null +++ b/src/components/Expenses/ExpenseFilterChips.jsx @@ -0,0 +1,86 @@ +import React, { useMemo } from "react"; + +const ExpenseFilterChips = ({ filters, filterData, removeFilterChip }) => { + // Build chips from filters + const filterChips = useMemo(() => { + const chips = []; + + const buildGroup = (ids, list, label, key) => { + if (!ids?.length) return; + const items = ids.map((id) => ({ + id, + name: list.find((item) => item.id === id)?.name || id, + })); + chips.push({ key, label, items }); + }; + + buildGroup(filters.projectIds, filterData.projects, "Project", "projectIds"); + buildGroup(filters.createdByIds, filterData.createdBy, "Submitted By", "createdByIds"); + buildGroup(filters.paidById, filterData.paidBy, "Paid By", "paidById"); + buildGroup(filters.statusIds, filterData.status, "Status", "statusIds"); + buildGroup(filters.ExpenseTypeIds, filterData.expensesType, "Category", "ExpenseTypeIds"); + + if (filters.startDate || filters.endDate) { + const start = filters.startDate + ? new Date(filters.startDate).toLocaleDateString() + : ""; + const end = filters.endDate + ? new Date(filters.endDate).toLocaleDateString() + : ""; + chips.push({ + key: "dateRange", + label: "Date Range", + items: [{ id: "dateRange", name: `${start} - ${end}` }], + }); + } + + return chips; + }, [filters, filterData]); + + if (!filterChips.length) return null; + + return ( +
    +
    +
    + {filterChips.map((chip) => ( +
    + {/* Chip Label */} + {chip.label}: + + {/* Chip Items */} +
    + {chip.items.map((item) => ( + + {item.name} +
    +
    + ))} +
    +
    +
    + ); +}; + +export default ExpenseFilterChips; + + diff --git a/src/components/Expenses/ExpenseFilterPanel.jsx b/src/components/Expenses/ExpenseFilterPanel.jsx index c04a0981..3f0085c3 100644 --- a/src/components/Expenses/ExpenseFilterPanel.jsx +++ b/src/components/Expenses/ExpenseFilterPanel.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from "react"; +import React, { forwardRef, useEffect, useImperativeHandle, useState, useMemo } from "react"; import { FormProvider, useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { defaultFilter, SearchSchema } from "./ExpenseSchema"; @@ -13,9 +13,11 @@ import { useSelector } from "react-redux"; import moment from "moment"; import { useExpenseFilter } from "../../hooks/useExpense"; import { ExpenseFilterSkeleton } from "./ExpenseSkeleton"; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; -const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { +const ExpenseFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata }, ref) => { + const { status } = useParams(); + const navigate = useNavigate(); const selectedProjectId = useSelector( (store) => store.localVariables.projectId ); @@ -29,17 +31,31 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { { id: "submittedBy", name: "Submitted By" }, { id: "project", name: "Project" }, { id: "paymentMode", name: "Payment Mode" }, - { id: "expensesType", name: "Expense Type" }, + { id: "expensesType", name: "Expense Category" }, { id: "createdAt", name: "Submitted Date" }, ].sort((a, b) => a.name.localeCompare(b.name)); }, []); - const [selectedGroup, setSelectedGroup] = useState(groupByList[0]); + const [selectedGroup, setSelectedGroup] = useState(groupByList[6]); const [resetKey, setResetKey] = useState(0); + const dynamicDefaultFilter = useMemo(() => { + return { + ...defaultFilter, + statusIds: status ? [status] : defaultFilter.statusIds || [], + projectIds: defaultFilter.projectIds || [], + createdByIds: defaultFilter.createdByIds || [], + paidById: defaultFilter.paidById || [], + ExpenseTypeIds: defaultFilter.ExpenseTypeIds || [], + isTransactionDate: defaultFilter.isTransactionDate ?? true, + startDate: defaultFilter.startDate, + endDate: defaultFilter.endDate, + }; + }, [status]); + const methods = useForm({ resolver: zodResolver(SearchSchema), - defaultValues: defaultFilter, + defaultValues: dynamicDefaultFilter, }); const { control, handleSubmit, reset, setValue, watch } = methods; @@ -49,11 +65,30 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { document.querySelector(".offcanvas.show .btn-close")?.click(); }; + // Change here + useEffect(() => { + if (data && setFilterdata) { + setFilterdata(data); + } + }, [data, setFilterdata]); + const handleGroupChange = (e) => { const group = groupByList.find((g) => g.id === e.target.value); if (group) setSelectedGroup(group); }; + useImperativeHandle(ref, () => ({ + resetFieldValue: (name, value) => { + // Reset specific field + if (value !== undefined) { + setValue(name, value); + } else { + reset({ ...methods.getValues(), [name]: defaultFilter[name] }); + } + }, + getValues: methods.getValues, // optional, to read current filter state + })); + const onSubmit = (formData) => { onApply({ ...formData, @@ -71,17 +106,55 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { onApply(defaultFilter); handleGroupBy(groupByList[0].id); closePanel(); + if (status) { + navigate("/expenses", { replace: true }); + } }; - // Close popup when navigating to another component const location = useLocation(); useEffect(() => { closePanel(); }, [location]); + const [appliedStatusId, setAppliedStatusId] = useState(null); + + useEffect(() => { + if (!status || !data) return; + + if (status !== appliedStatusId) { + const filterWithStatus = { + ...dynamicDefaultFilter, + projectIds: selectedProjectId ? [selectedProjectId] : dynamicDefaultFilter.projectIds || [], + startDate: dynamicDefaultFilter.startDate + ? moment.utc(dynamicDefaultFilter.startDate, "DD-MM-YYYY").toISOString() + : undefined, + endDate: dynamicDefaultFilter.endDate + ? moment.utc(dynamicDefaultFilter.endDate, "DD-MM-YYYY").toISOString() + : undefined, + }; + + onApply(filterWithStatus); + handleGroupBy(selectedGroup.id); + setAppliedStatusId(status); + } + }, [ + status, + data, + dynamicDefaultFilter, + onApply, + handleGroupBy, + selectedGroup.id, + appliedStatusId, + selectedProjectId, // ✅ Added dependency + ]); + + if (isLoading || isFetching) return ; if (isError && isFetched) return
    Something went wrong Here- {error.message}
    ; + + + return ( <> @@ -92,18 +165,16 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
    + +
    + )}
    @@ -292,8 +367,10 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { )) ) : ( - - No Expense Found + +
    +

    No Expense Found

    +
    )} diff --git a/src/components/Expenses/PreviewDocument.jsx b/src/components/Expenses/PreviewDocument.jsx index ce554ca9..31f50991 100644 --- a/src/components/Expenses/PreviewDocument.jsx +++ b/src/components/Expenses/PreviewDocument.jsx @@ -1,54 +1,137 @@ -import { useState } from "react"; +import { useState, useRef ,useEffect} from "react"; const PreviewDocument = ({ imageUrl }) => { const [loading, setLoading] = useState(true); const [rotation, setRotation] = useState(0); + const [zoom, setZoom] = useState(1); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [startPos, setStartPos] = useState({ x: 0, y: 0 }); + const containerRef = useRef(null); + + // Zoom handlers + const handleZoomIn = () => setZoom((prev) => Math.min(prev + 0.2, 3)); + const handleZoomOut = () => setZoom((prev) => Math.max(prev - 0.2, 0.5)); + + // Mouse wheel zoom + const handleWheel = (e) => { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + setZoom((prev) => Math.min(Math.max(prev + delta, 0.5), 3)); + }; + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + container.addEventListener("wheel", handleWheel, { passive: false }); + + return () => { + container.removeEventListener("wheel", handleWheel); + }; + }, []); + const handleMouseDown = (e) => { + if (zoom <= 1) return; + setIsDragging(true); + setStartPos({ + x: e.clientX - position.x, + y: e.clientY - position.y, + }); + }; + + const handleMouseMove = (e) => { + if (!isDragging) return; + setPosition({ + x: e.clientX - startPos.x, + y: e.clientY - startPos.y, + }); + }; + + const handleMouseUp = () => setIsDragging(false); + const handleMouseLeave = () => setIsDragging(false); + + const handleReset = () => { + setRotation(0); + setZoom(1); + setPosition({ x: 0, y: 0 }); + }; return ( - <> -
    + <> +
    setRotation((prev) => prev + 90)} > + + +
    -
    - - {loading && ( -
    Loading...
    - )} -
    +
    1 ? (isDragging ? "grabbing" : "grab") : "default", + userSelect: "none", + position: "relative", + }} + > + {loading && ( +
    + Loading... +
    + )} Full View setLoading(false)} + style={{ + transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${zoom})`, + transition: isDragging ? "none" : "transform 0.3s ease", + objectFit: "contain", + maxWidth: "100%", + maxHeight: "100%", + display: loading ? "none" : "block", + pointerEvents: "none", + }} />
    -
    + {/*
    -
    -
    - +
    */} + ); }; export default PreviewDocument; + + + diff --git a/src/components/Expenses/ViewExpense.jsx b/src/components/Expenses/ViewExpense.jsx index 3feed000..cd608dfe 100644 --- a/src/components/Expenses/ViewExpense.jsx +++ b/src/components/Expenses/ViewExpense.jsx @@ -9,7 +9,11 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema"; import { useExpenseContext } from "../../pages/Expense/ExpensePage"; -import { getColorNameFromHex, getIconByFileType, localToUtc } from "../../utils/appUtils"; +import { + getColorNameFromHex, + getIconByFileType, + localToUtc, +} from "../../utils/appUtils"; import { ExpenseDetailsSkeleton } from "./ExpenseSkeleton"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; import { @@ -301,15 +305,15 @@ const ViewExpense = ({ ExpenseId }) => {
    {data?.documents?.map((doc) => { - const isImage = doc.contentType?.includes("image"); + const isImage = doc.contentType?.startsWith("image"); return (
    { if (isImage) { @@ -317,6 +321,8 @@ const ViewExpense = ({ ExpenseId }) => { IsOpen: true, Image: doc.preSignedUrl, }); + } else { + window.open(doc.preSignedUrl, "_blank"); } }} > @@ -332,7 +338,7 @@ const ViewExpense = ({ ExpenseId }) => {
    ); - })} + }) ?? "No Attachment"}
    @@ -418,7 +424,9 @@ const ViewExpense = ({ ExpenseId }) => { {((nextStatusWithPermission.length > 0 && !IsRejectedExpense) || (IsRejectedExpense && isCreatedBy)) && ( <> - +