diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css index 68fd44df..a8f1f9a3 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -9,4 +9,15 @@ } .table_header_border { border-bottom:2px solid var(--bs-table-border-color) ; +} +.text-gary-80 { + color:var(--bs-gray-500) +} + +.text-royalblue{ + color: #1796e3; +} + +.text-md { + font-size: 2rem; } \ No newline at end of file diff --git a/public/assets/vendor/css/core.css b/public/assets/vendor/css/core.css index 48163eb5..17572275 100644 --- a/public/assets/vendor/css/core.css +++ b/public/assets/vendor/css/core.css @@ -32573,4 +32573,7 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar { } .text-red{ color:var(--bs-red) +} +.bg-gray { +background:var(--bs-body-color) } \ No newline at end of file diff --git a/src/components/Dashboard/AttendanceOverview.jsx b/src/components/Dashboard/AttendanceOverview.jsx index 05ca367c..d25cfa3e 100644 --- a/src/components/Dashboard/AttendanceOverview.jsx +++ b/src/components/Dashboard/AttendanceOverview.jsx @@ -5,14 +5,9 @@ import { useAttendanceOverviewData } from "../../hooks/useDashboard_Data"; import flatColors from "../Charts/flatColor"; import ChartSkeleton from "../Charts/Skelton"; import { useSelectedProject } from "../../slices/apiDataManager"; +import { formatDate_DayMonth } from "../../utils/dateUtils"; + -const formatDate = (dateStr) => { - const date = new Date(dateStr); - return date.toLocaleDateString("en-GB", { - day: "2-digit", - month: "long", - }); -}; const AttendanceOverview = () => { const [dayRange, setDayRange] = useState(7); @@ -35,7 +30,7 @@ const AttendanceOverview = () => { const map = new Map(); attendanceData.forEach((entry) => { - const date = formatDate(entry.date); + const date = formatDate_DayMonth(entry.date); if (!map.has(date)) map.set(date, {}); map.get(date)[entry.role.trim()] = entry.present; }); diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index 9435d3bc..750e68be 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -17,6 +17,7 @@ import AttendanceOverview from "./AttendanceOverview"; import { useSelectedProject } from "../../slices/apiDataManager"; import { useProjectName } from "../../hooks/useProjects"; import ExpenseAnalysis from "./ExpenseAnalysis"; +import ExpenseStatus from "./ExpenseStatus"; const Dashboard = () => { // const { projectsCardData } = useDashboardProjectsCardData(); @@ -27,19 +28,30 @@ const Dashboard = () => { const projectId = useSelector((store) => store.localVariables.projectId); const isAllProjectsSelected = projectId === null; - return ( -
-
-
- +
+ {/*
+
{!isAllProjectsSelected && (
- )} + )} */} + +
+
+
+ +
+
+ +
+
+ +
+
); diff --git a/src/components/Dashboard/ExpenseAnalysis.jsx b/src/components/Dashboard/ExpenseAnalysis.jsx index f59eb917..f1ce888a 100644 --- a/src/components/Dashboard/ExpenseAnalysis.jsx +++ b/src/components/Dashboard/ExpenseAnalysis.jsx @@ -4,7 +4,7 @@ import { useExpenseAnalysis } from "../../hooks/useDashboard_Data"; import { useSelectedProject } from "../../slices/apiDataManager"; import { DateRangePicker1 } from "../common/DateRangePicker"; import { FormProvider, useForm } from "react-hook-form"; -import { localToUtc } from "../../utils/appUtils"; +import { formatCurrency, localToUtc } from "../../utils/appUtils"; const ExpenseAnalysis = () => { const projectId = useSelectedProject(); @@ -19,11 +19,11 @@ const ExpenseAnalysis = () => { const { watch } = methods; const [startDate, endDate] = watch(["startDate", "endDate"]); - const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis( - projectId, - localToUtc(startDate), - localToUtc(endDate) -); + const { data, isLoading, isError, error, isFetching } = useExpenseAnalysis( + projectId, + localToUtc(startDate), + localToUtc(endDate) + ); if (isError) return
{error.message}
; @@ -31,7 +31,7 @@ const ExpenseAnalysis = () => { const labels = report.map((item) => item.projectName); const series = report.map((item) => item.totalApprovedAmount || 0); - const total = data?.totalAmount || 0; + const total = formatCurrency(data?.totalAmount || 0); const donutOptions = { chart: { type: "donut" }, @@ -57,84 +57,93 @@ const ExpenseAnalysis = () => { }, }; - if (data?.report === 0) { return
No data found
; } return ( -
-
-
-
Expense Breakdown
-

Detailed project expenses

-
- + <> +
+
+
Expense Breakdown
+

Detailed project expenses

+
+
- - + +
-
+
-
- {/* Initial loading: show full loader */} - {isLoading && ( -
- Loading... -
- )} - - {/* Data display */} - {!isLoading && report.length === 0 && ( -
No data found
- )} - - {!isLoading && report.length > 0 && ( - <> - {/* Overlay spinner for refetch */} - {isFetching && ( -
- Loading... -
- )} - -
- item.totalApprovedAmount || 0)} - type="donut" - width="320" - /> +
+ {/* Initial loading: show full loader */} + {isLoading && ( +
+ Loading...
+ )} -
-
- {report.map((item, idx) => ( -
-
- - - -
-
- {item.projectName} - {item.totalApprovedAmount} -
-
- ))} + {/* Data display */} + {!isLoading && report.length === 0 && ( +
No data found
+ )} + + {!isLoading && report.length > 0 && ( + <> + {/* Overlay spinner for refetch */} + {isFetching && ( +
+ Loading... +
+ )} + +
+ item.totalApprovedAmount || 0)} + type="donut" + width="320" + />
-
- - )} -
-
+
+
+ {report.map((item, idx) => ( +
+
+ + + +
+
+ {item.projectName} + + {formatCurrency(item.totalApprovedAmount)} + +
+
+ ))} +
+
+ + )} +
+ ); }; diff --git a/src/components/Dashboard/ExpenseStatus.jsx b/src/components/Dashboard/ExpenseStatus.jsx new file mode 100644 index 00000000..3fdddf51 --- /dev/null +++ b/src/components/Dashboard/ExpenseStatus.jsx @@ -0,0 +1,116 @@ +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 { formatCurrency } from "../../utils/appUtils"; +import { EXPENSE_STATUS } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; + +const ExpenseStatus = () => { + const [projectName, setProjectName] = useState("All Project"); + const selectedProject = useSelectedProject(); + const { projectNames, loading } = useProjectName(); + const { data, isPending, error } = useExpenseStatus(selectedProject); + const navigate = useNavigate(); + + useEffect(() => { + if (selectedProject && projectNames?.length) { + const project = projectNames.find((p) => p.id === selectedProject); + setProjectName(project?.name || "All Project"); + } else { + setProjectName("All Project"); + } + }, [projectNames, selectedProject]); + return ( + <> +
+
+
Expense - By Status
+

{projectName}

+
+
+ +
+
+
+ Project Spendings:{" "} + + {formatCurrency(data?.totalAmount)} + +
+ {`(All Processed Payments)`} +
+
+ {[ + { + title: "Pending Payment", + count: data?.approvePending?.count, + amount: data?.approvePending?.totalAmount, + icon: "bx bx-rupee", + iconColor: "text-primary", + status: EXPENSE_STATUS.payment_pending, + }, + { + title: "Pending Approver", + count: data?.processPending?.count, + amount: data?.processPending?.totalAmount, + icon: "fa-solid fa-check", + iconColor: "text-warning", + status: EXPENSE_STATUS.approve_pending, + }, + { + title: "Pending Reviewer", + count: data?.reviewPending?.count, + amount: data?.reviewPending?.totalAmount, + icon: "bx bx-file", + iconColor: "text-secondary", + status: EXPENSE_STATUS.review_pending, + }, + { + title: "Draft", + count: data?.submited?.count, + amount: data?.submited?.totalAmount, + icon: "bx bx-file-blank", + iconColor: "text-info", + status: EXPENSE_STATUS.daft, + }, + ].map((item, idx) => ( +
navigate(`/expenses/${item.status}`)} + > +
+
+ + + +
+
+
+ {item.title} + + {formatCurrency(item.amount)} + +
+
+ {" "} + + {item.count}{" "} + + + + +
+
+
+
+ ))} +
+
+ + ); +}; + +export default ExpenseStatus; diff --git a/src/components/Expenses/ExpenseFilterPanel.jsx b/src/components/Expenses/ExpenseFilterPanel.jsx index c04a0981..c3e2db23 100644 --- a/src/components/Expenses/ExpenseFilterPanel.jsx +++ b/src/components/Expenses/ExpenseFilterPanel.jsx @@ -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 { status } = useParams(); + const navigate = useNavigate(); const selectedProjectId = useSelector( (store) => store.localVariables.projectId ); @@ -37,9 +39,22 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => { const [selectedGroup, setSelectedGroup] = useState(groupByList[0]); const [resetKey, setResetKey] = useState(0); + const dynamicDefaultFilter = useMemo(() => { + return { + ...defaultFilter, + statusIds: status ? [status] : defaultFilter.statusIds || [], + projectIds: defaultFilter.projectIds || [], + createdByIds: defaultFilter.createdByIds || [], + paidById: defaultFilter.paidById || [], + 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; @@ -71,14 +86,49 @@ 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) return; + + if (status !== appliedStatusId && data) { + const filterWithStatus = { + ...dynamicDefaultFilter, + 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, + ]); + if (isLoading || isFetching) return ; if (isError && isFetched) return
Something went wrong Here- {error.message}
; diff --git a/src/components/Expenses/ExpenseList.jsx b/src/components/Expenses/ExpenseList.jsx index 35085588..affb44a0 100644 --- a/src/components/Expenses/ExpenseList.jsx +++ b/src/components/Expenses/ExpenseList.jsx @@ -10,7 +10,7 @@ import { EXPENSE_REJECTEDBY, ITEMS_PER_PAGE, } from "../../utils/constants"; -import { getColorNameFromHex, useDebounce } from "../../utils/appUtils"; +import { formatCurrency, getColorNameFromHex, useDebounce } from "../../utils/appUtils"; import { ExpenseTableSkeleton } from "./ExpenseSkeleton"; import ConfirmModal from "../common/ConfirmModal"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; @@ -140,7 +140,7 @@ const ExpenseList = ({ filters, groupBy = "transactionDate", searchText }) => { label: "Amount", getValue: (e) => ( <> - {e?.amount} + {formatCurrency(e?.amount)} ), isAlwaysVisible: true, diff --git a/src/components/Expenses/ViewExpense.jsx b/src/components/Expenses/ViewExpense.jsx index 7db45afc..a59c445f 100644 --- a/src/components/Expenses/ViewExpense.jsx +++ b/src/components/Expenses/ViewExpense.jsx @@ -111,9 +111,6 @@ const ViewExpense = ({ ExpenseId }) => {
Expense Details

-
-
{data?.description}
-
{/* Row 1 */}
@@ -277,7 +274,7 @@ const ViewExpense = ({ ExpenseId }) => { className="form-label me-2 mb-0 fw-semibold" style={{ minWidth: "130px" }} > - Paid By: + Paid By :
{
+ +
+ +
{data?.description}
+
diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx index 870672e3..c75851c2 100644 --- a/src/components/Layout/Header.jsx +++ b/src/components/Layout/Header.jsx @@ -14,7 +14,7 @@ import { useProfile } from "../../hooks/useProfile"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import Avatar from "../../components/common/Avatar"; import { useChangePassword } from "../Context/ChangePasswordContext"; -import { useProjectModal, useProjects } from "../../hooks/useProjects"; +import { useProjects } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects"; import eventBus from "../../services/eventBus"; import { useHasUserPermission } from "../../hooks/useHasUserPermission"; @@ -28,7 +28,6 @@ const Header = () => { const dispatch = useDispatch(); const navigate = useNavigate(); - const { openModal } = useProjectModal(); const { mutate: logout, isPending: logouting } = useLogout(); const { onOpen } = useAuthModal(); const { openChangePassword } = useChangePassword(); @@ -232,16 +231,7 @@ const Header = () => {
-
+
{ }, enabled:shouldFetch }); -}; \ No newline at end of file +}; +export const useExpenseStatus = (projectId)=>{ + return useQuery({ + queryKey:["expense_stauts",projectId], + queryFn: async()=>{ + const resp = await GlobalRepository.getExpenseStatus(projectId); + return resp.data; + } + }) +} \ No newline at end of file diff --git a/src/pages/Directory/DirectoryPage.jsx b/src/pages/Directory/DirectoryPage.jsx index 744f3460..9580e339 100644 --- a/src/pages/Directory/DirectoryPage.jsx +++ b/src/pages/Directory/DirectoryPage.jsx @@ -206,7 +206,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
-
+
{
+ {IsCreatedAble && ( )} */} + + {/* )} */}
@@ -209,7 +224,11 @@ const ProjectPage = () => { isLoading={isLoading} /> ) : ( - + )} {/* ------------------ */} diff --git a/src/repositories/GlobalRepository.jsx b/src/repositories/GlobalRepository.jsx index 9fc8b074..999d58b4 100644 --- a/src/repositories/GlobalRepository.jsx +++ b/src/repositories/GlobalRepository.jsx @@ -67,6 +67,7 @@ const GlobalRepository = { return api.get(url ); }, + getExpenseStatus:(projectId)=>api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}`:""}`) }; diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 61ed40c8..78574732 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -94,7 +94,7 @@ const router = createBrowserRouter( { path: "/activities/task", element: }, { path: "/activities/reports", element: }, { path: "/gallary", element: }, - { path: "/expenses", element: }, + { path: "/expenses/:status?", element: }, { path: "/masters", element: }, { path: "/tenants", element: }, { path: "/tenants/new-tenant", element: }, diff --git a/src/router/ProtectedRoute.jsx b/src/router/ProtectedRoute.jsx index d174652c..cb6441be 100644 --- a/src/router/ProtectedRoute.jsx +++ b/src/router/ProtectedRoute.jsx @@ -59,7 +59,7 @@ const attemptTokenRefresh = async (storedRefreshToken) => { return true; } catch (error) { - console.error("Token refresh failed:", error); + removeSession() return false; } }; diff --git a/src/utils/appUtils.js b/src/utils/appUtils.js index 0533dead..775ac756 100644 --- a/src/utils/appUtils.js +++ b/src/utils/appUtils.js @@ -87,6 +87,17 @@ export function localToUtc(dateString) { if (!day || !month || !year) return null; - const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0)); + const date = new Date( + Date.UTC(Number(year), Number(month) - 1, Number(day), 0, 0, 0) + ); return isNaN(date.getTime()) ? null : date.toISOString(); } + +export const formatCurrency = (amount, currency = "INR", locale = "en-US") => { + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); +}; diff --git a/src/utils/constants.jsx b/src/utils/constants.jsx index 8ea2da02..d002e1ad 100644 --- a/src/utils/constants.jsx +++ b/src/utils/constants.jsx @@ -147,6 +147,14 @@ export const PROJECT_STATUS = [ ]; +export const EXPENSE_STATUS = { + daft:"297e0d8f-f668-41b5-bfea-e03b354251c8", + review_pending:"6537018f-f4e9-4cb3-a210-6c3b2da999d7", + payment_pending:"f18c5cfd-7815-4341-8da2-2c2d65778e27", + approve_pending:"4068007f-c92f-4f37-a907-bc15fe57d4d8", + +} + export const UUID_REGEX = /^\/employee\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; diff --git a/src/utils/dateUtils.jsx b/src/utils/dateUtils.jsx index 388f7563..61ccc8c3 100644 --- a/src/utils/dateUtils.jsx +++ b/src/utils/dateUtils.jsx @@ -1,5 +1,6 @@ import moment from "moment"; import { ActiveTenant } from "./constants"; +import {format} from 'date-fns' export const getDateDifferenceInDays = (startDate, endDate) => { if (!startDate || !endDate) { @@ -93,6 +94,11 @@ export const getCompletionPercentage = (completedWork, plannedWork)=> { return clamped.toFixed(2); } +export const formatDate_DayMonth = (dateInput)=>{ + const date = new Date(dateInput); + return format(date, "d MMM"); +} + export const getTenantStatus =(statusId)=>{ return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary" } \ No newline at end of file