Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Adding_dropdown
This commit is contained in:
commit
c4a46cbe80
345
src/components/Dashboard/CollectionOverview.jsx
Normal file
345
src/components/Dashboard/CollectionOverview.jsx
Normal file
@ -0,0 +1,345 @@
|
||||
import React from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import { useGetCollectionOverview } from "../../hooks/useDashboard_Data";
|
||||
import { formatFigure } from "../../utils/appUtils";
|
||||
|
||||
const CollectionOverview = ({ data, isLoading }) => {
|
||||
const borderColor = "#ddd";
|
||||
const labelColor = "#6c757d";
|
||||
|
||||
// Extract bucket values
|
||||
const labels = ["0–30 Days", "30–60 Days", "60–90 Days", "90+ Days"];
|
||||
|
||||
const amounts = [
|
||||
data.bucket0To30Amount,
|
||||
data.bucket30To60Amount,
|
||||
data.bucket60To90Amount,
|
||||
data.bucket90PlusAmount,
|
||||
];
|
||||
|
||||
// Colors (Zoho-style distributed)
|
||||
const colors = ["#7367F0", "#00cfe8", "#28c76f", "#ea5455"];
|
||||
|
||||
const options = {
|
||||
chart: {
|
||||
type: "bar",
|
||||
height: 260,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
barHeight: "65%",
|
||||
distributed: true,
|
||||
borderRadius: 8,
|
||||
startingShape: "rounded",
|
||||
},
|
||||
},
|
||||
|
||||
colors: colors,
|
||||
|
||||
grid: {
|
||||
borderColor: borderColor,
|
||||
strokeDashArray: 6,
|
||||
padding: { top: -10, bottom: -10 },
|
||||
xaxis: { lines: { show: true } },
|
||||
},
|
||||
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: (_, opts) => labels[opts.dataPointIndex],
|
||||
style: {
|
||||
colors: ["#fff"],
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
},
|
||||
offsetX: 0,
|
||||
},
|
||||
|
||||
xaxis: {
|
||||
categories: amounts.map((a) => a),
|
||||
labels: {
|
||||
style: { colors: labelColor, fontSize: "12px" },
|
||||
formatter: (val) => `₹${val.toLocaleString()}`,
|
||||
},
|
||||
},
|
||||
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontSize: "13px",
|
||||
},
|
||||
formatter: () => "", // hide duplicate labels
|
||||
},
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
custom: ({ series, seriesIndex, dataPointIndex }) => {
|
||||
return `
|
||||
<div class="px-2 py-1">
|
||||
<strong>${labels[dataPointIndex]}</strong><br>
|
||||
₹${series[seriesIndex][dataPointIndex].toLocaleString()}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
legend: { show: false },
|
||||
};
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: "Amount",
|
||||
data: amounts,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Chart options={options} series={series} type="bar" height={260} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionOverview;
|
||||
export const TopicBarChart = ({ data,isLoading }) => {
|
||||
const data1 = {
|
||||
totalDueAmount: 213590,
|
||||
totalCollectedAmount: 5000,
|
||||
totalValue: 218590,
|
||||
pendingPercentage: 97.71,
|
||||
collectedPercentage: 2.29,
|
||||
|
||||
bucket0To30Invoices: 10,
|
||||
bucket30To60Invoices: 4,
|
||||
bucket60To90Invoices: 2,
|
||||
bucket90PlusInvoices: 1,
|
||||
|
||||
bucket0To30Amount: 2130,
|
||||
bucket30To60Amount: 2003,
|
||||
bucket60To90Amount: 4500,
|
||||
bucket90PlusAmount: 8800,
|
||||
|
||||
topClientBalance: 55300,
|
||||
topClient: {
|
||||
id: "4e3a6d31-c640-40f7-8d67-6c109fcdb9ea",
|
||||
name: "Marco Secure Solutions Ltd.",
|
||||
email: "admin@marcoaiot.com",
|
||||
contactPerson: "Admin",
|
||||
address:
|
||||
"2nd Floor, Fullora Building, Tejas CHS, behind Kothrud Stand, Tejas Society, Dahanukar Colony, Kothrud, Pune, Maharashtra 411038",
|
||||
gstNumber: null,
|
||||
contactNumber: "123456789",
|
||||
sprid: 5400,
|
||||
},
|
||||
};
|
||||
|
||||
const borderColor = "#ddd";
|
||||
const labelColor = "#6c757d";
|
||||
|
||||
// COLORS
|
||||
const config = {
|
||||
colors: {
|
||||
b0: "#7367F0",
|
||||
b30: "#00cfe8",
|
||||
b60: "#28c76f",
|
||||
b90: "#ea5455",
|
||||
},
|
||||
};
|
||||
|
||||
// NEW LABELS (BUCKETS)
|
||||
const chartLabels = ["0–30 Days", "30–60 Days", "60–90 Days", "90+ Days"];
|
||||
|
||||
// NEW VALUES (BUCKET AMOUNT)
|
||||
const chartValues = [
|
||||
data.bucket0To30Amount,
|
||||
data.bucket30To60Amount,
|
||||
data.bucket60To90Amount,
|
||||
data.bucket90PlusAmount,
|
||||
];
|
||||
|
||||
const options = {
|
||||
chart: {
|
||||
height: 300,
|
||||
type: "bar",
|
||||
toolbar: { show: false },
|
||||
},
|
||||
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
barHeight: "40%",
|
||||
distributed: true,
|
||||
startingShape: "rounded",
|
||||
borderRadius: 7,
|
||||
},
|
||||
},
|
||||
|
||||
grid: {
|
||||
strokeDashArray: 10,
|
||||
borderColor,
|
||||
xaxis: { lines: { show: true } },
|
||||
yaxis: { lines: { show: false } },
|
||||
padding: { top: -35, bottom: -12 },
|
||||
},
|
||||
|
||||
colors: [
|
||||
config.colors.b0,
|
||||
config.colors.b30,
|
||||
config.colors.b60,
|
||||
config.colors.b90,
|
||||
],
|
||||
|
||||
labels: chartLabels,
|
||||
|
||||
fill: { opacity: 1 },
|
||||
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
colors: ["#fff"],
|
||||
fontWeight: 400,
|
||||
fontSize: "13px",
|
||||
fontFamily: "Public Sans",
|
||||
},
|
||||
formatter: (_, opts) => chartLabels[opts.dataPointIndex],
|
||||
},
|
||||
|
||||
xaxis: {
|
||||
categories: chartValues.map((x) => formatFigure(x, { type: "currency" })),
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { show: false },
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontFamily: "Public Sans",
|
||||
fontSize: "13px",
|
||||
},
|
||||
formatter: (val) => `₹${Number(val).toLocaleString()}`,
|
||||
},
|
||||
},
|
||||
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: labelColor,
|
||||
fontFamily: "Public Sans",
|
||||
fontSize: "13px",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
custom: ({ series, seriesIndex, dataPointIndex }) => {
|
||||
return `
|
||||
<div class="px-3 py-2">
|
||||
<span>₹${series[seriesIndex][
|
||||
dataPointIndex
|
||||
].toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
legend: { show: false },
|
||||
};
|
||||
|
||||
const series = [
|
||||
{
|
||||
data: chartValues,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="row p-2">
|
||||
<div className="col-md-8">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<h5 class="card-title m-0 me-2">Collection Overview</h5>
|
||||
</div>
|
||||
<div className="w-100 d-flex align-items-center text-start px-6">
|
||||
<p className="text-secondary fs-6 m-0">Due Amount</p>
|
||||
<span className="ms-2 fs-5">
|
||||
{formatFigure(data.totalDueAmount, { type: "currency" })}
|
||||
</span>
|
||||
<p className="text-secondary fs-6 m-0 ms-1">Collected Amount</p>
|
||||
<span className="ms-2 fs-5">
|
||||
{formatFigure(data.totalCollectedAmount, { type: "currency" })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Chart options={options} series={series} type="bar" height={300} />
|
||||
</div>
|
||||
|
||||
<div className="col-md-4 d-flex flex-column gap-2">
|
||||
<div class="card-header d-flex align-items-end justify-content-between"></div>
|
||||
<div className="p-1 m-1 text-start">
|
||||
{/* <p className="fs-5 fs-5">Top Client</p>
|
||||
<small> {data.topClient.name}</small> */}
|
||||
</div>
|
||||
|
||||
{/* 0–30 Days */}
|
||||
<div
|
||||
className="p-1 rounded-3 text-start mx-1"
|
||||
style={{
|
||||
background: "var(--bs-primary-bg-subtle)",
|
||||
borderLeft: "4px solid var(--bs-primary)",
|
||||
minWidth: "170px",
|
||||
}}
|
||||
>
|
||||
<h5 className="fw-bold mb-0">
|
||||
{formatFigure(data.bucket0To30Amount, { type: "currency" })}
|
||||
</h5>
|
||||
<p className="text-secondary mb-0 small">0–30 Days</p>
|
||||
</div>
|
||||
|
||||
{/* 30–60 Days */}
|
||||
<div
|
||||
className="p-1 rounded-3 text-start mx-1"
|
||||
style={{
|
||||
background: "var(--bs-info-bg-subtle)",
|
||||
borderLeft: "4px solid var(--bs-info)",
|
||||
minWidth: "170px",
|
||||
}}
|
||||
>
|
||||
<h5 className="fw-bold mb-0">
|
||||
{formatFigure(data.bucket30To60Amount, { type: "currency" })}
|
||||
</h5>
|
||||
<p className="text-secondary mb-0 small">30–60 Days</p>
|
||||
</div>
|
||||
|
||||
{/* 60–90 Days */}
|
||||
<div
|
||||
className="p-1 rounded-3 text-start mx-1"
|
||||
style={{
|
||||
background: "var(--bs-warning-bg-subtle)",
|
||||
borderLeft: "4px solid var(--bs-warning)",
|
||||
minWidth: "170px",
|
||||
}}
|
||||
>
|
||||
<h5 className="fw-bold mb-0">
|
||||
{formatFigure(data.bucket60To90Amount, { type: "currency" })}
|
||||
</h5>
|
||||
<p className="text-secondary mb-0 small">60–90 Days</p>
|
||||
</div>
|
||||
|
||||
{/* 90+ Days */}
|
||||
<div
|
||||
className="p-1 rounded-3 text-start mx-1"
|
||||
style={{
|
||||
background: "var(--bs-danger-bg-subtle)",
|
||||
borderLeft: "4px solid var(--bs-danger)",
|
||||
minWidth: "170px",
|
||||
}}
|
||||
>
|
||||
<h5 className="fw-bold mb-0">
|
||||
{formatFigure(data.bucket90PlusAmount, { type: "currency" })}
|
||||
</h5>
|
||||
<p className="text-secondary mb-0 small">90+ Days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
40
src/components/Dashboard/CollectionOverviewSkeleton.jsx
Normal file
40
src/components/Dashboard/CollectionOverviewSkeleton.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||
<div
|
||||
className={`skeleton mb-2 ${className}`}
|
||||
style={{ height, width }}
|
||||
></div>
|
||||
);
|
||||
export const CollectionOverviewSkeleton = () => {
|
||||
return (
|
||||
<div className="card row p-1">
|
||||
|
||||
{/* LEFT SIDE */}
|
||||
<div className="col-8">
|
||||
<div className="">
|
||||
|
||||
{/* Header */}
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<SkeletonLine height={24} width="180px" />
|
||||
</div>
|
||||
|
||||
{/* Due & Collected summary */}
|
||||
<div className="d-flex align-items-center text-start px-6 mb-3">
|
||||
<SkeletonLine height={16} width="100px" className="me-2" />
|
||||
<SkeletonLine height={20} width="120px" className="me-2" />
|
||||
<SkeletonLine height={16} width="120px" className="ms-2 me-2" />
|
||||
<SkeletonLine height={20} width="120px" />
|
||||
</div>
|
||||
|
||||
{/* Chart Skeleton */}
|
||||
<SkeletonLine height={250} width="100%" className="mt-2" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
@ -4,7 +4,8 @@ import {
|
||||
useDashboardProjectsCardData,
|
||||
useDashboardTeamsCardData,
|
||||
useDashboardTasksCardData,
|
||||
useAttendanceOverviewData
|
||||
useAttendanceOverviewData,
|
||||
useGetCollectionOverview,
|
||||
} from "../../hooks/useDashboard_Data";
|
||||
|
||||
import Projects from "./Projects";
|
||||
@ -19,10 +20,15 @@ import ExpenseByProject from "./ExpenseByProject";
|
||||
import ProjectStatistics from "../Project/ProjectStatistics";
|
||||
import ServiceJobs from "./ServiceJobs";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { REGULARIZE_ATTENDANCE, SELF_ATTENDANCE, TEAM_ATTENDANCE } from "../../utils/constants";
|
||||
import {
|
||||
REGULARIZE_ATTENDANCE,
|
||||
SELF_ATTENDANCE,
|
||||
TEAM_ATTENDANCE,
|
||||
} from "../../utils/constants";
|
||||
import CollectionOverview, { TopicBarChart } from "./CollectionOverview";
|
||||
import { CollectionOverviewSkeleton } from "./CollectionOverviewSkeleton";
|
||||
|
||||
const Dashboard = () => {
|
||||
|
||||
// Get the selected project ID from Redux store
|
||||
const projectId = useSelector((store) => store.localVariables.projectId);
|
||||
const isAllProjectsSelected = projectId === null;
|
||||
@ -30,7 +36,8 @@ const Dashboard = () => {
|
||||
const canTeamAttendance = useHasUserPermission(TEAM_ATTENDANCE);
|
||||
const canSelfAttendance = useHasUserPermission(SELF_ATTENDANCE);
|
||||
|
||||
|
||||
const { data,isLoading,isError } = useGetCollectionOverview();
|
||||
console.log("data-->", data);
|
||||
return (
|
||||
<div className="container-fluid mt-5">
|
||||
<div className="row gy-4">
|
||||
@ -40,11 +47,19 @@ const Dashboard = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||
<div
|
||||
className={`${
|
||||
!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"
|
||||
}`}
|
||||
>
|
||||
<Teams />
|
||||
</div>
|
||||
|
||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||
<div
|
||||
className={`${
|
||||
!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"
|
||||
}`}
|
||||
>
|
||||
<TasksCard />
|
||||
</div>
|
||||
<div className="col-12 col-xl-4 col-md-6">
|
||||
@ -70,7 +85,8 @@ const Dashboard = () => {
|
||||
<div className="col-xxl-6 col-lg-6">
|
||||
<ProjectProgressChart />
|
||||
</div>
|
||||
{!isAllProjectsSelected && (canRegularize || canTeamAttendance || canSelfAttendance) && (
|
||||
{!isAllProjectsSelected &&
|
||||
(canRegularize || canTeamAttendance || canSelfAttendance) && (
|
||||
<div className="col-12 col-md-6 mb-sm-0 mb-4">
|
||||
<AttendanceOverview />
|
||||
</div>
|
||||
@ -83,13 +99,97 @@ const Dashboard = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* <div className="col-12 col-md-12">
|
||||
<ServiceJobs />
|
||||
</div> */}
|
||||
<div className="col-md-8">
|
||||
{isLoading ? (
|
||||
<CollectionOverviewSkeleton />
|
||||
) : (
|
||||
data && (
|
||||
<div className="card">
|
||||
<TopicBarChart data={data} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
// <div class="col-12 col-xl-8">
|
||||
// <div class="card h-100">
|
||||
// <div class="card-header d-flex align-items-center justify-content-between">
|
||||
// <h5 class="card-title m-0 me-2">Topic you are interested in</h5>
|
||||
// <div class="dropdown">
|
||||
// <button
|
||||
// class="btn p-0"
|
||||
// type="button"
|
||||
// id="topic"
|
||||
// data-bs-toggle="dropdown"
|
||||
// aria-haspopup="true"
|
||||
// aria-expanded="false">
|
||||
// <i class="bx bx-dots-vertical-rounded bx-lg text-muted"></i>
|
||||
// </button>
|
||||
// <div class="dropdown-menu dropdown-menu-end" aria-labelledby="topic">
|
||||
// <a class="dropdown-item" href="javascript:void(0);">Highest Views</a>
|
||||
// <a class="dropdown-item" href="javascript:void(0);">See All</a>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="card-body row g-3">
|
||||
// <div class="col-md-8">
|
||||
// <div id="horizontalBarChart"></div>
|
||||
// </div>
|
||||
// <div class="col-md-4 d-flex justify-content-around align-items-center">
|
||||
// <div>
|
||||
// <div class="d-flex align-items-baseline">
|
||||
// <span class="text-primary me-2"><i class="bx bxs-circle bx-12px"></i></span>
|
||||
// <div>
|
||||
// <p class="mb-0">UI Design</p>
|
||||
// <h5>35%</h5>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="d-flex align-items-baseline my-12">
|
||||
// <span class="text-success me-2"><i class="bx bxs-circle bx-12px"></i></span>
|
||||
// <div>
|
||||
// <p class="mb-0">Music</p>
|
||||
// <h5>14%</h5>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="d-flex align-items-baseline">
|
||||
// <span class="text-danger me-2"><i class="bx bxs-circle bx-12px"></i></span>
|
||||
// <div>
|
||||
// <p class="mb-0">React</p>
|
||||
// <h5>10%</h5>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div>
|
||||
// <div class="d-flex align-items-baseline">
|
||||
// <span class="text-info me-2"><i class="bx bxs-circle bx-12px"></i></span>
|
||||
// <div>
|
||||
// <p class="mb-0">UX Design</p>
|
||||
// <h5>20%</h5>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="d-flex align-items-baseline my-12">
|
||||
// <span class="text-secondary me-2"><i class="bx bxs-circle bx-12px"></i></span>
|
||||
// <div>
|
||||
// <p class="mb-0">Animation</p>
|
||||
// <h5>12%</h5>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="d-flex align-items-baseline">
|
||||
// <span class="text-warning me-2"><i class="bx bxs-circle bx-12px"></i></span>
|
||||
// <div>
|
||||
// <p class="mb-0">SEO</p>
|
||||
// <h5>9%</h5>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
@ -69,7 +69,7 @@ const Sidebar = () => {
|
||||
</>
|
||||
)}
|
||||
{data &&
|
||||
data?.data.map((section) => (
|
||||
data?.data?.map((section) => (
|
||||
<React.Fragment
|
||||
key={section.id || section.header || section.items[0]?.id}
|
||||
>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useMemo } from "react";
|
||||
import Timeline from "../../common/Timeline";
|
||||
|
||||
import { getColorNameFromHex } from "../../../utils/appUtils";
|
||||
import Timeline from "../../common/TimeLine";
|
||||
|
||||
const JobStatusLog = ({ data }) => {
|
||||
// Prepare timeline items
|
||||
|
||||
@ -192,3 +192,13 @@ export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCollectionOverview = (projectId) => {
|
||||
return useQuery({
|
||||
queryKey: ["collection_overview", projectId],
|
||||
queryFn: async () => {
|
||||
const resp = await GlobalRepository.getCollectionOverview(projectId);
|
||||
return resp.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -82,9 +82,9 @@ const GlobalRepository = {
|
||||
return api.get(url);
|
||||
},
|
||||
|
||||
getAttendanceOverview: (projectId, days) => api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`)
|
||||
|
||||
getAttendanceOverview: (projectId, days) => api.get(`/api/dashboard/attendance-overview/${projectId}?days=${days}`),
|
||||
|
||||
getCollectionOverview:(projectId) =>api.get(`/api/Dashboard/collection-overview`)
|
||||
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user