added Expensebstatus widget, fixed small bugs

This commit is contained in:
pramod.mahajan 2025-10-03 19:07:44 +05:30
parent 3553a7b521
commit 78808ecac0
21 changed files with 365 additions and 120 deletions

View File

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

View File

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

View File

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

View File

@ -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 (
<div className="container-fluid mt-5">
<div className="row gy-4">
<div className="col-xxl-6 col-lg-6">
<ExpenseAnalysis />
<div className="container-xxl flex-grow-1 container-p-y">
{/* <div className="col-xxl-6 col-lg-6">
</div>
{!isAllProjectsSelected && (
<div className="col-xxl-6 col-lg-6">
<AttendanceOverview />
</div>
)}
)} */}
<div class="row mb-6 g-6">
<div class="col-12 col-xl-8">
<div class="card h-100">
<ExpenseAnalysis />
</div>
</div>
<div class="col-12 col-xl-4 col-md-6">
<div class="card h-100">
<ExpenseStatus />
</div>
</div>
</div>
</div>
);

View File

@ -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 <div>{error.message}</div>;
@ -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 <div>No data found</div>;
}
return (
<div className="card shadow-sm" style={{ minHeight: "500px" }}>
<div className="card-header d-flex justify-content-between align-items-center mt-3">
<div>
<h5 className="mb-1 fw-bold">Expense Breakdown</h5>
<p className="card-subtitle me-3">Detailed project expenses</p>
</div>
<>
<div className="card-header d-flex justify-content-between align-items-center ">
<div>
<h5 className="mb-1 fw-bold">Expense Breakdown</h5>
<p className="card-subtitle me-3">Detailed project expenses</p>
</div>
<div className="text-end">
<FormProvider {...methods}>
<DateRangePicker1 />
<FormProvider {...methods}>
<DateRangePicker1 />
</FormProvider>
</div>
</div>
</div>
<div className="card-body position-relative">
{/* Initial loading: show full loader */}
{isLoading && (
<div className="d-flex justify-content-center align-items-center" style={{ height: "200px" }}>
<span>Loading...</span>
</div>
)}
{/* Data display */}
{!isLoading && report.length === 0 && (
<div className="text-center text-muted py-5">No data found</div>
)}
{!isLoading && report.length > 0 && (
<>
{/* Overlay spinner for refetch */}
{isFetching && (
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
<span>Loading...</span>
</div>
)}
<div className="d-flex justify-content-center mb-8">
<Chart
options={donutOptions}
series={report.map((item) => item.totalApprovedAmount || 0)}
type="donut"
width="320"
/>
<div className="card-body position-relative">
{/* Initial loading: show full loader */}
{isLoading && (
<div
className="d-flex justify-content-center align-items-center"
style={{ height: "200px" }}
>
<span>Loading...</span>
</div>
)}
<div className="mb-2 w-100">
<div className="row">
{report.map((item, idx) => (
<div className="col-6 d-flex justify-content-start align-items-start mb-2" key={idx}>
<div className="avatar me-2">
<span
className="avatar-initial rounded-2"
style={{
backgroundColor: donutOptions.colors[idx % donutOptions.colors.length],
}}
>
<i className="bx bx-receipt fs-4"></i>
</span>
</div>
<div className="d-flex flex-column gap-1 text-start">
<small className="fw-bold fs-6">{item.projectName}</small>
<span className="fw-bold text-muted ms-1">{item.totalApprovedAmount}</span>
</div>
</div>
))}
{/* Data display */}
{!isLoading && report.length === 0 && (
<div className="text-center text-muted py-5">No data found</div>
)}
{!isLoading && report.length > 0 && (
<>
{/* Overlay spinner for refetch */}
{isFetching && (
<div className="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-white bg-opacity-75">
<span>Loading...</span>
</div>
)}
<div className="d-flex justify-content-center mb-8">
<Chart
options={donutOptions}
series={report.map((item) => item.totalApprovedAmount || 0)}
type="donut"
width="320"
/>
</div>
</div>
</>
)}
</div>
</div>
<div className="mb-2 w-100">
<div className="row">
{report.map((item, idx) => (
<div
className="col-6 d-flex justify-content-start align-items-start mb-2"
key={idx}
>
<div className="avatar me-2">
<span
className="avatar-initial rounded-2"
style={{
backgroundColor:
donutOptions.colors[
idx % donutOptions.colors.length
],
}}
>
<i className="bx bx-receipt fs-4"></i>
</span>
</div>
<div className="d-flex flex-column gap-1 text-start">
<small className="fw-semibold ">{item.projectName}</small>
<span className="fw-semibold text-muted ms-1">
{formatCurrency(item.totalApprovedAmount)}
</span>
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
</>
);
};

View File

@ -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 (
<>
<div className="card-header d-flex justify-content-between text-start">
<div className="m-0">
<h5 className="card-title mb-1">Expense - By Status</h5>
<p className="card-subtitle m-0 ">{projectName}</p>
</div>
</div>
<div className="card-body ">
<div className=" py-0 text-start mb-2">
<div className="d-flex justify-content-between align-items-center">
<span className="fs-5"> Project Spendings:</span>{" "}
<span className="text-end text-royalblue text-md">
{formatCurrency(data?.totalAmount)}
</span>
</div>
<small className=" text-gary-80">{`(All Processed Payments)`}</small>
</div>
<div className="report-list text-start">
{[
{
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) => (
<div
key={idx}
className="report-list-item rounded-2 mb-4 bg-lighter px-2 py-3 cursor-pointer"
onClick={() => navigate(`/expenses/${item.status}`)}
>
<div className="d-flex align-items-center">
<div className="report-list-icon shadow-xs me-4">
<span className="d-inline-flex align-items-center justify-content-center rounded-circle border p-2">
<i className={`${item.icon} ${item.iconColor} bx-lg`}></i>
</span>
</div>
<div className="d-flex justify-content-between align-items-center w-100 flex-wrap gap-2">
<div className="d-flex flex-column gap-2">
<span className="fw-bold">{item.title}</span>
<small className="mb-0 text-primary">
{formatCurrency(item.amount)}
</small>
</div>
<div className="">
{" "}
<small className="text-muted fs-semibold text-royalblue text-md">
{item.count}{" "}
</small>
<small className="text-muted fs-semibold text-royalblue text-md">
<i className="bx bx-chevron-right"></i>
</small>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default ExpenseStatus;

View File

@ -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 <ExpenseFilterSkeleton />;
if (isError && isFetched)
return <div>Something went wrong Here- {error.message} </div>;

View File

@ -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) => (
<>
<i className="bx bx-rupee b-xs"></i> {e?.amount}
{formatCurrency(e?.amount)}
</>
),
isAlwaysVisible: true,

View File

@ -111,9 +111,6 @@ const ViewExpense = ({ ExpenseId }) => {
<h5 className="fw-semibold">Expense Details</h5>
<hr />
</div>
<div className="text-start mb-2">
<div className="text-muted">{data?.description}</div>
</div>
{/* Row 1 */}
<div className="col-md-6 mb-3">
<div className="d-flex">
@ -277,7 +274,7 @@ const ViewExpense = ({ ExpenseId }) => {
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
Paid By:
Paid By :
</label>
<div className="d-flex align-items-center ">
<Avatar
@ -294,6 +291,11 @@ const ViewExpense = ({ ExpenseId }) => {
</div>
</div>
</div>
<div className="text-start my-1">
<label className="fw-semibold form-label">Description : </label>
<div className="text-muted">{data?.description}</div>
</div>
</div>
<div className="col-12 text-start">

View File

@ -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 = () => {
<ul className="navbar-nav flex-row align-items-center ms-md-auto">
{/* {HasManageProjectPermission && ( */}
<li className="nav-item navbar-dropdown dropdown-user dropdown">
<button
className="btn btn-sm btn-primary"
type="button"
onClick={() => openModal(null)}
>
<i className="bx bx-plus-circle me-2"></i>
<span className="d-none d-md-inline-block">Create Project</span>
</button>
</li>
{/* )} */}
<li className="nav-item navbar-dropdown dropdown-user dropdown">
<a

View File

@ -54,7 +54,7 @@ const ProjectCard = ({ project }) => {
style={{ fontSize: "xx-large" }}
></i>
</div>
<div className="me-2">
<div className="me-2 text-start">
<h5
className="mb-0 stretched-link text-heading text-start"
onClick={handleViewProject}

View File

@ -270,4 +270,13 @@ export const useExpenseAnalysis = (projectId, startDate, endDate) => {
},
enabled:shouldFetch
});
};
};
export const useExpenseStatus = (projectId)=>{
return useQuery({
queryKey:["expense_stauts",projectId],
queryFn: async()=>{
const resp = await GlobalRepository.getExpenseStatus(projectId);
return resp.data;
}
})
}

View File

@ -206,7 +206,7 @@ export default function DirectoryPage({ IsPage = true, projectId = null }) {
</button>
</div>
<div className="form-check form-switch d-flex align-items-center">
<div className="form-check form-switch d-flex align-items-center m-0">
<input
type="checkbox"
className="form-check-input"

View File

@ -129,6 +129,9 @@ const ExpensePage = () => {
</div>
<div className="col-6 text-end mt-2 mt-sm-0">
<button className="btn btn-sm" onClick={clearFilter}>
CLear
</button>
{IsCreatedAble && (
<button
className="btn btn-sm btn-primary"

View File

@ -1,6 +1,10 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import { ITEMS_PER_PAGE, MANAGE_PROJECT, PROJECT_STATUS } from "../../utils/constants";
import {
ITEMS_PER_PAGE,
MANAGE_PROJECT,
PROJECT_STATUS,
} from "../../utils/constants";
import ProjectListView from "../../components/Project/ProjectListView";
import GlobalModel from "../../components/common/GlobalModel";
import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
@ -95,9 +99,18 @@ const ProjectPage = () => {
}
}, [data, isLoading, selectedStatuses]);
if(isLoading) return <div className="page-min-h"><Loader/></div>
if(isError) return <div className="page-min-h d-flex justify-content-center align-items-center"><p>{error.message}</p></div>
if (isLoading)
return (
<div className="page-min-h">
<Loader />
</div>
);
if (isError)
return (
<div className="page-min-h d-flex justify-content-center align-items-center">
<p>{error.message}</p>
</div>
);
return (
<ProjectContext.Provider value={contextDispatcher}>
<div className="container-fluid">
@ -181,7 +194,8 @@ const ProjectPage = () => {
</div>
<div>
{/* {HasManageProject && ( <button
{/* {HasManageProject && ( */}
<button
className="btn btn-sm btn-primary"
type="button"
onClick={() =>
@ -192,7 +206,8 @@ const ProjectPage = () => {
<span className="d-none d-md-inline-block">
Add New Project
</span>
</button>)} */}
</button>
{/* )} */}
</div>
</div>
</div>
@ -209,7 +224,11 @@ const ProjectPage = () => {
isLoading={isLoading}
/>
) : (
<ProjectCardView currentItems={currentItems} setCurrentPage={setCurrentPage} totalPages={totalPages} />
<ProjectCardView
currentItems={currentItems}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
)}
{/* ------------------ */}

View File

@ -67,6 +67,7 @@ const GlobalRepository = {
return api.get(url );
},
getExpenseStatus:(projectId)=>api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}`:""}`)
};

View File

@ -94,7 +94,7 @@ const router = createBrowserRouter(
{ path: "/activities/task", element: <TaskPlannng /> },
{ path: "/activities/reports", element: <Reports /> },
{ path: "/gallary", element: <ComingSoonPage /> },
{ path: "/expenses", element: <ExpensePage /> },
{ path: "/expenses/:status?", element: <ExpensePage /> },
{ path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> },
{ path: "/tenants/new-tenant", element: <CreateTenant /> },

View File

@ -59,7 +59,7 @@ const attemptTokenRefresh = async (storedRefreshToken) => {
return true;
} catch (error) {
console.error("Token refresh failed:", error);
removeSession()
return false;
}
};

View File

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

View File

@ -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}$/;

View File

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