Compare commits

...

4 Commits

12 changed files with 766 additions and 15 deletions

View File

@ -3,11 +3,16 @@ import HorizontalBarChart from "../Charts/HorizontalBarChart";
import { useProjects } from "../../hooks/useProjects";
const ProjectCompletionChart = () => {
const { data: projects = [], isLoading: loading, isError, error } = useProjects();
const { data, isLoading: loading } = useProjects();
const projects = Array.isArray(data)
? data
: data?.data && Array.isArray(data.data)
? data.data
: [];
// Bar chart logic
const projectNames = projects?.map((p) => p.name) || [];
const projectNames = projects?.map((p) => p.name) ?? [];
const projectProgress =
projects?.map((p) => {
const completed = p.completedWork || 0;

View File

@ -3,6 +3,7 @@ import { useController } from "react-hook-form";
const DatePicker = ({
name,
defaultDate,
control,
placeholder = "DD-MM-YYYY",
className = "",

View File

@ -0,0 +1,31 @@
const ActivitiesTable = ({ date, rows }) => {
return (
<div className="reports-activities">
<h2>Activities (Tasks) Performed {date}</h2>
<table className="reports-table">
<thead>
<tr>
<th>NAME</th>
<th>JOB ROLE</th>
<th>CHECK IN</th>
<th>CHECK OUT</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
<td>{row.name}</td>
<td>{row.role}</td>
<td>{row.in}</td>
<td>{row.out}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default ActivitiesTable;

View File

@ -0,0 +1,114 @@
import React from "react";
import Chart from "react-apexcharts";
import { formatFigure } from "../../utils/appUtils";
const Progress = ({
color = "#00aaff",
series = 0,
total = 0,
height = 100,
width = 100,
completed =0,
}) => {
const options = {
chart: {
type: "radialBar",
},
colors: [color],
plotOptions: {
radialBar: {
hollow: { size: "55%" },
track: { background: "#f1f1f1" },
dataLabels: {
name: { show: false },
value: {
show: true,
fontSize: "13px",
color: color,
fontWeight: 600,
offsetY: 7,
formatter: () => `${formatFigure(completed,{notation:"compact"})} / ${formatFigure(total,{notation:"compact"})}`,
},
},
},
},
stroke: { lineCap: "round" },
};
return (
<div style={{ width: "fit-content" }}>
<Chart
options={options}
series={[Number(series)]}
type="radialBar"
height={height}
width={width}
/>
</div>
);
};
export default Progress;
// const Progress = ({
// completed = 0,
// inProgress = 0,
// total = 100,
// height = 140,
// width = 140,
// }) => {
// const completedPercent =
// total > 0 ? Math.round((completed / total) * 100) : 0;
// const progressPercent =
// total > 0 ? Math.round((completed / total) * 100) : 0;
// const options = {
// chart: {
// type: "radialBar",
// },
// colors: ["#28a745", "#0d6efd"],
// plotOptions: {
// radialBar: {
// hollow: {
// size: "35%",
// },
// track: {
// margin: 4,
// },
// dataLabels: {
// name: { show: false },
// value: {
// show: true,
// fontSize: "14px",
// fontWeight: 600,
// offsetY: 5,
// formatter: () => `${completed + inProgress}/${total}`,
// },
// },
// },
// },
// labels: ["Completed", "In Progress"],
// stroke: {
// lineCap: "round",
// },
// };
// return (
// <div style={{ width: "fit-content" }}>
// <Chart
// options={options}
// series={[completedPercent, progressPercent]}
// type="radialBar"
// height={height}
// width={width}
// />
// </div>
// );
// };

View File

@ -0,0 +1,66 @@
import { getCompletionPercentage } from "../../utils/dateUtils";
import Progress from "./Progress";
import ReportsLegend from "./ReportsLegend";
const ReportsDonutCard = ({
title,
value,
total,
donutClass = "",
footer = "Team members present on the site",
chartColor,
}) => {
return (
<div className="border-top card border-primary py-4 px-2">
<h4 className="reports-card-title">{title}</h4>
<div className="d-flex justify-content-center align-items-center flex-column">
<div className="d-inline-flex flex-row align-items-center gap-12 gap-md-3">
<Progress
color={chartColor}
width={120}
height={120}
series={getCompletionPercentage(value, total)}
completed={value}
total={total}
/>
<ReportsLegend />
</div>
</div>
<div className="text-center p-2">
<p className="text-muted mb-0">{footer}</p>
</div>
</div>
);
};
export default ReportsDonutCard;
export const ReportsCard = ({
title,
value,
total,
donutClass = "",
footer = "Team members present on the site",
chartColor,
}) => {
return (
<div className="border-top card border-primary py-4 px-2">
<h4 className="reports-card-title">{title}</h4>
<div className="d-flex justify-content-start align-items-center flex-column">
<div className="d-inline-flex flex-row align-items-center gap-12 gap-md-3">
<ReportsLegend />
</div>
</div>
<div className="text-center p-2">
<p className="text-muted mb-0">{footer}</p>
</div>
</div>
);
};

View File

@ -0,0 +1,22 @@
const ReportsLegend = () => {
return (
<div className=" d-inline-flex flex-column text-start gap-2 pe-5">
<div className=" d-flex align-items-center gap-2">
<span className="reports-legend-color reports-legend-green"></span>
<span>Completed</span>
</div>
<div className=" d-flex align-items-center gap-2">
<span className="reports-legend-color reports-legend-blue"></span>
<span>In Progress</span>
</div>
<div className=" d-flex align-items-center gap-2">
<span className="reports-legend-color reports-legend-gray"></span>
<span>Pending</span>
</div>
</div>
);
};
export default ReportsLegend;

View File

@ -0,0 +1,20 @@
const TeamStrengthCard = ({ data }) => {
return (
<div className="reports-card">
<h4 className="reports-card-title">Team Strength on Site</h4>
<table style={{ width: "100%" }}>
<tbody>
{data.map((item, i) => (
<tr key={i}>
<td style={{ textAlign: "left" }}>{item.role}</td>
<td style={{ textAlign: "right" }}>{item.count}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default TeamStrengthCard;

View File

@ -0,0 +1,121 @@
import React from "react";
import { useProjectReportByProject } from "../../hooks/useReports";
import Progress from "./Progress";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { localToUtc } from "../../utils/appUtils";
import ReportsDonutCard, { ReportsCard } from "./ReportsDonutCard";
const ReportDPR = ({ project, date }) => {
const { data, isLoading, isError, error } = useProjectReportByProject(
project,
localToUtc(date)
);
return (
<>
<div className="card">
<div className="d-flex text-start px-2 mt-2">
Project Status Reported - Generated at{" "}
{formatUTCToLocalTime(data?.date, true)}
</div>
{/* <!-- Status Cards */}
<div className="d-flex justify-content-between flex-wrap gap-5 px-3 py-2">
<ReportsDonutCard
title={"TODAY'S ATTENDANCE"}
total={data?.totalEmployees}
value={data?.todaysAttendances}
/>
<ReportsDonutCard
title={"DAILY TASKS COMPLETED"}
total={data?.totalCompletedTask}
value={data?.totalPlannedWork}
chartColor={"blue"}
/>
<ReportsDonutCard
title={"PROJECT COMPLETION STATUS"}
total={data?.totalPlannedWork}
value={data?.totalCompletedWork}
chartColor={"green"}
/>
<div class="card px-3 border-top border-warning">
<h4 class="reports-card-title">Attendance Pending Report</h4>
<p>Team member present</p>
<div className="d-flex flex-column gap-2">
<div className="d-flex justify-content-between">
<span className="text-secondry"> Regualrization</span>{" "}
<span className="text-secondry">
{" "}
{data?.regularizationPending}
</span>{" "}
</div>
<div className="d-flex justify-content-between">
<span className="text-secondry"> Checking</span>{" "}
<span className="text-secondry"> {data?.checkoutPending}</span>{" "}
</div>
<div className="d-flex justify-content-between">
<span className="text-secondry"> Total Employee</span>{" "}
<span className="text-secondry">
{" "}
{ data?.todaysAttendances}
</span>{" "}
</div>
</div>
</div>
<div className="reports-card">
{/* {/* <!-- Row 1: Header */}
<div>
<h4 class="reports-card-title">Team Strength on Site</h4>
</div>
<table style={{ width: "100%" }}>
<tbody>
{data?.teamOnSite?.map((member, index) => (
<tr key={index}>
<td style={{ textAlign: "left" }}>{member?.roleName}</td>
<td style={{ textAlign: "right" }}>
{member?.numberofEmployees}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* {/* <!-- Activities */}
<div className="reports-activities">
<h2>Activities (Tasks) Performed 17-Sep-2025</h2>
<table className="reports-table">
<thead>
<tr>
<th>NAME</th>
<th>JOB ROLE</th>
<th>CHECK IN</th>
<th>CHECK OUT</th>
</tr>
</thead>
<tbody>
{data?.performedAttendance?.map((att) => (
<tr>
<td>{att.name}</td>
<td>{att.roleName}</td>
<td>{formatUTCToLocalTime(att.inTime, true)}</td>
<td>
{att.outTime
? formatUTCToLocalTime(att.outTime, true)
: "--"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
);
};
export default ReportDPR;

16
src/hooks/useReports.js Normal file
View File

@ -0,0 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import { ReportsRepository } from "../repositories/GlobalRepository";
export const useProjectReportByProject = (projectId, date) => {
return useQuery({
queryKey: ["daily_report", projectId, date],
queryFn: async () => {
const resp = await ReportsRepository.getDailyProgressByProject(
projectId,
date
);
return resp.data;
},
enabled: !!projectId && !!date,
});
};

View File

@ -0,0 +1,282 @@
/* .reports-container {
margin: 20px auto;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
} */
.reports-header {
color: #fff;
padding: 20px;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.reports-header h1 {
font-size: 22px;
margin: 0;
}
.reports-header .project-info {
font-size: 14px;
text-align: right;
}
.reports-status-note {
font-size: 12px;
color: #555;
padding: 15px 20px 0 20px;
}
.reports-status-cards {
display: flex;
justify-content: space-between;
gap: 15px;
padding: 20px;
flex-wrap: wrap;
}
.reports-card {
flex: 1;
min-width: 200px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
text-align: center;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
/* <-- added shadow */
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-top: 1px solid #e63946;
}
.reports-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.reports-card h3 {
font-size: 14px;
margin: 0 0 10px 0;
}
.reports-card p {
margin: 5px 0;
}
.reports-card .value {
font-size: 22px;
font-weight: bold;
}
.reports-card-title {
font-size: 0.9rem;
text-transform: uppercase;
font-weight: 600;
color: #6c757d;
}
.reports-attendance {
color: #b10000;
}
.reports-tasks {
color: #007bff;
}
.reports-completion {
color: #28a745;
}
.reports-activities {
padding: 20px;
}
.reports-activities h2 {
font-size: 18px;
margin-bottom: 10px;
}
.reports-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.reports-table th,
.reports-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
.reports-table th {
background: #f0f0f0;
}
/* .footer {
background: #b10000;
color: #fff;
text-align: center;
padding: 15px;
font-size: 12px;
}
.footer a {
color: #fff;
margin: 0 8px;
text-decoration: none;
} */
/* Responsive */
@media (max-width: 600px) {
.reports-header {
flex-direction: column;
text-align: center;
}
.reports-header .reports-project-info {
text-align: center;
margin-top: 10px;
}
.reports-status-cards {
flex-direction: column;
}
}
.reports-legend {
margin-top: 10px;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
font-size: 12px;
color: #555;
}
.reports-legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.reports-legend-color {
width: 10px;
height: 10px;
border-radius: 2px;
display: inline-block;
}
.reports-legend-red {
background: #b10000;
}
.reports-legend-blue {
background: #007bff;
}
.reports-legend-green {
background: #28a745;
}
.reports-legend-gray {
background: #ccc;
}
.reports-donut {
--percentage: 65;
/* Change this per chart */
--danger: #e63946;
--primary: #007bff;
--warning: #ffc107;
--success: #198754;
/* Fill color */
--track: #e9ecef;
/* Background track */
--size: 120px;
/* Default size */
--thickness: 20px;
/* Default thickness */
width: var(--size);
height: var(--size);
border-radius: 50%;
background: conic-gradient(
var(--danger) calc(var(--percentage) * 1%),
var(--track) 0
);
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-family: Arial, sans-serif;
font-weight: bold;
color: #333;
}
.reports-donut::before {
content: "";
position: absolute;
width: calc(var(--size) - var(--thickness));
height: calc(var(--size) - var(--thickness));
border-radius: 50%;
background: #fff;
/* Inner cut-out */
}
.reports-donut span {
position: absolute;
font-size: calc(var(--size) / 6);
}
/* Variants */
.reports-donut.thin {
--size: 80px;
--thickness: 12px;
}
.reports-donut.medium {
--size: 120px;
--thickness: 25px;
}
.reports-donut.large {
--size: 180px;
--thickness: 35px;
}
/* Color variants */
.reports-donut-success {
background: conic-gradient(
var(--success) calc(var(--percentage) * 1%),
var(--track) 0
);
color: var(--success);
}
.reports-donut-warning {
background: conic-gradient(
var(--warning) calc(var(--percentage) * 1%),
var(--track) 0
);
color: var(--warning);
}
.reports-donut-danger {
background: conic-gradient(
var(--danger) calc(var(--percentage) * 1%),
var(--track) 0
);
color: var(--danger);
}
.reports-donut-primary {
background: conic-gradient(
var(--primary) calc(var(--percentage) * 1%),
var(--track) 0
);
color: var(--primary);
}

View File

@ -1,8 +1,69 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { ComingSoonPage } from "../Misc/ComingSoonPage";
import ReportDPR from "../../components/reports/report-dpr";
import "./Reports.css";
import Breadcrumb from "../../components/common/Breadcrumb";
import { useProjectName } from "../../hooks/useProjects";
import DatePicker from "../../components/common/DatePicker";
import { useForm } from "react-hook-form";
const Reports = () => {
return <ComingSoonPage></ComingSoonPage>;
const [selectedProject, setSelectedProject] = useState();
const { projectNames, isError, loading } = useProjectName(true);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const { control, watch } = useForm({
defaultValues: {
startDate: yesterday.toISOString().split("T")[0],
},
});
const selelectedDate = watch("startDate");
useEffect(()=>{
if(!selectedProject && projectNames){
setSelectedProject(projectNames[0]?.id)
}
},[projectNames])
return (
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Daily Progress Report", link: null },
]}
/>
<div className="card my-3 px-sm-4 px-0">
<div className="card-body py-2 px-1 mx-2">
<div className="row align-items-center mb-0">
<div className="col-12 col-md-6 ">
<div className="w-max">
<select
className="form-select form-select-sm"
onChange={(e) => setSelectedProject(e.target.value)}
>
{projectNames?.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select>
</div>
</div>
<div className="col-12 col-md-6 d-flex justify-content-end">
<DatePicker
name="startDate"
control={control}
placeholder="Select Date"
maxDate={new Date()}
/>
</div>
</div>
</div>
</div>
<ReportDPR project={selectedProject} date={selelectedDate} />
</div>
);
};
export default Reports;

View File

@ -1,7 +1,11 @@
import { api } from "../utils/axiosClient";
const GlobalRepository = {
getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => {
getDashboardProgressionData: ({
days = "",
FromDate = "",
projectId = "",
}) => {
let params;
if (projectId == null) {
params = new URLSearchParams({
@ -20,8 +24,9 @@ const GlobalRepository = {
},
getDashboardAttendanceData: (date, projectId) => {
return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`);
return api.get(
`/api/Dashboard/project-attendance/${projectId}?date=${date}`
);
},
getDashboardProjectsCardData: () => {
return api.get(`/api/Dashboard/projects`);
@ -42,7 +47,7 @@ const GlobalRepository = {
},
getExpenseData: (projectId, startDate, endDate) => {
let url = `api/Dashboard/expense/type`
let url = `api/Dashboard/expense/type`;
const queryParams = [];
if (projectId) {
queryParams.push(`projectId=${projectId}`);
@ -60,10 +65,15 @@ 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`
let url = `api/Dashboard/expense/monthly`;
const queryParams = [];
if (projectId) {
queryParams.push(`projectId=${projectId}`);
@ -80,11 +90,13 @@ 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}`),
};
export default GlobalRepository;
export const ReportsRepository = {
getDailyProgressByProject: (projectId,date) =>
api.get(`/api/Market/get/project/report/${projectId}?date=${date}`),
};