addd Expense By project
This commit is contained in:
parent
18ac8e11bd
commit
997971629f
@ -18,6 +18,7 @@ import { useSelectedProject } from "../../slices/apiDataManager";
|
||||
import { useProjectName } from "../../hooks/useProjects";
|
||||
import ExpenseAnalysis from "./ExpenseAnalysis";
|
||||
import ExpenseStatus from "./ExpenseStatus";
|
||||
import ExpenseByProject from "./ExpenseByProject";
|
||||
|
||||
const Dashboard = () => {
|
||||
// const { projectsCardData } = useDashboardProjectsCardData();
|
||||
@ -30,23 +31,32 @@ const Dashboard = () => {
|
||||
|
||||
return (
|
||||
<div className="container-xxl flex-grow-1 container-p-y">
|
||||
<div class="row mb-6 g-6">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card h-100">
|
||||
<div className="row mb-6 g-6">
|
||||
<div className="col-12 col-xl-8">
|
||||
<div className="card h-100">
|
||||
<ExpenseAnalysis />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4 col-md-6">
|
||||
<div class="card h-100">
|
||||
<div className="col-12 col-xl-4 col-md-6">
|
||||
<div className="card h-100">
|
||||
<ExpenseStatus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isAllProjectsSelected && (<div className="co-12">
|
||||
|
||||
<div className="row g-6">
|
||||
{!isAllProjectsSelected && (
|
||||
<div className="col-6">
|
||||
<AttendanceOverview />
|
||||
</div>)}
|
||||
</div>
|
||||
)}
|
||||
<div className="col-6">
|
||||
<ExpenseByProject />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
162
src/components/Dashboard/ExpenseByProject.jsx
Normal file
162
src/components/Dashboard/ExpenseByProject.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
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";
|
||||
|
||||
const ExpenseByProject = () => {
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const [range, setRange] = useState("12M");
|
||||
const [selectedType, setSelectedType] = useState("");
|
||||
const [viewMode, setViewMode] = useState("Category");
|
||||
const [chartData, setChartData] = useState({ categories: [], data: [] });
|
||||
|
||||
const { ExpenseTypes, loading: typeLoading } = useExpenseType();
|
||||
|
||||
const { data: expenseApiData, isLoading } = useExpenseDataByProject(
|
||||
projectId,
|
||||
selectedType,
|
||||
range === "All" ? null : parseInt(range)
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="card shadow-sm ">
|
||||
{/* Header */}
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-start align-items-center mb-3 mt-3">
|
||||
<div>
|
||||
<h5 className="mb-1 me-6 fw-bold">Monthly Expense -</h5>
|
||||
<p className="card-subtitle me-5 mb-0">Detailed project expenses</p>
|
||||
</div>
|
||||
<div className="btn-group mb-4 ms-n8">
|
||||
<button
|
||||
className="btn btn-sm dropdown-toggle fs-5"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{viewMode}
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end ">
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
setViewMode("Category");
|
||||
setSelectedType("");
|
||||
}}
|
||||
>
|
||||
Category
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
setViewMode("Project");
|
||||
setSelectedType("");
|
||||
}}
|
||||
>
|
||||
Project
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{viewMode === "Category" && (
|
||||
<select
|
||||
className="form-select form-select-sm ms-auto mb-3"
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
disabled={typeLoading}
|
||||
style={{ maxWidth: "200px" }}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
{ExpenseTypes.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Range Buttons + Expense Dropdown */}
|
||||
<div className="d-flex gap-2 align-items-center flex-wrap">
|
||||
{["1M", "3M", "6M", "12M", "All"].map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
className={`border-0 px-2 py-1 text-sm rounded ${range === item
|
||||
? "text-white bg-primary"
|
||||
: "text-body bg-transparent"
|
||||
}`}
|
||||
style={{ cursor: "pointer", transition: "all 0.2s ease" }}
|
||||
onClick={() => setRange(item)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div
|
||||
className="card-body bg-white text-dark p-3"
|
||||
style={{ minHeight: "400px" }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<p>Loading chart...</p>
|
||||
) : (
|
||||
<Chart options={options} series={series} type="bar" height={400} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpenseByProject;
|
@ -283,3 +283,14 @@ export const useExpenseStatus = (projectId)=>{
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
||||
return useQuery({
|
||||
queryKey: ["expenseByProject", projectId, categoryId, months],
|
||||
queryFn: async () => {
|
||||
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
|
||||
return resp.data;
|
||||
},
|
||||
|
||||
});
|
||||
};
|
@ -129,9 +129,7 @@ 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"
|
||||
|
@ -67,7 +67,30 @@ const GlobalRepository = {
|
||||
return api.get(url );
|
||||
},
|
||||
|
||||
getExpenseStatus:(projectId)=>api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}`:""}`)
|
||||
getExpenseStatus:(projectId)=>api.get(`/api/Dashboard/expense/pendings${projectId ? `?projectId=${projectId}`:""}`),
|
||||
|
||||
getExpenseDataByProject: (projectId, categoryId, months) => {
|
||||
let url = `api/Dashboard/expense/monthly`
|
||||
|
||||
const queryParams = [];
|
||||
|
||||
if (projectId) {
|
||||
queryParams.push(`projectId=${projectId}`);
|
||||
}
|
||||
|
||||
if (categoryId) {
|
||||
queryParams.push(`categoryId=${categoryId}`);
|
||||
}
|
||||
if (months) {
|
||||
queryParams.push(`months=${months}`);
|
||||
}
|
||||
|
||||
if (queryParams.length > 0) {
|
||||
url += `?${queryParams.join("&")}`;
|
||||
}
|
||||
|
||||
return api.get(url);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
@ -94,11 +94,15 @@ 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 formatDate_DayMonth = (monthName, year) => {
|
||||
if (!monthName || !year) return "";
|
||||
try {
|
||||
const shortMonth = monthName.substring(0, 3);
|
||||
return `${shortMonth} ${year}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
|
||||
};
|
||||
export const getTenantStatus =(statusId)=>{
|
||||
return ActiveTenant === statusId ? " bg-label-success":"bg-label-secondary"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user