Compare commits

...

59 Commits

Author SHA1 Message Date
bc114b6ae0 Merge remote-tracking branch 'origin/Subscription_Plan' into Purchase_Invoice_Management 2025-12-16 11:06:57 +05:30
92a09bfcf0 Handle Date range in Expense Widget on dashboard
Link Project to detail page in Attendance Report Widget
2025-12-15 11:45:43 +05:30
4f9fa1b7c2 Add link to he project from Attendance details widget 2025-12-15 11:08:27 +05:30
34d5ce9ef9 missing request demo links 2025-12-14 17:22:21 +05:30
476fb49e07 adjust sidebar logo size and position from left border 2025-12-14 17:03:12 +05:30
7143af1e1e format DPR page 2025-12-14 16:49:06 +05:30
a477090cb8 Add missing images 2025-12-14 16:07:46 +05:30
f1a5f72db7 modified landing page hero images 2025-12-14 13:27:50 +05:30
e8886577d8 Correction in weidget. 2025-12-13 16:36:47 +05:30
e10a6ff14c Change the design of subscription page. 2025-12-13 14:51:17 +05:30
a309d13247 Correction in teams, projectinfra and daily task for dropdown. 2025-12-13 11:51:16 +05:30
fa694d8361 Correction in Attendance page. 2025-12-13 11:37:53 +05:30
a9bbd75d6c Correction in name of Project by status to Attendance by Project 2025-12-13 11:26:50 +05:30
3ff80ee032 Sorting in Subscription page. 2025-12-13 11:20:16 +05:30
e695807e77 Adding 2025-12-13 11:18:45 +05:30
630c11985d Correction in Subscription page. 2025-12-13 11:18:11 +05:30
d68cb9e664 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Subscription_Plan 2025-12-13 09:59:35 +05:30
caeece0660 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Purchase_Invoice_Management 2025-12-13 09:57:19 +05:30
44f3d8783d Adding additional fields in landing subscription cards. 2025-12-13 09:54:02 +05:30
3a8c1745f4 Removing dropdown logic all the places wehre dropdown is added. 2025-12-12 19:07:55 +05:30
deba5dfa01 Adding View functionality in Add challan. 2025-12-12 18:11:38 +05:30
bb743d2bb0 Adding Submit functionality in tenant. 2025-12-12 17:27:37 +05:30
6f9eeadc22 fixed project list paginantion 2025-12-12 16:22:55 +05:30
6099dd2ea5 fixed logo label on side bar 2025-12-12 15:25:21 +05:30
7b1b360c78 added missing view_all_employee permissions 2025-12-12 10:31:36 +05:30
cdaf642eba added zoom in - out using mouse wheel 2025-12-11 16:01:11 +05:30
b31195c5a1 Adding sorting in Project display. 2025-12-11 11:04:29 +05:30
9daeffd90e Adding formatcurrecny at hover effect at Expense Breakdown. 2025-12-11 10:54:24 +05:30
ab34ea63fa Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Purchase_Invoice_Management 2025-12-11 10:39:36 +05:30
17ef307ff2 Adding MyJobs at the place of all jobs in service Jobs. 2025-12-11 10:39:32 +05:30
ae9ef7938d fixed : Project created permission for service 2025-12-11 10:37:39 +05:30
e755faecdc fixed: added array guards in mergedServices to stop iterable error 2025-12-11 10:25:45 +05:30
23660379c9 fixed 'ProjectwiseTeamCount' path inside dashboard 2025-12-11 09:51:01 +05:30
2bf5e9a13f Merge branch 'Encrypted_Reponse' into Purchase_Invoice_Management 2025-12-11 09:40:09 +05:30
3413806670 Merge branch 'Encrypted_Reponse' into Purchase_Invoice_Management 2025-12-11 09:39:51 +05:30
8033fdb7e7 selected default current selected service - create task 2025-12-10 16:52:52 +05:30
b62fc82a9c Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Purchase_Invoice_Management 2025-12-10 16:33:04 +05:30
4253ed71eb modified ui base on suggestion 2025-12-10 16:32:59 +05:30
01fc302011 Merge pull request 'Adding_Chips_DailyTask :- Adding Chips in Daily task report.' (#538) from Adding_Chips_DailyTask into Purchase_Invoice_Management
Reviewed-on: #538
merged
2025-12-10 11:01:16 +00:00
d56aefde02 Adding sizing in manageemployee. 2025-12-10 11:01:16 +00:00
c71fe3a45e Adding dates chips in document. 2025-12-10 11:01:16 +00:00
0f43c877c4 Adding Date picker chips in Expense and 2025-12-10 11:01:16 +00:00
153ffcdc3e Adding w-100 at doument filter panel. 2025-12-10 11:01:16 +00:00
e31e4cfc31 Adding Filter chips at Tenant. 2025-12-10 11:01:16 +00:00
8216bf1f2d Correction in Infra Project Card view. 2025-12-10 11:01:16 +00:00
6b9d7c56bc Adding filter chips in daily Progress Report. 2025-12-10 11:01:16 +00:00
07ba95e533 removed console warning 2025-12-10 12:40:30 +05:30
c1ae9ee55e changed footer 2025-12-10 12:26:43 +05:30
117d82769a Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into dpr-report 2025-12-10 12:16:11 +05:30
35f1aa8c13 hide attendance project wise overview project having selected 2025-12-10 12:12:12 +05:30
cd1ae64753 added attandace poject wise overview 2025-12-10 12:09:53 +05:30
363a9c5feb Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Purchase_Invoice_Management 2025-12-10 10:32:00 +05:30
69148331f5 added partially proejctwise attendace 2025-12-10 10:31:54 +05:30
65158b9368 added encryption 2025-12-09 16:37:13 +05:30
96bcdffdca added optional chain for n=menu 2025-12-05 18:45:19 +05:30
b348117f05 added Total employee present figure 2025-11-25 09:50:39 +05:30
1157643916 added report data by project and date wise 2025-11-24 16:08:45 +05:30
37212e489e splitted code and added server data 2025-11-22 16:38:39 +05:30
b2c0388412 dpr report wireframe 2025-11-22 13:14:57 +05:30
74 changed files with 2471 additions and 962 deletions

7
package-lock.json generated
View File

@ -18,6 +18,7 @@
"apexcharts": "^4.5.0",
"axios": "^1.7.9",
"axios-retry": "^4.5.0",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"dotenv-webpack": "^8.1.0",
@ -2414,6 +2415,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",

View File

@ -21,6 +21,7 @@
"apexcharts": "^4.5.0",
"axios": "^1.7.9",
"axios-retry": "^4.5.0",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"dotenv-webpack": "^8.1.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

After

Width:  |  Height:  |  Size: 52 KiB

BIN
public/img/hero/bg-011.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,95 @@
import React, { useMemo } from "react";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
const TaskReportFilterChips = ({ filter, filterData, removeFilterChip, clearFilter }) => {
const data = filterData?.data || filterData || {};
const filterChips = useMemo(() => {
const chips = [];
const addGroup = (ids, list, label, key) => {
if (!ids || ids.length === 0) return;
const items = ids.map((id) => ({
id,
name: list?.find((i) => i.id === id)?.name || id,
}));
chips.push({ key, label, items });
};
// Building
addGroup(filter?.buildingIds, data?.buildings, "Building", "buildingIds");
// Floor
addGroup(filter?.floorIds, data?.floors, "Floor", "floorIds");
// Activities
addGroup(filter?.activityIds, data?.activities, "Activity", "activityIds");
// Date Range Chips
if (filter?.dateFrom || filter?.dateTo) {
chips.push({
key: "date",
label: "Date Range",
items: [
{
id: "date-range",
name: `${filter?.dateFrom ? formatUTCToLocalTime(filter.dateFrom) : ""}
${filter?.dateTo ? " to " + formatUTCToLocalTime(filter.dateTo) : ""}`,
},
],
});
}
return chips;
}, [filter, filterData]);
if (!filterChips.length) return null;
return (
<div className="d-flex flex-wrap align-items-center gap-2 mt-2">
{filterChips.map((chipGroup) => (
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
<span className="fw-semibold me-2">{chipGroup.label}:</span>
{chipGroup.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
>
<span>{item.name}</span>
{/* If date chip → remove whole date range */}
{chipGroup.key === "date" ? (
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => clearFilter()}
/>
) : (
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chipGroup.key, item.id)}
/>
)}
</span>
))}
</div>
))}
</div>
);
};
export default TaskReportFilterChips;

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { useCurrentService } from "../../hooks/useProjects";
import { useSelectedProject } from "../../slices/apiDataManager";
import { FormProvider, useForm } from "react-hook-form";
@ -11,8 +11,9 @@ import { DateRangePicker1 } from "../common/DateRangePicker";
import SelectMultiple from "../common/SelectMultiple";
import { localToUtc } from "../../utils/appUtils";
import { useTaskFilter } from "../../hooks/useTasks";
import { set } from "date-fns";
const TaskReportFilterPanel = ({ handleFilter }) => {
const TaskReportFilterPanel = forwardRef(({ handleFilter, setFilterdata, clearFilter }, ref) => {
const [resetKey, setResetKey] = useState(0);
const selectedProject = useSelectedProject();
const selectedService = useCurrentService();
@ -23,10 +24,42 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
defaultValues: TaskReportDefaultValue,
});
const dynamicDefaultFilter = useMemo(() => {
return {
...TaskReportDefaultValue,
buildingIds: TaskReportDefaultValue.buildingIds || [],
floorIds: TaskReportDefaultValue.floorIds || [],
activityIds: TaskReportDefaultValue.activityIds || [],
dateFrom: TaskReportDefaultValue.dateFrom,
dateTo: TaskReportDefaultValue.dateTo,
};
}, [selectedProject]);
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
// Reset specific field
if (value !== undefined) {
setValue(name, value);
} else {
// Fix: Use TaskReportDefaultValue or get current values before setting new ones
// reset({ ...methods.getValues(), [name]: TaskReportDefaultValue[name] }); // Updated to use TaskReportDefaultValue
setValue(name, TaskReportDefaultValue[name]);
}
},
getValues: methods.getValues,
onClear: onClear, // 💡 EXPOSE THE ONCLEAR FUNCTION
}));
useEffect(() => {
if (data && setFilterdata) {
setFilterdata(data);
}
}, [data, setFilterdata]);
const {
register,
reset,
handleSubmit,
setValue,
formState: { errors },
} = methods;
const closePanel = () => {
@ -105,6 +138,6 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
</form>
</FormProvider>
);
};
});
export default TaskReportFilterPanel;

View File

@ -17,8 +17,9 @@ import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import Pagination from "../common/Pagination";
import { TaskReportListSkeleton } from "./TaskRepprtListSkeleton";
import HoverPopup from "../common/HoverPopup";
import TaskReportFilterChips from "./TaskReportFilterChips";
const TaskReportList = () => {
const TaskReportList = ({ filter, filterData, removeFilterChip, clearFilter }) => {
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState({
selectedBuilding: "",
@ -29,7 +30,7 @@ const TaskReportList = () => {
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
const { service, openModal, closeModal, filter } = useDailyProgrssContext();
const { service, openModal, closeModal, filter: contextFilter } = useDailyProgrssContext();
const selectedProject = useSelectedProject();
const { projectNames } = useProjectName();
@ -37,7 +38,7 @@ const TaskReportList = () => {
selectedProject,
ITEMS_PER_PAGE,
currentPage,
service, filter
service, contextFilter
);
const ProgrssReportColumn = [
@ -193,6 +194,16 @@ const TaskReportList = () => {
if (isError) return <div>Loading....</div>;
return (
<div>
<div className="main-content">
<div className="col-12 mb-2 mt-2 px-4">
<TaskReportFilterChips
filter={filter}
filterData={filterData}
removeFilterChip={removeFilterChip}
clearFilter={clearFilter}
/>
</div>
<div className="mt-2 table-responsive text-nowrap">
<table className="table">
<thead>
@ -312,6 +323,7 @@ const TaskReportList = () => {
)
}
</div >
</div >
);
};

View File

@ -31,7 +31,7 @@ const selectedProjectId = useSelectedProject()
<div className="card-header mb-1 pb-0">
<div className="d-flex flex-wrap justify-content-between align-items-center">
<div className="card-title mb-0 text-start">
<h5 className="mb-1">Attendance</h5>
<h5 class="card-title m-0 me-2">Attendance</h5>
<p className="card-subtitle">Daily Attendance Data</p>
</div>

View File

@ -104,7 +104,7 @@ const AttendanceOverview = () => {
{/* Header */}
<div className="d-flex mt-2 justify-content-between align-items-center mb-3">
<div className="card-title mb-0 text-start">
<h5 className="mb-1 fw-bold">Attendance Overview</h5>
<h5 className="mb-1 fw-semibold">Attendance Overview</h5>
<p className="card-subtitle">Role-wise present count</p>
</div>
<div className="d-flex gap-2">

View File

@ -78,7 +78,7 @@ const CollectionOverview = ({ data, isLoading }) => {
tooltip: {
custom: ({ series, seriesIndex, dataPointIndex }) => {
return `
<div class="px-2 py-1">
<div className="px-2 py-1">
<strong>${labels[dataPointIndex]}</strong><br>
${series[seriesIndex][dataPointIndex].toLocaleString()}
</div>
@ -235,7 +235,7 @@ export const TopicBarChart = ({ data,isLoading }) => {
enabled: true,
custom: ({ series, seriesIndex, dataPointIndex }) => {
return `
<div class="px-3 py-2">
<div className="px-3 py-2">
<span>${series[seriesIndex][
dataPointIndex
].toLocaleString()}</span>
@ -256,8 +256,8 @@ export const TopicBarChart = ({ data,isLoading }) => {
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 className="card-header d-flex align-items-center justify-content-between">
<h5 className="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>
@ -274,7 +274,7 @@ export const TopicBarChart = ({ data,isLoading }) => {
</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="card-header d-flex align-items-end justify-content-between"></div>
<div className="p-1 m-1 text-start">
<small className="fw-medium">Overdue Days</small>
</div>

View File

@ -27,6 +27,8 @@ import {
} from "../../utils/constants";
import CollectionOverview, { TopicBarChart } from "./CollectionOverview";
import { CollectionOverviewSkeleton } from "./CollectionOverviewSkeleton";
import ProjectWiseTeamCount from "./ProjectWiseTeamCount";
const Dashboard = () => {
// Get the selected project ID from Redux store
@ -41,24 +43,24 @@ const Dashboard = () => {
<div className="container-fluid mt-5">
<div className="row gy-4">
{isAllProjectsSelected && (
<div className="col-sm-6 col-lg-4">
<div className="col-sm-6 col-lg-6">
<Projects />
</div>
)}
<div
className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"
className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-6"
}`}
>
<Teams />
</div>
<div
className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"
{!isAllProjectsSelected && ( <div
className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-6"
}`}
>
<TasksCard />
</div>
</div>)}
<div className="col-12 col-xl-4 col-md-6">
<div className="card ">
<ExpenseStatus />
@ -84,10 +86,13 @@ const Dashboard = () => {
</div>
{!isAllProjectsSelected &&
(canRegularize || canTeamAttendance || canSelfAttendance) && (
<div className="col-12 col-md-8 mb-sm-0 mb-4">
<div className="col-12 col-md-6 mb-sm-0 mb-4">
<AttendanceOverview />
</div>
)}
{isAllProjectsSelected && <div className="col-12 col-md-6 mb-sm-0 mb-4">
<ProjectWiseTeamCount />
</div>}
{!isAllProjectsSelected && (
<div className="col-xxl-4 col-lg-4">
@ -97,7 +102,7 @@ const Dashboard = () => {
</div>
)}
{isAllProjectsSelected && (
<div className="col-12 col-md-6 mb-sm-0 mb-4">
<div className="col-12 col-md-4 mb-sm-0 mb-4">
<ServiceJobs />
</div>
)}
@ -118,80 +123,3 @@ const Dashboard = () => {
};
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>

View File

@ -52,6 +52,18 @@ const ExpenseAnalysis = () => {
legend: { show: false },
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
colors: flatColors,
tooltip: {
y: {
formatter: function (value) {
return formatCurrency(value);
},
},
x: {
formatter: function (label) {
return label;
},
},
},
plotOptions: {
pie: {
donut: {
@ -88,7 +100,7 @@ const ExpenseAnalysis = () => {
<div className="text-end text-sm-end">
<FormProvider {...methods}>
<DateRangePicker1 />
<DateRangePicker1 pastDays="30" />
</FormProvider>
</div>
</div>
@ -140,7 +152,9 @@ const ExpenseAnalysis = () => {
className="col-6"
key={idx}
style={{
borderLeft: `3px solid ${flatColors[idx % flatColors.length]}`,
borderLeft: `3px solid ${
flatColors[idx % flatColors.length]
}`,
}}
>
<div className="d-flex flex-column text-start">
@ -165,7 +179,6 @@ const ExpenseAnalysis = () => {
</span>
</div>
</div>
))}
</div>
</div>

View File

@ -92,45 +92,40 @@ const ExpenseByProject = () => {
<div className="card shadow-sm h-100 rounded ">
{/* Header */}
<div className="card-header">
<div className="d-flex justify-content-between align-items-center mb-1 mt-1">
<div className="d-block justify-content-between align-items-center mb-1 mt-1">
<div className="text-start">
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5>
<p className="card-subtitle m-0">{projectName}</p>
<h5 className="mb-1 me-6 card-title">Monthly Expense </h5>
<div className="row w-100">
<div className="col-6"> <p className="card-subtitle m-0">{projectName}</p></div>
<div className="col-6 d-flex justify-content-between align-items-center px-0">
<select
className="form-select form-select-sm ms-auto mt-sm-0"
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
disabled={typeLoading}
style={{ maxWidth: "200px" }}
>
<option value="">All Categories</option>
{expenseCategories?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
</div>
<div className="btn-group mb-5 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>
</div>
</div>
{/* Range Buttons + Expense Dropdown */}
@ -148,22 +143,7 @@ const ExpenseByProject = () => {
{item}
</button>
))}
{viewMode === "Category" && (
<select
className="form-select form-select-sm ms-auto mb-3 mt-1 mt-sm-0"
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
disabled={typeLoading}
style={{ maxWidth: "200px" }}
>
<option value="">All Types</option>
{expenseCategories?.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
)}
</div>
</div>

View File

@ -92,7 +92,7 @@ const ProjectProgressChart = ({
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
{/* Left: Title */}
<div className="card-title text-start">
<h5 className="mb-1 fw-bold">Project Progress</h5>
<h5 className="mb-1 fw-semibold">Project Progress</h5>
<p className="card-subtitle">Progress Overview by Project</p>
</div>
</div>

View File

@ -0,0 +1,112 @@
import React from "react";
import { useProjectName } from "../../hooks/useProjects";
import { BUCKET_BG_CLASSES } from "../../utils/constants";
import { useAttendaceProjectWiseOveriew } from "../../hooks/useDashboard_Data";
import { AppColors, localToUtc } from "../../utils/appUtils";
import DatePicker from "../common/DatePicker";
import { useAppForm, useAppWatch } from "../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { setProjectId } from "../../slices/localVariablesSlice";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
const ProjectWiseTeamCount = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { control } = useAppForm({
resolver: zodResolver(
z.object({
date: z.string().optional(),
})
),
defaultValues: {
date: new Date().toISOString().slice(0, 10),
},
});
const goToProject = (projectId) => () => {
dispatch(setProjectId(projectId));
navigate(`/projects/details`);
};
const selectedDate = useAppWatch({ control, name: "date" });
const { data, isLoading, isFetching, isError, error } =
useAttendaceProjectWiseOveriew(localToUtc(selectedDate));
const percent = (teamCount, attendanceCount) => {
return teamCount > 0 ? Math.round((attendanceCount / teamCount) * 100) : 0;
};
return (
<div className="card h-100 p-3">
{/* Header */}
<div className="d-flex justify-content-between text-start mb-2">
<h5 className="card-title m-0 me-2">Attendance by Project</h5>
<DatePicker name="date" control={control} maxDate={new Date()} />
</div>
{/* Only show spinner for new data, not full component */}
{isFetching && !isLoading && (
<div className="small text-end text-muted">Updating</div>
)}
{/* Table */}
<div className="table-container">
<table className="table table-borderless mb-0">
<thead className="table-header">
<tr>
<th style={{ width: 200 }} className="text-start">
Project
</th>
<th style={{ width: 100 }}>Team Size</th>
<th style={{ width: 80 }} className="text-start">
Logged In
</th>
{/* <th>Percentage</th> */}
</tr>
</thead>
</table>
<div
className="table-body-scroll overflow-auto pe-1"
style={{ maxHeight: "60vh" }}
>
<table className="table table-borderless mb-0">
<tbody>
{(data ?? []).map((item, index) => (
<tr key={item.projectId || index}>
<td style={{ width: 200 }}>
<div
className="d-flex align-items-center text-wrap my-2 text-start"
style={{ width: "180px" }}
>
<a
onClick={goToProject(item.projectId)}
className="text-heading text-truncate cursor-pointer"
>
{" "}
<span className="text-heading">{item.projectName}</span>
</a>
</div>
</td>
<td className="text-center" style={{ width: 80 }}>
{item.teamCount}
</td>
<td className="text-center" style={{ width: 80 }}>
{item.attendanceCount}
</td>
{/* <td>{percent(item.teamCount, item.attendanceCount)}%</td> */}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default ProjectWiseTeamCount;

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { useJobsProgression } from "../../hooks/useDashboard_Data";
import { SpinnerLoader } from "../common/Loader";
@ -7,56 +7,90 @@ import { useServiceProject } from "../../hooks/useServiceProject";
const ServiceJobs = () => {
const { projectId } = useParams();
const { data, isLoading, isError } = useJobsProgression(projectId);
const { data, isLoading, isError } = useJobsProgression(projectId);
const jobs = data || {};
const { data: projectData, isLoading: projectLoading } = useServiceProject(projectId);
const { data: projectData, isLoading: projectLoading } =
useServiceProject(projectId);
const [activeTab, setActiveTab] = useState("tab-new");
// 👇 prevents re-running auto logic after first load
const hasInitializedTab = useRef(false);
const tabMapping = [
{ id: "tab-new", label: "My Jobs", key: "allJobs" },
{ id: "tab-preparing", label: "Assigned", key: "assignedJobs" },
{ id: "tab-new", label: "My Jobs", key: "myJobs" },
{ id: "tab-shipping", label: "In Progress", key: "inProgressJobs" },
{ id: "tab-preparing", label: "Assigned", key: "assignedJobs" },
];
/* ---------- INITIAL TAB SELECTION ONLY ---------- */
useEffect(() => {
if (hasInitializedTab.current || !jobs) return;
if (jobs.myJobs?.length > 0) {
setActiveTab("tab-new");
} else if (jobs.inProgressJobs?.length > 0) {
setActiveTab("tab-shipping");
} else {
setActiveTab("tab-preparing");
}
hasInitializedTab.current = true;
}, [jobs]);
return (
<div className="">
<div>
<div className="card page-min-h">
{/* Header */}
<div className="card-header d-flex justify-content-between">
<div className="card-title mb-0 text-start">
<h5 className="mb-1 fw-bold">Service Jobs</h5>
<p className="card-subtitle">
{projectLoading ? "Loading..." : projectData?.name || "All Projects"}
{projectLoading
? "Loading..."
: projectData?.name || "All Projects"}
</p>
</div>
</div>
<div className="card-body p-0">
<div className="nav-align-top">
{/* ---------------- Tabs ---------------- */}
<ul className="nav nav-tabs nav-fill rounded-0 timeline-indicator-advanced" role="tablist">
{tabMapping.map((t, index) => (
<li className="nav-item" key={t.id}>
{/* Tabs */}
<ul className="nav nav-tabs nav-fill rounded-0 timeline-indicator-advanced">
{tabMapping.map((tab) => (
<li className="nav-item" key={tab.id}>
<button
className={`nav-link ${index === 0 ? "active" : ""}`}
data-bs-toggle="tab"
data-bs-target={`#${t.id}`}
type="button"
className={`nav-link ${
activeTab === tab.id ? "active" : ""
}`}
onClick={() => setActiveTab(tab.id)}
>
{t.label}
{tab.label}
</button>
</li>
))}
</ul>
{/* ---------------- Tab Content ---------------- */}
{/* Content */}
<div className="tab-content border-0 mx-1 text-start">
{isLoading && (
<div className="text-center" style={{ height: "250px", display: "flex", justifyContent: "center", alignItems: "center" }}>
<div
className="text-center"
style={{
height: "250px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<SpinnerLoader />
</div>
)}
{isError && (
<p
className="text-center"
@ -70,19 +104,19 @@ const ServiceJobs = () => {
>
No data found
</p>
)}
{!isLoading &&
!isError &&
tabMapping.map((t, index) => {
const list = jobs[t.key] || [];
tabMapping.map((tab) => {
const list = jobs[tab.key] || [];
return (
<div
key={t.id}
className={`tab-pane fade ${index === 0 ? "show active" : ""}`}
id={t.id}
key={tab.id}
className={`tab-pane fade ${
activeTab === tab.id ? "show active" : ""
}`}
>
{list.length === 0 ? (
<p
@ -97,24 +131,19 @@ const ServiceJobs = () => {
>
No jobs found
</p>
) : (
<div className="job-scroll-wrapper">
{list.map((job, i) => (
<React.Fragment key={i}>
{list.map((job, index) => (
<React.Fragment key={index}>
<ul className="timeline mb-0">
{/* Assigned By */}
<li className="timeline-item ps-6 border-left-dashed">
<span className="timeline-indicator-advanced timeline-indicator-success border-0 shadow-none">
<i className="bx bx-check-circle"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-success text-uppercase">
Assigned By
</small>
</div>
<h6 className="my-50">{job.assignedBy}</h6>
<p className="text-body mb-0">
{formatUTCToLocalTime(job.assignedAt)}
@ -122,23 +151,23 @@ const ServiceJobs = () => {
</div>
</li>
{/* Project */}
<li className="timeline-item ps-6 border-transparent">
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
<i className="bx bx-map"></i>
</span>
<div className="timeline-event ps-1">
<div className="timeline-header">
<small className="text-primary text-uppercase">Project</small>
</div>
<small className="text-primary text-uppercase">
Project
</small>
<h6 className="my-50">{job.project}</h6>
<p className="text-body mb-0">{job.title}</p>
<p className="text-body mb-0">
{job.title}
</p>
</div>
</li>
</ul>
{/* Divider */}
{i < list.length - 1 && (
{index < list.length - 1 && (
<div className="border-1 border-light border-top border-dashed my-4"></div>
)}
</React.Fragment>

View File

@ -59,12 +59,18 @@ const DocumentFilterPanel = forwardRef(
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
if (value !== undefined) {
setValue(name, value);
setValue(name, value, { shouldValidate: true, shouldDirty: true });
} else {
reset({ ...methods.getValues(), [name]: DocumentFilterDefaultValues[name] });
}
if ((name === "startDate" || name === "endDate") && value === null) {
setTimeout(() => {
setResetKey((prev) => prev + 1);
}, 0);
}
},
getValues: methods.getValues, // optional, to read current filter state
getValues: methods.getValues,
}));
//changes
@ -141,6 +147,7 @@ const DocumentFilterPanel = forwardRef(
defaultRange={false}
resetSignal={resetKey}
maxDate={new Date()}
className="w-100"
/>
</div>

View File

@ -129,6 +129,7 @@ const Documents = ({ Document_Entity, Entity }) => {
else if (key === "dateRange") {
updatedFilters.startDate = null;
updatedFilters.endDate = null;
// These calls correctly tell the DocumentFilterPanel to update its form state:
updatedRef.current?.resetFieldValue("startDate", null);
updatedRef.current?.resetFieldValue("endDate", null);
}
@ -143,7 +144,7 @@ const Documents = ({ Document_Entity, Entity }) => {
<DocumentContext.Provider value={contextValues}>
<div className="mt-2">
<div className="card page-min-h d-flex p-5">
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
<div className="row align-items-center">
{/* Search */}
<div className="d-flex flex-row gap-2 col-12 col-md-8 col-lg-4 mb-md-0 align-items-center mb-2">
@ -194,6 +195,9 @@ const Documents = ({ Document_Entity, Entity }) => {
)}
</div>
</div>
<div className="ms-n1">
<DocumentFilterChips filters={filters} filterData={filterData} removeFilterChip={removeFilterChip} />
</div>
<DocumentsList
Document_Entity={DocumentEntity}
Entity={Entity}

View File

@ -127,7 +127,7 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
const hasAccessAplication = watch("hasApplicationAccess");
return (
<>
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-2 p-2">
<div className="text-center">
<p className="fs-5 fw-semibold">
{" "}

View File

@ -94,6 +94,14 @@ const ExpenseFilterPanel = forwardRef(
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
}
},
// --- START FIX: Add resetDateRange method ---
resetDateRange: () => {
setValue("startDate", null);
setValue("endDate", null);
// Trigger re-render/reset of the DateRangePicker component
setResetKey((prev) => prev + 1);
},
// --- END FIX ---
getValues: methods.getValues, // optional, to read current filter state
}));
@ -176,8 +184,7 @@ const ExpenseFilterPanel = forwardRef(
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
isTransactionDate ? "active btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", true)}
>
@ -185,8 +192,7 @@ const ExpenseFilterPanel = forwardRef(
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!isTransactionDate ? "active btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${!isTransactionDate ? "active btn-primary text-white" : ""
}`}
onClick={() => setValue("isTransactionDate", false)}
>

View File

@ -167,8 +167,7 @@ const groupByField = (items, field) => {
label: "Submitted By",
align: "text-start",
getValue: (e) =>
`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
}`.trim() || "N/A",
customRender: (e) => (
<div
@ -182,8 +181,7 @@ const groupByField = (items, field) => {
lastName={e.createdBy?.lastName}
/>
<span className="text-truncate">
{`${e.createdBy?.firstName ?? ""} ${
e.createdBy?.lastName ?? ""
{`${e.createdBy?.firstName ?? ""} ${e.createdBy?.lastName ?? ""
}`.trim() || "N/A"}
</span>
</div>
@ -216,8 +214,7 @@ const groupByField = (items, field) => {
align: "text-center",
getValue: (e) => (
<span
className={`badge bg-label-${
getColorNameFromHex(e?.status?.color) || "secondary"
className={`badge bg-label-${getColorNameFromHex(e?.status?.color) || "secondary"
}`}
>
{e.status?.name || "Unknown"}
@ -343,23 +340,19 @@ const grouped = isNoGrouping
(col.isAlwaysVisible || groupBy !== col.key) && (
<td
key={col.key}
className={`d-table-cell ml-2 ${
col.align ?? ""
className={`d-table-cell ml-2 ${col.align ?? ""
} `}
>
<div
className={`d-flex px-2 ${
col.key === "status"
className={`d-flex px-2 ${col.key === "status"
? "justify-content-center"
: ""
}
${
col.key === "amount"
${col.key === "amount"
? "justify-content-end"
: ""
}
${
col.key === "submitted"
${col.key === "submitted"
? "justify-content-center"
: ""
}
@ -445,7 +438,18 @@ const grouped = isNoGrouping
<tr>
<td colSpan={8} className="text-center border-0 ">
<div className="py-8">
<p>No Expense Found</p>
<p
className="text-center"
style={{
height: "250px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
No Expense found
</p>
</div>
</td>
</tr>

View File

@ -59,6 +59,28 @@ const PreviewDocument = ({ files = [] }) => {
};
};
const handleWheel = (e) => {
if (isDocumentType) return;
e.preventDefault();
const delta = e.deltaY;
setScale((prev) => {
let newScale = prev;
if (delta < 0) {
// Scroll UP -> Zoom IN
newScale = Math.min(prev + 0.1, MAX_ZOOM);
} else {
// Scroll DOWN -> Zoom OUT
newScale = Math.max(prev - 0.1, MIN_ZOOM);
}
return newScale;
});
};
const handleMouseMove = (e) => {
if (!dragging || isDocumentType) return;
@ -105,6 +127,8 @@ const PreviewDocument = ({ files = [] }) => {
</div>
<div
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}

View File

@ -13,37 +13,22 @@ const Sidebar = () => {
id="layout-menu"
className="layout-menu menu-vertical menu bg-menu-theme "
>
<div className="app-brand" style={{ paddingLeft: "30px" }}>
<div className="app-brand" style={{ paddingLeft: "15px" }}>
<Link to="/dashboard" className="app-brand-link">
{/* <span className="app-brand-logo rounded-circle app-brand-logo-border">
<img
className="app-brand-logo-sidebar"
src="/img/brand/marco.png"
alt="logo"
aria-label="logo image"
style={{ margin: "5px", paddingRight: "5px" }}
/>
</span> */}
<a
href="/"
className="app-brand-link d-flex align-items-center gap-1 fw-bold navbar-brand "
>
<span className="app-brand-logo demo d-flex align-items-center">
<img
src="/img/brand/marco.png"
width="40"
width="50"
height="40"
alt="OnFieldWork logo"
/>
</span>
<span className="app-brand-text">
<span className="text-primary ">OnField</span>
<span className="mx-1">Work</span>
<span className="text-blue ms-1">OnField</span>
<span className="text-green">Work</span>
<span className="text-dark">.com</span>
</span>
</a>
</Link>
<small className="layout-menu-toggle menu-link text-large ms-auto cursor-pointer">

View File

@ -33,8 +33,11 @@ const AssignOrg = ({ setStep }) => {
});
const isPending = isPendingProject || isPendingTenat;
const mergedServices = useMemo(() => {
if (!masterService || !projectServices) return [];
const combined = [...masterService?.data, ...projectServices];
const master = Array.isArray(masterService?.data) ? masterService.data : [];
const project = Array.isArray(projectServices) ? projectServices : [];
const combined = [...master, ...project];
return combined.filter(
(item, index, self) => index === self.findIndex((s) => s.id === item.id)
);
@ -100,7 +103,8 @@ const AssignOrg = ({ setStep }) => {
alt="logo"
width={40}
height={40}
/> <p className="fw-semibold fs-5 mt-2 m-0">{orgData.name}</p>
/>{" "}
<p className="fw-semibold fs-5 mt-2 m-0">{orgData.name}</p>
</div>
<div className="text-end">
<button
@ -113,7 +117,10 @@ const AssignOrg = ({ setStep }) => {
</div>
</div>
</div>
<div className="d-flex text-secondary mb-3"> <i className="bx bx-sm bx-info-circle me-2" /> Organization Info</div>
<div className="d-flex text-secondary mb-3">
{" "}
<i className="bx bx-sm bx-info-circle me-2" /> Organization Info
</div>
{/* Contact Info */}
<div className="col-md-12 mb-4">
<div className="d-flex">
@ -132,7 +139,7 @@ const AssignOrg = ({ setStep }) => {
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className='bx bx-sm me-1 bx-phone'></i> Contact Number :
<i className="bx bx-sm me-1 bx-phone"></i> Contact Number :
</label>
<div className="text-muted">{orgData.contactNumber}</div>
</div>
@ -143,7 +150,7 @@ const AssignOrg = ({ setStep }) => {
className="form-label me-2 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className='bx bx-sm me-1 bx-envelope'></i> Email Address :
<i className="bx bx-sm me-1 bx-envelope"></i> Email Address :
</label>
<div className="text-muted text-wrap">{orgData.email}</div>
</div>
@ -166,7 +173,7 @@ const AssignOrg = ({ setStep }) => {
className="form-label me-1 mb-0 fw-semibold"
style={{ minWidth: "130px" }}
>
<i className='bx bx-sm me-1 bx-map'></i> Address :
<i className="bx bx-sm me-1 bx-map"></i> Address :
</label>
<div className="text-muted text-start">{orgData.address}</div>
</div>
@ -180,7 +187,11 @@ const AssignOrg = ({ setStep }) => {
<>
{/* Organization Type */}
<div className="mb-3 text-start">
<Label htmlFor="organizationTypeId" className="mb-3 fw-semibold" required>
<Label
htmlFor="organizationTypeId"
className="mb-3 fw-semibold"
required
>
Organization Type
</Label>
<div className="d-flex flex-wrap gap-3 mt-1">
@ -214,7 +225,11 @@ const AssignOrg = ({ setStep }) => {
{/* Services */}
<div className="mb-3">
<Label htmlFor="serviceIds" className="mb-3 fw-semibold" required>
<Label
htmlFor="serviceIds"
className="mb-3 fw-semibold"
required
>
Select Services
</Label>
{mergedServices?.map((service) => (

View File

@ -1,3 +1,4 @@
import moment from "moment";
import React, { useMemo } from "react";
const PaymentRequestFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => {
@ -22,6 +23,21 @@ const PaymentRequestFilterChips = ({ filters, filterData, removeFilterChip, clea
addGroup(filters.projectIds, data.projects, "Projects", "projectIds");
addGroup(filters.statusIds, data.status, "Status", "statusIds");
if (filters.startDate || filters.endDate) {
const start = filters.startDate ? moment(filters.startDate).format("DD MMM YYYY") : "";
const end = filters.endDate ? moment(filters.endDate).format("DD MMM YYYY") : "";
chips.push({
key: "dateRange",
label: "Date",
items: [
{
id: "dateRange",
name: start && end ? `${start} to ${end}` : start || end,
},
],
});
}
return chips;
}, [filters, filterData]);

View File

@ -76,9 +76,18 @@ const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilte
if (value !== undefined) {
setValue(name, value);
} else {
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
// NOTE: Using defaultPaymentRequestFilter for clear functionality
reset({ ...methods.getValues(), [name]: defaultPaymentRequestFilter[name] });
}
},
// --- START FIX: Add resetDateRange method ---
resetDateRange: () => {
setValue("startDate", null);
setValue("endDate", null);
// Trigger re-render/reset of the DateRangePicker component
setResetKey((prev) => prev + 1);
},
// --- END FIX ---
getValues: methods.getValues, // optional, to read current filter state
}));
@ -94,7 +103,7 @@ const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilte
startDate: moment.utc(formData.startDate, "DD-MM-YYYY").toISOString(),
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
});
handleGroupBy(selectedGroup.id);
// handleGroupBy(selectedGroup.id);
// closePanel();
};

View File

@ -355,8 +355,18 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
) : (
<tr>
<td colSpan={8} className="text-center border-0 ">
<div className="py-8">
<p>No Request Found</p>
<div >
<p
className="text-center"
style={{
height: "250px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
No data found
</p>
</div>
</td>
</tr>

View File

@ -237,13 +237,116 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
))}
</span>
</p>
<div
className="col-12 col-md-4 d-flex flex-row gap-3 align-items-center mt-4 mb-6 justify-content-end"
ref={dropdownRef}
>
<div className="d-flex justify-content-between align-items-center w-100">
{/* Left Side → Heading + Search */}
<div className="d-flex flex-column w-100">
<label className=" text-start small mb-1">Search Employee</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="Search employees or roles..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
{/* Right Side → Dropdown */}
<div className="dropdown position-relative d-inline-block ms-2 mt-4">
<a
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") || selectedRoles.length === 0
? "text-secondary"
: "text-primary"
}`}
onClick={() => setOpen(!open)}
>
<i className="bx bx-slider-alt ms-2"></i>
</a>
{/* Badge */}
{selectedRolesCount > 0 && (
<span
className="position-absolute top-0 start-100 translate-middle badge rounded-circle bg-warning text-white text-tiny"
style={{
fontSize: "0.65rem",
minWidth: "18px",
height: "18px",
padding: "0",
lineHeight: "18px",
textAlign: "center",
zIndex: 10,
}}
>
{selectedRolesCount}
</span>
)}
{/* Dropdown Menu */}
{open && (
<ul
className="dropdown-menu show p-2 text-capitalize"
style={{ maxHeight: "300px", overflowY: "auto" }}
>
{/* All Roles */}
<li key="all">
<div className="form-check dropdown-item py-0">
<input
className="form-check-input"
type="checkbox"
id="checkboxAllRoles"
value="all"
checked={selectedRoles.includes("all")}
onChange={(e) => handleRoleChange(e, e.target.value)}
/>
<label
className="form-check-label ms-2"
htmlFor="checkboxAllRoles"
>
All Roles
</label>
</div>
</li>
{/* Dynamic Roles */}
{jobRolesForDropdown?.map((role) => (
<li key={role.id}>
<div className="form-check dropdown-item py-0">
<input
className="form-check-input"
type="checkbox"
id={`checkboxRole-${role.id}`}
value={role.id}
checked={selectedRoles.includes(String(role.id))}
onChange={(e) => handleRoleChange(e, e.target.value)}
/>
<label
className="form-check-label ms-2"
htmlFor={`checkboxRole-${role.id}`}
>
{role.name}
</label>
</div>
</li>
))}
</ul>
)}
</div>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-label text-start">
<div className="row mb-1">
<div className="col-12">
<div className="row text-start">
<div className="col-12 col-md-8 d-flex flex-row gap-3 align-items-center mt-4">
<div className="col-12 col-md-8 d-flex flex-row gap-3 align-items-center mt-4 d-none">
<div>
<AppFormController
name="organizationId"
@ -296,109 +399,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
)}
</div>
</div>
<div
className="col-12 col-md-4 d-flex flex-row gap-3 align-items-center justify-content-end"
ref={dropdownRef}
>
{/* Dropdown */}
<div className="dropdown position-relative d-inline-block">
<a
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") ||
selectedRoles.length === 0
? "text-secondary"
: "text-primary"
}`}
onClick={() => setOpen(!open)}
>
<i className="bx bx-slider-alt ms-2"></i>
</a>
{/* Badge */}
{selectedRolesCount > 0 && (
<span
className="position-absolute top-0 start-100 translate-middle badge rounded-circle bg-warning text-white text-tiny"
style={{
fontSize: "0.65rem",
minWidth: "18px",
height: "18px",
padding: "0",
lineHeight: "18px",
textAlign: "center",
zIndex: 10,
}}
>
{selectedRolesCount}
</span>
)}
{/* Dropdown Menu */}
{open && (
<ul
className="dropdown-menu show p-2 text-capitalize"
style={{ maxHeight: "300px", overflowY: "auto" }}
>
{/* All Roles */}
<li key="all">
<div className="form-check dropdown-item py-0">
<input
className="form-check-input"
type="checkbox"
id="checkboxAllRoles"
value="all"
checked={selectedRoles.includes("all")}
onChange={(e) =>
handleRoleChange(e, e.target.value)
}
/>
<label
className="form-check-label ms-2"
htmlFor="checkboxAllRoles"
>
All Roles
</label>
</div>
</li>
{/* Dynamic Roles */}
{jobRolesForDropdown?.map((role) => (
<li key={role.id}>
<div className="form-check dropdown-item py-0">
<input
className="form-check-input"
type="checkbox"
id={`checkboxRole-${role.id}`}
value={role.id}
checked={selectedRoles.includes(
String(role.id)
)}
onChange={(e) =>
handleRoleChange(e, e.target.value)
}
/>
<label
className="form-check-label ms-2"
htmlFor={`checkboxRole-${role.id}`}
>
{role.name}
</label>
</div>
</li>
))}
</ul>
)}
</div>
{/* Search Box */}
<div>
<input
type="text"
className="form-control form-control-sm ms-auto mb-2 mt-2"
placeholder="Search employees or roles..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
</div>
</div>
</div>
</div>

View File

@ -8,7 +8,7 @@ import {
useGroups,
useWorkCategoriesMaster,
} from "../../../hooks/masterHook/useMaster";
import { useManageTask, useProjectAssignedOrganizationsName, useProjectAssignedServices } from "../../../hooks/useProjects";
import { useCurrentService, useManageTask, useProjectAssignedOrganizationsName, useProjectAssignedServices } from "../../../hooks/useProjects";
import showToast from "../../../services/toastService";
import Label from "../../common/Label";
import { useSelectedProject } from "../../../slices/apiDataManager";
@ -28,12 +28,16 @@ const taskSchema = z.object({
comment: z.string(),
});
const TaskModel = ({ project, onSubmit, onClose }) => {
const currentService = useCurrentService()
const defaultModel = {
id: null,
buildingID: "",
floorId: "",
workAreaId: "",
serviceId: "",
serviceId: currentService ?? "",
activityGroupId: "",
activityID: "",
workCategoryId: "",
@ -41,8 +45,6 @@ const defaultModel = {
completedWork: 0,
comment: "",
};
const TaskModel = ({ project, onSubmit, onClose }) => {
// const { activities, loading: activityLoading } = useActivitiesMaster();
const { categories, categoryLoading } = useWorkCategoriesMaster();

View File

@ -105,7 +105,7 @@ const ProjectCard = ({ project, isCore = true }) => {
>
{project?.shortName ? project?.shortName : project?.name}
</h5>
<div className="client-info text-body">
<div className="client-info text-body text-start">
<span>{project?.shortName ? project?.name : ""}</span>
</div>
</div>

View File

@ -55,7 +55,7 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
const { data: assignedServices, isLoading: servicesLoading } =
useProjectAssignedServices(projectId);
const { control } = useForm({
const { control, setValue } = useForm({
defaultValues: {
serviceId: selectedService || "",
},
@ -74,6 +74,22 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
};
useEffect(() => {
if (!servicesLoading && assignedServices?.length === 1) {
const serviceId = assignedServices[0].id;
// set form value
setValue("serviceId", serviceId, {
shouldDirty: true,
shouldValidate: true,
});
// sync redux
dispatch(setService(serviceId));
}
}, [assignedServices, servicesLoading, setValue, dispatch]);
return (
<>
{showModalBuilding && (
@ -132,31 +148,27 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
<div className="col-md-4 col-12 dataTables_length text-start py-2 px-2">
<div className="ms-4 mt-n1">
{!servicesLoading && assignedServices?.length > 0 && (
assignedServices.length > 1 ? (
<AppFormController
name="serviceId"
control={control}
render={({ field }) => (
<SelectField
label="Select Service"
options={[{ id: "", name: "All Projects" }, ...(assignedServices ?? [])]}
options={[...(assignedServices ?? [])]}
placeholder="Choose a Service"
required
labelKey="name"
valueKey="id"
value={field.value}
onChange={(val) => {
field.onChange(val);
handleServiceChange(val);
dispatch(setService(val));
}}
isLoading={servicesLoading}
/>
)}
/>
) : (
<h5>{assignedServices[0].name}</h5>
)
)}
</div>
</div>

View File

@ -159,7 +159,8 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</td>
))}
<td
className={`mx-2 ${canManageProject ? "d-sm-table-cell" : "d-none"
className={`mx-2 ${
canManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
<div className="dropdown z-2">
@ -238,7 +239,7 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
<Pagination
currentPage={currentPage}
totalPages={totalPages}
paginate={paginate}
onPageChange={paginate}
/>
</div>
);

View File

@ -170,7 +170,7 @@ const ProjectStatistics = ({ project }) => {
<h5 className="card-action-title mb-0">
{" "}
<i className="fa fa-line-chart rounded-circle text-primary"></i>
<span className="ms-2 fw-bold">Project Statistics</span>
<span className="ms-2 fw-semibold">Project Statistics</span>
</h5>
</div>
<div className="card-body">

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import TeamEmployeeList from "./TeamEmployeeList";
import { useOrganization } from "../../../hooks/useDirectory";
import { useOrganizationsList } from "../../../hooks/useOrganization";
@ -14,9 +14,19 @@ const TeamAssignToProject = ({ closeModal }) => {
const project = useSelectedProject();
const { data, isLoading, isError, error } =
useProjectAssignedOrganizationsName(project);
const { control, watch, formState: { errors } } = useForm({
const { control, watch, setValue, formState: { errors } } = useForm({
defaultValues: { organizationId: "" },
});
useEffect(() => {
if (data?.length === 1) {
setValue("organizationId", data[0].id, {
shouldValidate: true,
shouldDirty: true,
});
}
}, [data, setValue]);
return (
<div className="container">
{/* <p className="fs-5 fs-seminbod ">Assign Employee To Project </p> */}
@ -47,7 +57,7 @@ const TeamAssignToProject = ({ closeModal }) => {
<small className="danger-text">{errors.organizationId.message}</small>
)}
</div>
<div className="col-12 col-md-6 mt-n5">
<div className="col-12 col-md-6 mt-n2">
<div className="d-flex flex-column">
<label htmlFor="search" className="form-label mb-1">
Search Employee

View File

@ -162,15 +162,23 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
<table className="table" style={{ maxHeight: "80px", overflowY: "auto" }}>
<thead className=" position-sticky top-0">
<tr>
<th>Select</th>
<th>Employee</th>
<th>Service</th>
<th>Job Role</th>
<th>Select</th>
</tr>
</thead>
<tbody>
{employees.map((emp, index) => (
<tr key={emp.id}>
<td>
<input
type="checkbox"
className="form-check-input"
checked={emp.isChecked}
onChange={() => handleCheckboxChange(index)}
/>
</td>
<td>
<div className="d-flex align-items-center">
<Avatar firstName={emp.firstName} lastName={emp.lastName} />
@ -179,6 +187,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
</span>
</div>
</td>
<td>
<select
value={emp.serviceId}
@ -221,14 +230,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
<div className="danger-text">{emp.errors.jobRole}</div>
)}
</td>
<td>
<input
type="checkbox"
className="form-check-input"
checked={emp.isChecked}
onChange={() => handleCheckboxChange(index)}
/>
</td>
</tr>
))}
</tbody>

View File

@ -34,7 +34,7 @@ const Teams = () => {
const [selectedEmployee, setSelectedEmployee] = useState(null);
const [deleteEmployee, setDeleteEmplyee] = useState(null);
const [activeEmployee, setActiveEmployee] = useState(false);
const { control, watch } = useForm({
const { control, watch, setValue } = useForm({
defaultValues: {
selectedService: "",
searchTerm: "",
@ -136,6 +136,18 @@ const Teams = () => {
return () => eventBus.off("employee", employeeHandler);
}, [employeeHandler]);
useEffect(() => {
if (!servicesLoading && assignedServices?.length === 1) {
const serviceId = assignedServices[0].id;
setValue("selectedService", serviceId, {
shouldDirty: true,
shouldValidate: true,
});
}
}, [assignedServices, servicesLoading, setValue]);
return (
<>
{AssigTeam && (
@ -162,13 +174,7 @@ const Teams = () => {
<div className="card-body">
<div className="row align-items-center justify-content-between mb-4 g-3">
<div className="col-md-6 col-12 d-flex flex-wrap align-items-center gap-3">
{!servicesLoading && assignedServices && (
<>
{assignedServices.length === 1 && (
<h5 className="mb-2">{assignedServices[0].name}</h5>
)}
{assignedServices.length > 1 && (
{!servicesLoading && assignedServices?.length > 0 && (
<div className="col-12 col-md-6 mb-2 text-start">
<AppFormController
name="selectedService"
@ -176,7 +182,7 @@ const Teams = () => {
render={({ field }) => (
<SelectField
label="Select Service"
options={[{ id: "", name: "All Services" }, ...assignedServices]}
options={[ ...assignedServices]}
placeholder="Choose a Service"
labelKey="name"
valueKey="id"
@ -189,8 +195,7 @@ const Teams = () => {
/>
</div>
)}
</>
)}
<div className="form-check form-switch d-flex align-items-center text-nowrap">

View File

@ -56,7 +56,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
// onSubmitTenant(data);
// onNext();
const tenantPayload = { ...data, onBoardingDate: moment.utc(data.onBoardingDate, "DD-MM-YYYY").toISOString() }
// CreateTenant(tenantPayload);
CreateTenant(tenantPayload);
}
};

View File

@ -0,0 +1,100 @@
import React, { useMemo } from "react";
import { TENANT_STATUS, reference } from "../../utils/constants";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
const TenantFilterChips = ({ filter, filterData, removeFilterChip, clearFilter }) => {
const data = filterData || {};
const filterChips = useMemo(() => {
const chips = [];
const addGroup = (ids, list, label, key, valueKey = "id") => {
if (!ids || ids.length === 0) return;
const items = ids.map((id) => {
const found = list?.find((i) => i[valueKey] === id);
return {
id,
name: found?.name || id,
};
});
chips.push({ key, label, items });
};
// Industries
addGroup(filter?.industryIds, data, "Industries", "industryIds");
// References
addGroup(filter?.references, reference, "References", "references", "val");
// Tenant Status
addGroup(filter?.tenantStatusIds, TENANT_STATUS, "Status", "tenantStatusIds");
// Date Range Chip
if (filter?.startDate || filter?.endDate) {
chips.push({
key: "date",
label: "Date Range",
items: [
{
id: "date-range",
name: `${filter?.startDate ? formatUTCToLocalTime(filter.startDate) : ""}${
filter?.endDate ? " to " + formatUTCToLocalTime(filter.endDate) : ""
}`,
},
],
});
}
return chips;
}, [filter, filterData]);
if (!filterChips.length) return null;
return (
<div className="d-flex flex-wrap align-items-center gap-2 mt-2">
{filterChips.map((chipGroup) => (
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
<span className="fw-semibold me-2">{chipGroup.label}:</span>
{chipGroup.items.map((item) => (
<span
key={item.id}
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
>
<span>{item.name}</span>
{/* Remove Date Range → remove full filter */}
{chipGroup.key === "date" ? (
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={clearFilter}
/>
) : (
<button
type="button"
className="btn-close btn-close-white btn-sm ms-2"
style={{
filter: "invert(1) grayscale(1)",
opacity: 0.7,
fontSize: "0.6rem",
}}
onClick={() => removeFilterChip(chipGroup.key, item.id)}
/>
)}
</span>
))}
</div>
))}
</div>
);
};
export default TenantFilterChips;

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState, useCallback, useEffect } from "react";
import React, { useState, useCallback, useEffect, useImperativeHandle, forwardRef, useMemo } from "react";
import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { defaultFilterValues, filterSchema } from "./TenantSchema";
import Label from "../common/Label";
@ -10,16 +10,44 @@ import { DateRangePicker1 } from "../common/DateRangePicker";
import moment from "moment";
import { useLocation } from "react-router-dom";
const TenantFilterPanel = ({ onApply }) => {
const TenantFilterPanel = forwardRef(({ onApply, setFilterdata, clearFilter }, ref) => {
const [resetKey, setResetKey] = useState(0);
const methods = useForm({
resolver: zodResolver(filterSchema),
defaultValues: defaultFilterValues,
});
const { handleSubmit, reset, setValue } = methods;
const { data: industries, isLoading } = useIndustries();
const dynamicDefaultFilter = useMemo(() => {
return {
...defaultFilterValues,
industryIds: defaultFilterValues.industryIds || [],
tenantStatusIds: defaultFilterValues.tenantStatusIds || [],
references: defaultFilterValues.references || [],
startDate: defaultFilterValues.startDate,
endDate: defaultFilterValues.endDate,
};
}, [defaultFilterValues]);
useImperativeHandle(ref, () => ({
resetFieldValue: (name, value) => {
setValue(name, value);
},
resetFields: () => {
reset(defaultFilterValues);
setResetKey(prev => prev + 1); // reset date picker
}
}));
useEffect(() => {
if (industries && setFilterdata) {
setFilterdata(industries);
}
}, [industries, setFilterdata]);
const { handleSubmit, reset } = methods;
const { data: industries = [], isLoading } = useIndustries();
const handleClosePanel = useCallback(() => {
document.querySelector(".offcanvas.show .btn-close")?.click();
@ -32,9 +60,8 @@ const TenantFilterPanel = ({ onApply }) => {
startDate: moment.utc(formData.startDate, "DD-MM-YYYY").toISOString(),
endDate: moment.utc(formData.endDate, "DD-MM-YYYY").toISOString(),
});
handleClosePanel();
},
[onApply, handleClosePanel]
[onApply]
);
@ -65,6 +92,7 @@ const TenantFilterPanel = ({ onApply }) => {
endField="endDate"
resetSignal={resetKey}
defaultRange={false}
className="w-100"
/>
</div>
<div className="text-strat mb-2">
@ -119,6 +147,6 @@ const TenantFilterPanel = ({ onApply }) => {
</form>
</FormProvider>
);
};
});
export default TenantFilterPanel;

View File

@ -7,12 +7,14 @@ import Pagination from "../common/Pagination";
import { TenantTableSkeleton } from "./TenanatSkeleton";
import { useTenantContext } from "../../pages/Tenant/TenantPage";
import { useNavigate } from "react-router-dom";
import TenantFilterChips from "./TenantFilterChips";
const TenantsList = ({
filters,
searchText,
setIsRefetching,
setRefetchFn,
filterData, removeFilterChip, clearFilter
}) => {
const [currentPage, setCurrentPage] = useState(1);
const navigate = useNavigate();
@ -135,6 +137,16 @@ const TenantsList = ({
);
return (
<>
<div className="main-content">
<div className="col-12 mb-2 mt-2 px-4">
<TenantFilterChips
filter={filters}
filterData={filterData}
removeFilterChip={removeFilterChip}
clearFilter={clearFilter}
/>
</div>
<div className=" mt-3">
<div className=" text-nowrap table-responsive">
<table className="table border-top dataTable text-nowrap">
@ -185,6 +197,7 @@ const TenantsList = ({
)}
</div>
</div>
</div>
</>
);
};

View File

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

View File

@ -93,6 +93,7 @@ export const DateRangePicker1 = ({
resetSignal,
defaultRange = true,
maxDate = null,
pastDays = 6,
...rest
}) => {
const inputRef = useRef(null);
@ -105,7 +106,7 @@ export const DateRangePicker1 = ({
const applyDefaultDates = () => {
const today = new Date();
const past = new Date();
past.setDate(today.getDate() - 6);
past.setDate(today.getDate() - pastDays);
const format = (d) => flatpickr.formatDate(d, "d-m-Y");
const formattedStart = format(past);

View File

@ -2,10 +2,17 @@ import React from "react";
import { useDeliverChallane } from "../../hooks/usePurchase";
import { SpinnerLoader } from "../common/Loader";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { FileView } from "../Expenses/Filelist";
import { FileView } from "../Expenses/Filelist"; // Assuming FileView is the component showing the file icon/name
import { usePurchaseContext } from "../../pages/purchase/PurchasePage";
import { getIconByFileType } from "../../utils/appUtils";
// Assuming you have an Error component imported somewhere else
// import Error from "../common/Error";
const DeliverChallanList = ({ purchaseId, viewDocuments }) => {
const { setDocumentView } = usePurchaseContext();
const { data, isLoading, isError, error } = useDeliverChallane(purchaseId);
if (isLoading) {
return (
<div className="d-flex justify-content-center align-items-center text-center vh-50">
@ -17,10 +24,12 @@ const DeliverChallanList = ({ purchaseId, viewDocuments }) => {
if (isError) {
return (
<div className="py-3">
{/* Assuming Error component is used here */}
<Error error={error} />
</div>
);
}
if (!isLoading && data.length === 0)
return (
<div className="d-flex justify-content-center align-items-center text-center vh-50">
@ -55,8 +64,29 @@ const DeliverChallanList = ({ purchaseId, viewDocuments }) => {
{item.description || "-"}
</p>
{/* Check if attachment exists and open document view on click */}
{item.attachment?.preSignedUrl && (
<FileView file={item.attachment} viewFile={viewDocuments} />
<div
className="d-flex align-items-center cusor-pointer mt-2"
onClick={() => {
setDocumentView({
IsOpen: true,
Images: [item.attachment],
});
}}
>
{/* Replicating the display style used in ViewExpense for single file attachment */}
<i
className={`bx ${getIconByFileType(item.attachment.contentType)}`}
style={{ fontSize: "30px" }}
></i>
<small
className="text-start text-tiny text-truncate w-100 ms-1"
title={item.attachment.fileName}
>
{item.attachment.fileName}
</small>
</div>
)}
</div>
</div>

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,121 @@
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",
width: "12px",
textWrap: "wrap",
color: color,
fontWeight: 400,
offsetY: 7,
formatter: () =>
`${formatFigure(completed, {
notation: "compact",
})} / ${formatFigure(total, { notation: "compact" })}`,
},
style: {
textWrap: "wrap",
},
},
},
},
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,65 @@
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,
legend1 = "Completed",
legend2 = "Pending",
}) => {
return (
<div className="border-top card border-primary py-4 px-2 h-100">
<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 legend1={legend1} legend2={legend2} />
</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,17 @@
const ReportsLegend = ({legend1, legend2}) => {
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>{legend1}</span>
</div>
<div className=" d-flex align-items-center gap-2">
<span className="reports-legend-color reports-legend-blue"></span>
<span>{legend2}</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,176 @@
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";
import { SpinnerLoader } from "../common/Loader";
const ReportDPR = ({ project, date }) => {
const { data, isLoading, isError, error } = useProjectReportByProject(
project,
localToUtc(date)
);
return (
<>
<div className="card py-2 mb-5">
<div className="d-flex text-start px-2 mt-2">
Project Status Reported - Generated at{" "}
{formatUTCToLocalTime(data?.date, true)}
</div>
{/* <!-- Status Cards */}
<div className="row px-5 py-4">
<div className="col-6 col-md-3" style={{ minHeight: "255px" }}>
{" "}
<ReportsDonutCard
title={"TODAY'S ATTENDANCE"}
total={data?.totalEmployees}
value={data?.todaysAttendances}
legend1="Today's Attendance"
legend2="Total Employees"
/>
</div>
<div className="col-6 col-md-3" style={{ minHeight: "255px" }}>
{" "}
<ReportsDonutCard
title={"DAILY TASKS COMPLETED"}
total={data?.totalCompletedTask}
value={data?.totalPlannedWork}
chartColor={"blue"}
footer=""
legend1="Completed Work"
legend2="Planned Work"
/>
</div>
<div className="col-6 col-md-3" style={{ minHeight: "255px" }}>
{" "}
<ReportsDonutCard
title={"PROJECT COMPLETION STATUS"}
total={data?.totalPlannedWork}
value={data?.totalCompletedWork}
chartColor={"green"}
footer=""
legend1="Completed Work"
legend2="Planned Work"
/>
</div>
<div className="col-6 col-md-3" style={{ minHeight: "255px" }}>
<div className="border-top card border-warning py-4 px-2 h-100">
<h5 className="reports-card-title">Attendance Pending Report</h5>
<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>
</div>
<div className="row px-5 pb-5">
<div className="col-4">
<div className="reports-card h-100" style={{ minHeight: "250px" }}>
{/* {/* <!-- Row 1: Header */}
<div>
<h4 className="reports-card-title">Team Strength on Site</h4>
</div>
<table style={{ width: "100%" }}>
<tbody>
{data?.teamOnSite?.filter(
(item) => item?.numberofEmployees > 0
).length > 0 ? (
data?.teamOnSite
?.filter((item) => item?.numberofEmployees > 0)
?.map((member, index) => (
<tr key={index}>
<td style={{ textAlign: "left" }}>
{member?.roleName}
</td>
<td style={{ textAlign: "right" }}>
{member?.numberofEmployees}
</td>
</tr>
))
) : (
<tr>
<td colSpan="2" style={{ textAlign: "center" }}>
No Records Available
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div className="col-8">
{/* {/* <!-- Activities */}
<div className="reports-card h-100" style={{ minHeight: "250px" }}>
<h4 className="reports-card-title">Employee In-Out Report </h4>
<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?.length > 0 ? (
data?.performedAttendance?.map((att, index) => (
<tr key={att.index + att.name}>
<td>{att.name}</td>
<td>{att.roleName}</td>
<td>{formatUTCToLocalTime(att.inTime, true)}</td>
<td>
{att.outTime
? formatUTCToLocalTime(att.outTime, true)
: "--"}
</td>
</tr>
))
) : (
<tr>
<td
colSpan="4"
className="text-center py-4 border-0"
style={{ height: "200px" }}
>
{isLoading ? (
<SpinnerLoader />
) : (
<div className="py-8">No Records Available</div>
)}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</>
);
};
export default ReportDPR;

View File

@ -1,6 +1,7 @@
import { useForm, Controller, FormProvider, useFormContext } from "react-hook-form";
import { useForm, Controller, FormProvider, useFormContext, useWatch } from "react-hook-form";
export const useAppForm = (config) => useForm(config);
export const AppFormProvider = FormProvider;
export const AppFormController = Controller;
export const useAppFormContext = useFormContext;
export const useAppWatch = useWatch;

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import GlobalRepository from "../repositories/GlobalRepository";
import { useQuery } from "@tanstack/react-query";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
const [dashboard_data, setDashboard_Data] = useState([]);
@ -212,3 +212,16 @@ export const useJobsProgression = (projectId) => {
},
});
};
export const useAttendaceProjectWiseOveriew = (date) => {
return useQuery({
queryKey: ["attendaceOverview", date],
queryFn: async () => {
const resp = await GlobalRepository.getAttendanceProjectWiseOverview(
date
);
return resp.data;
},
keepPreviousData: true,
});
};

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

@ -136,6 +136,15 @@ const AttendancePage = () => {
: []),
];
// --- END: Tab Configuration Array
useEffect(() => {
if (!orgLoading && organizations?.length === 1) {
setAppliedFilters((prev) => ({
...prev,
selectedOrganization: organizations[0].id,
}));
}
}, [orgLoading, organizations]);
return (
<>
@ -178,8 +187,7 @@ const AttendancePage = () => {
<ul className="nav nav-tabs" role="tablist">
{tabsData.map((tab) => (
<li
className={`nav-item ${
tab.id === "regularization" && !DoRegularized
className={`nav-item ${tab.id === "regularization" && !DoRegularized
? "d-none"
: ""
}`} // Keep the d-none logic for regularization, although it's filtered above
@ -187,8 +195,7 @@ const AttendancePage = () => {
>
<button
type="button"
className={`nav-link ${
activeTab === tab.id ? "active" : ""
className={`nav-link ${activeTab === tab.id ? "active" : ""
} fs-6`}
onClick={() => handleTabChange(tab.id)}
data-bs-toggle="tab"
@ -245,8 +252,7 @@ const AttendancePage = () => {
{tabsData.map((tab) => (
<div
key={tab.id}
className={`tab-pane fade ${
activeTab === tab.id ? "show active" : ""
className={`tab-pane fade ${activeTab === tab.id ? "show active" : ""
} py-0 ${tab.id === "all" ? "mx-2" : "p-3"}`}
id={`navs-top-${tab.id}`}
role="tabpanel"

View File

@ -15,7 +15,7 @@ const TaskPlanning = () => {
const selectedProject = useSelectedProject();
const selectedService = useCurrentService();
const dispatch = useDispatch();
const { control } = useForm({
const { control,setValue } = useForm({
defaultValues: {
serviceFilter: selectedService ?? ""
},
@ -39,6 +39,20 @@ const TaskPlanning = () => {
return <div className="text-center py-5">Loading...</div>;
}
useEffect(() => {
if (!servicesLoading && data?.length === 1) {
const serviceId = data[0].id;
setValue("serviceFilter", serviceId, {
shouldValidate: true,
shouldDirty: true,
});
dispatch(setService(serviceId));
}
}, [servicesLoading, data, setValue, dispatch]);
return (
<div className="container-fluid">
<Breadcrumb
@ -51,7 +65,7 @@ const TaskPlanning = () => {
<div className="card py-2 page-min-h">
<div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start">
{data?.length === 0 ? (
<p className="badge bg-label-secondary m-0">Service not assigned</p>
<p className="badge bg-label-secondary m-0"></p>
) : (
<AppFormController
name="serviceFilter"
@ -78,6 +92,7 @@ const TaskPlanning = () => {
{/* Planning Component */}
{selectedProject ? (
<InfraPlanning />

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import { useServices } from "../../hooks/masterHook/useMaster";
import TaskReportList from "../../components/DailyProgressRport/TaskReportList";
@ -13,6 +13,7 @@ import { useSelectedProject } from "../../slices/apiDataManager";
import SelectField from "../../components/common/Forms/SelectField";
import { AppFormController } from "../../hooks/appHooks/useAppForm";
import { useForm } from "react-hook-form";
import { TaskReportDefaultValue } from "../../components/DailyProgressRport/TaskRportScheam";
const DailyProgrssContext = createContext();
export const useDailyProgrssContext = () => {
@ -30,8 +31,9 @@ const DailyProgrssReport = () => {
const [service, setService] = useState("");
const [filter, setFilter] = useState('')
const { setOffcanvasContent, setShowTrigger } = useFab();
const updatedRef = useRef();
const { data, isLoading, isError, error } = useProjectAssignedServices(selectedProject);
const [filterData, setFilterdata] = useState(null);
const [modal, setModal] = useState({ type: null, data: null });
const openModal = (type, data = null) => setModal({ type, data });
@ -44,25 +46,61 @@ const DailyProgrssReport = () => {
filter,
};
const { control } = useForm({
const { control,setValue } = useForm({
defaultValues: {
serviceFilter: ""
}
});
const clearFilter = () => {
updatedRef.current?.onClear();
};
const handleFilter = (filterObj) => {
setFilter(filterObj)
}
useEffect(() => {
setShowTrigger(true);
setOffcanvasContent("Report Filter", <TaskReportFilterPanel handleFilter={handleFilter} />);
setOffcanvasContent("Report Filter", <TaskReportFilterPanel handleFilter={handleFilter} ref={updatedRef}
clearFilter={clearFilter}
setFilterdata={setFilterdata} />);
return () => {
setShowTrigger(false);
setOffcanvasContent("", null);
};
}, []);
const handleRemoveChip = (key, id) => {
setFilter((prev) => {
const updated = { ...prev };
if (Array.isArray(updated[key])) {
updated[key] = updated[key].filter((v) => v !== id);
setTimeout(() => updatedRef.current?.resetFieldValue(key, updated[key]), 0);
} else {
updated[key] = null;
setTimeout(() => updatedRef.current?.resetFieldValue(key, null), 0);
}
return updated;
});
};
useEffect(() => {
if (!isLoading && data?.length === 1) {
const serviceId = data[0].id;
// set form value
setValue("serviceFilter", serviceId, {
shouldDirty: true,
shouldValidate: true,
});
// set local state
setService(serviceId);
}
}, [isLoading, data, setValue]);
return (
<div className="container-fluid">
<DailyProgrssContext.Provider value={contextDispatcher}>
@ -97,7 +135,7 @@ const DailyProgrssReport = () => {
]}
/>
<div className="card card-fullscreen p-5">
<div className="card page-min-h p-5">
{data?.length > 0 && (
<div className="col-sm-4 col-md-3 col-12 text-start">
<AppFormController
@ -107,7 +145,7 @@ const DailyProgrssReport = () => {
render={({ field }) => (
<SelectField
label="Services"
options={[{ id: "", name: "All Projects" }, ...(data ?? [])]}
options={[{ id: "", name: "All Services" }, ...(data ?? [])]}
placeholder="Select Service"
labelKey="name"
valueKey="id"
@ -125,7 +163,10 @@ const DailyProgrssReport = () => {
)}
<div>
<TaskReportList />
<TaskReportList filter={filter}
filterData={filterData}
removeFilterChip={handleRemoveChip}
clearFilter={clearFilter} />
</div>
</div>

View File

@ -106,10 +106,13 @@ const ExpensePage = () => {
updated[key] = updated[key].filter((v) => v !== id);
filterPanelRef.current?.resetFieldValue(key, updated[key]);
} else if (key === "dateRange") {
// --- START FIX: Use a dedicated function to reset the date range ---
updated.startDate = null;
updated.endDate = null;
filterPanelRef.current?.resetFieldValue("startDate", null);
filterPanelRef.current?.resetFieldValue("endDate", null);
// Call the new resetDateRange method on the ref
filterPanelRef.current?.resetDateRange();
// --- END FIX ---
}
return updated;
});

View File

@ -86,7 +86,7 @@ const LandingPage = () => {
<li className="nav-item ms-1">
<a
className="btn btn-sm btn-green btn-ovel-small px-3 my-1"
href="#"
href="/auth/reqest/demo"
>
Request For Demo
</a>
@ -162,7 +162,7 @@ const LandingPage = () => {
Make data-driven decisions with real-time project analytics.
</p>
<a
href="#"
href="/auth/reqest/demo"
className="btn btn-green btn-square-small btn-lg mt-3 p-3"
>
View Demo
@ -185,7 +185,7 @@ const LandingPage = () => {
Eliminate Paper Receipts. Take Control of Your Cash Flow.
</p>
<a
href="#"
href="/auth/reqest/demo"
className="btn btn-green btn-square-small btn-lg mt-3 p-3"
>
View Demo
@ -320,65 +320,6 @@ const LandingPage = () => {
</p>
{/* <SubscriptionPlans/> */}
<SubscriptionPlans></SubscriptionPlans>
{/* <div className="row g-4 justify-content-center hidden">
<div className="col-md-4">
<div className="card pricing-card border-0 shadow-sm">
<div className="card-body">
<h5 className="text-green fw-bold">Starter</h5>
<h2>
499<span className="fs-6 text-muted">/month</span>
</h2>
<ul className="list-unstyled mt-3 mb-4 text-muted">
<li>Up to 10 users</li>
<li>Basic reporting</li>
<li>Project tracking</li>
</ul>
<a href="#" className="btn btn-outline-success">
Request Demo
</a>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card pricing-card border-primary shadow-lg">
<div className="card-body">
<h5 className="text-green fw-bold">Professional</h5>
<h2>
999<span className="fs-6 text-muted">/month</span>
</h2>
<ul className="list-unstyled mt-3 mb-4 text-muted">
<li>Unlimited projects</li>
<li>Expense & attendance tracking</li>
<li>Advanced analytics</li>
</ul>
<a
href="https://ofw.marcoaiot.com/auth/reqest/demo"
className="btn btn-green btn-square-small"
>
Request Demo
</a>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card pricing-card border-0 shadow-sm">
<div className="card-body">
<h5 className="text-green fw-bold">Enterprise</h5>
<h2>Custom</h2>
<ul className="list-unstyled mt-3 mb-4 text-muted">
<li>Dedicated support</li>
<li>Custom integrations</li>
<li>Private cloud hosting</li>
</ul>
<a href="#" className="btn btn-outline-success">
Request Demo
</a>
</div>
</div>
</div>
</div> */}
</div>
</section>
{/* <!-- About --> */}

View File

@ -9,7 +9,8 @@ const SubscriptionPlans = () => {
const [frequency, setFrequency] = useState(1);
const { data, isLoading, isError, error } = useSubscription(frequency);
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true);
console.log(data);
const frequencyLabel = (freq) => {
switch (freq) {
case 0:
@ -35,8 +36,7 @@ const SubscriptionPlans = () => {
<button
key={idx}
type="button"
className={`btn btn-${
frequency === idx ? "primary" : "outline-secondary"
className={`btn btn-${frequency === idx ? "primary" : "outline-secondary"
}`}
onClick={() => setFrequency(idx)}
>
@ -64,7 +64,7 @@ const SubscriptionPlans = () => {
<p>{error.name}</p>
</div>
) : (
data.map((plan) => (
data.map((plan, index) => (
<div key={plan.id} className="col-xl-4 col-lg-6 col-md-6">
<div className="card h-100 shadow-lg border-0 p-3 text-center p-10">
{/* Header */}
@ -102,24 +102,69 @@ const SubscriptionPlans = () => {
<h6 className="fw-bold text-uppercase border-top pt-3 mb-3 text-center">
Features
</h6>
<ul className="list-unstyled text-start mb-4 ms-7 fs-6">
<div className="accordion" id={`planFeatures-${plan.id}`}>
{plan.features?.modules &&
Object.values(plan.features.modules).map((mod) =>
mod && mod.name ? (
<li
key={mod.id}
className="d-flex align-items-center mb-2"
Object.entries(plan.features.modules)
.sort(([, a], [, b]) => Number(b.enabled) - Number(a.enabled))
.map(([key, mod]) => {
if (!mod || !mod.name) return null;
const isFirst = index === 0;
return (
<div className="accordion-item mb-2" key={`${plan.id}-${mod.id}`}>
<h2 id={`heading-${plan.id}-${mod.id}`} className="accordion-header">
<button
className="accordion-button py-2 d-flex justify-content-between align-items-center"
type="button"
data-bs-toggle="collapse"
data-bs-target={`#collapse-${plan.id}-${mod.id}`}
aria-expanded={`${isFirst ? "true" : "false"}`}
aria-controls={`collapse-${plan.id}-${mod.id}`}
>
<span className="fw-semibold d-flex align-items-center">
{mod.enabled ? (
<i className="fa-regular fa-circle-check text-success me-2"></i>
) : (
<i className="fa-regular fa-circle-xmark text-danger me-2"></i>
)}
{mod.name}
</span>
<i className="bx bx-chevron-down ms-2"></i>
</button>
</h2>
<div
id={`collapse-${plan.id}-${mod.id}`}
className={`accordion-collapse collapse ${isFirst ? "show" : ""}`}
aria-labelledby={`heading-${plan.id}-${mod.id}`}
>
<div className="accordion-body py-2">
{mod.features?.length > 0 ? (
<ul className="list-unstyled ms-2">
{mod.features.map((feat) => (
<li key={feat.id} className="d-flex align-items-start mb-1">
<i
className={`bx bxs-circle ${mod.enabled ? "text-success" : "text-danger"
} me-2 mt-1`}
style={{ fontSize: "8px" }}
></i>
<span>{feat.name}</span>
</li>
) : null
)}
))}
</ul>
) : (
<p className="text-muted small mb-0">No additional features</p>
)}
</div>
</div>
</div>
);
})}
</div>
{/* Button */}
<div className="mt-auto">
@ -138,6 +183,7 @@ const SubscriptionPlans = () => {
</div>
</div>
</div>
))
)}
</div>

View File

@ -71,6 +71,11 @@ const PaymentRequestPage = () => {
if (Array.isArray(updated[key])) {
updated[key] = updated[key].filter((v) => v !== id);
setTimeout(() => updatedRef.current?.resetFieldValue(key, updated[key]), 0);
} else if (key === "dateRange") { // Handle date range reset
updated.startDate = null;
updated.endDate = null;
// Call the new resetDateRange method on the ref
updatedRef.current?.resetDateRange();
} else {
updated[key] = null;
setTimeout(() => updatedRef.current?.resetFieldValue(key, null), 0);

View File

@ -20,7 +20,7 @@ const ServiceProjectDisplay = ({ listView, selectedStatuses, searchTerm }) => {
const { manageServiceProject, setManageServiceProject } = useProjectContext();
const debouncedSearch = useDebounce(searchTerm, 500);
const { data, isLoading, isError, error } = useServiceProjects(
ITEMS_PER_PAGE,
9,
currentPage,
debouncedSearch
);

View File

@ -5,6 +5,7 @@ import React, {
useContext,
useCallback,
useMemo,
useRef,
} from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@ -59,6 +60,7 @@ const TenantPage = () => {
const [refetchFn, setRefetchFn] = useState(null);
const [filters, setFilters] = useState();
// ---------- Hooks ----------
const debouncedSearch = useDebounce(searchText, 500);
const { setOffcanvasContent, setShowTrigger } = useFab();
@ -66,6 +68,9 @@ const TenantPage = () => {
const isSuperTenant = useHasUserPermission(SUPPER_TENANT);
const canManageTenants = useHasUserPermission(MANAGE_TENANTS);
const isSelfTenant = useHasUserPermission(VIEW_TENANTS);
const updatedRef = useRef();
const [filterData, setFilterdata] = useState(null);
const [filter, setFilter] = useState('')
const methods = useForm({
resolver: zodResolver(filterSchema),
@ -77,8 +82,18 @@ const TenantPage = () => {
setFilters(values);
}, []);
const clearFilter = () => {
setFilters(defaultFilterValues); // update active filters
updatedRef.current?.resetFields?.(); // reset RHF fields in panel
};
const filterPanelElement = useMemo(
() => <TenantFilterPanel onApply={handleApplyFilters} />,
() => <TenantFilterPanel
onApply={handleApplyFilters}
ref={updatedRef}
clearFilter={clearFilter}
setFilterdata={setFilterdata}
/>,
[handleApplyFilters]
);
// ---------- Fab Filter Panel ----------
@ -112,6 +127,20 @@ const TenantPage = () => {
// ---------- Context Value ----------
const contextValue = {};
const handleRemoveChip = (key, id) => {
setFilters((prev) => {
const updated = { ...prev };
if (Array.isArray(updated[key])) {
updated[key] = updated[key].filter((v) => v !== id);
} else {
updated[key] = null;
}
setTimeout(() => updatedRef.current?.resetFieldValue(key, updated[key]), 0);
return updated;
});
};
return (
<TenantContext.Provider value={contextValue}>
<div className="container-fluid">
@ -121,7 +150,7 @@ const TenantPage = () => {
{ label: "Tenant", link: null },
]}
/>
<div className="card text-center my-4 p-md-5 px-1 py-3 pb-10">
<div className="card page-min-h text-center my-4 p-md-5 px-1 py-3 pb-10">
{/* Super Tenant Actions */}
{isSuperTenant && (
<div className="p-0">
@ -157,10 +186,13 @@ const TenantPage = () => {
{/* Tenant List or Access Denied */}
{isSuperTenant ? (
<TenantsList
setRefetchFn={setRefetchFn}
setIsRefetching={setIsRefetching}
filters={filters}
searchText={debouncedSearch}
setIsRefetching={setIsRefetching}
setRefetchFn={setRefetchFn}
filterData={filterData}
removeFilterChip={handleRemoveChip}
clearFilter={clearFilter}
/>
) : !isSelfTenant ? (
<div className="text-center my-4 p-2">

View File

@ -1,5 +1,5 @@
import React from 'react'
import LoginWithOtp from './LoginWithOtp'
import React from "react";
import LoginWithOtp from "./LoginWithOtp";
const MainLoginWithOTPPage = () => {
return (
@ -8,7 +8,7 @@ const MainLoginWithOTPPage = () => {
<div className="d-none d-lg-flex col-lg-7 col-xl-8 align-items-center p-5">
<div className="w-100 d-flex justify-content-center">
<img
src="/img/illustrations/worker_03.png"
src="/img/illustrations/registration.jpg"
className="img-fluid"
alt="Login image"
width="70%"
@ -20,7 +20,7 @@ const MainLoginWithOTPPage = () => {
<LoginWithOtp />
</div>
</>
)
}
);
};
export default MainLoginWithOTPPage
export default MainLoginWithOTPPage;

View File

@ -1,5 +1,5 @@
import React from 'react'
import ResetPasswordPage from './ResetPassword'
import React from "react";
import ResetPasswordPage from "./ResetPassword";
const MainResetPasswordPage = () => {
return (
@ -8,7 +8,7 @@ const MainResetPasswordPage = () => {
<div className="d-none d-lg-flex col-lg-7 col-xl-8 align-items-center p-5">
<div className="w-100 d-flex justify-content-center">
<img
src="/img/illustrations/worker_03.png"
src="/img/illustrations/registration.jpg"
className="img-fluid"
alt="Login image"
width="70%"
@ -20,7 +20,7 @@ const MainResetPasswordPage = () => {
<ResetPasswordPage />
</div>
</>
)
}
);
};
export default MainResetPasswordPage
export default MainResetPasswordPage;

View File

@ -41,6 +41,7 @@ import Pagination from "../../components/common/Pagination";
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
import { SpinnerLoader } from "../../components/common/Loader";
import ManageReporting from "../../components/Employee/ManageReporting";
import { capitalizeFirstLetter } from "../../utils/appUtils";
const EmployeeList = () => {
const selectedProjectId = useSelector(
@ -73,9 +74,11 @@ const EmployeeList = () => {
const [selectedEmployee, setSelectedEmployee] = useState(null);
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedEmpFordelete, setSelectedEmpFordelete] = useState(null);
const [showManageReportingModal, setShowManageReportingModal] = useState(false);
const [showManageReportingModal, setShowManageReportingModal] =
useState(false);
const [employeeLodaing, setemployeeLodaing] = useState(false);
const ViewTeamMember = useHasUserPermission(VIEW_TEAM_MEMBERS);
const ViewAllEmployee = useHasUserPermission(VIEW_ALL_EMPLOYEES);
const { mutate: suspendEmployee, isPending: empLodaing } = useSuspendEmployee(
{
setIsDeleteModalOpen,
@ -139,11 +142,15 @@ const EmployeeList = () => {
const tableRef = useRef(null);
const handleExport = (type) => {
handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef);
handleEmployeeExport(
type,
employeeList,
filteredData,
searchText,
tableRef
);
};
const handleAllEmployeesToggle = (e) => {
const isChecked = e.target.checked;
setShowInactive(false);
@ -172,9 +179,11 @@ const EmployeeList = () => {
useEffect(() => {
if (!loading && Array.isArray(employees)) {
const sorted = [...employees].sort((a, b) => {
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
const nameA = `${a.firstName || ""}${a.middleName || ""}${
a.lastName || ""
}`.toLowerCase();
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
const nameB = `${b.firstName || ""}${b.middleName || ""}${
b.lastName || ""
}`.toLowerCase();
return nameA?.localeCompare(nameB);
});
@ -265,7 +274,8 @@ const EmployeeList = () => {
? "Suspend Employee"
: "Reactivate Employee"
}
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
message={`Are you sure you want to ${
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
} this employee?`}
onSubmit={(id) =>
suspendEmployee({
@ -287,7 +297,7 @@ const EmployeeList = () => {
]}
></Breadcrumb>
{ViewTeamMember ? (
{ViewTeamMember || ViewAllEmployee ? (
// <div className="row">
<div className="card p-1 page-min-h">
<div className="card-datatable table-responsive pt-5 mx-5 py-10">
@ -341,8 +351,10 @@ const EmployeeList = () => {
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end shadow-sm " style={{ minWidth: "220px" }}>
<ul
className="dropdown-menu dropdown-menu-end shadow-sm "
style={{ minWidth: "220px" }}
>
<li className="dropdown-item d-flex align-items-center justify-content-between">
<div className="form-check form-switch mb-0">
<input
@ -351,39 +363,53 @@ const EmployeeList = () => {
role="switch"
id="inactiveEmployeesCheckboxMenu"
checked={showInactive}
onChange={(e) => setShowInactive(e.target.checked)}
onChange={(e) =>
setShowInactive(e.target.checked)
}
/>
</div>
<span className="ms-0">Show Inactive Employees</span>
</li>
<li><hr className="dropdown-divider" /></li>
<li>
<hr className="dropdown-divider" />
</li>
<li>
<button className="dropdown-item" onClick={() => handleExport("print")}>
<button
className="dropdown-item"
onClick={() => handleExport("print")}
>
<i className="bx bx-printer me-2"></i> Print
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => handleExport("csv")}>
<button
className="dropdown-item"
onClick={() => handleExport("csv")}
>
<i className="bx bx-file me-2"></i> CSV
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => handleExport("excel")}>
<button
className="dropdown-item"
onClick={() => handleExport("excel")}
>
<i className="bx bxs-file-export me-2"></i> Excel
</button>
</li>
<li>
<button className="dropdown-item" onClick={() => handleExport("pdf")}>
<button
className="dropdown-item"
onClick={() => handleExport("pdf")}
>
<i className="bx bxs-file-pdf me-2"></i> PDF
</button>
</li>
</ul>
</div>
</div>
</div>
<table
@ -460,7 +486,8 @@ const EmployeeList = () => {
Status
</th>
<th
className={`sorting_disabled ${!Manage_Employee && "d-none"
className={`sorting_disabled ${
!Manage_Employee && "d-none"
}`}
rowSpan="1"
colSpan="1"
@ -475,29 +502,25 @@ const EmployeeList = () => {
{loading && (
<tr>
<td colSpan={8} className="text-center py-5">
<div className="d-flex justify-content-center align-items-center py-5" style={{ minHeight: "50vh" }}>
<div
className="d-flex justify-content-center align-items-center py-5"
style={{ minHeight: "50vh" }}
>
<SpinnerLoader />
</div>
</td>
</tr>
)}
{!loading &&
displayData?.length === 0 &&
(!searchText) ? (
{!loading && displayData?.length === 0 && !searchText ? (
<tr>
<td colSpan={8} className="border-0 py-3">
<div className="py-4">
No Data Found
</div>
<div className="py-4">No Data Found</div>
</td>
</tr>
) : null}
{!loading &&
displayData?.length === 0 &&
(searchText) ? (
{!loading && displayData?.length === 0 && searchText ? (
<tr>
<td colSpan={8} className="border-0 py-3">
<div className="py-4">
@ -526,8 +549,9 @@ const EmployeeList = () => {
className="text-heading text-truncate cursor-pointer"
>
<span className="fw-normal">
{item.firstName} {item.middleName}{" "}
{item.lastName}
{capitalizeFirstLetter(item.firstName)}{" "}
{capitalizeFirstLetter(item.middleName)}{" "}
{capitalizeFirstLetter(item.lastName)}
</span>
</a>
</div>
@ -540,7 +564,9 @@ const EmployeeList = () => {
{item.email}
</span>
) : (
<span className="d-block text-start text-muted fst-italic">NA</span>
<small className="d-block text-start text-muted small">
-
</small>
)}
</td>
@ -561,7 +587,9 @@ const EmployeeList = () => {
{item.joiningDate ? (
moment(item.joiningDate).format("DD-MMM-YYYY")
) : (
<span className="d-block text-center text-muted fst-italic">NA</span>
<span className="d-block text-center text-muted fst-italic">
NA
</span>
)}
</td>

View File

@ -1,4 +1,10 @@
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import {
ITEMS_PER_PAGE,
@ -68,7 +74,7 @@ const ProjectPage = () => {
const handleToggleProject = (value) => {
setCoreProjects(value);
sessionStorage.setItem("whichProjectDisplay", String(value));
sessionStorage.setItem("whichProjectDisplay", value ? "true" : "false");
};
useEffect(() => {
@ -100,7 +106,8 @@ const ProjectPage = () => {
{/* Service Project Button */}
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${!coreProjects ? "btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${
!coreProjects ? "btn-primary text-white" : ""
}`}
onClick={() => handleToggleProject(false)}
>
@ -109,7 +116,8 @@ const ProjectPage = () => {
{/* Organization Project Button */}
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${coreProjects ? "btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${
coreProjects ? "btn-primary text-white" : ""
}`}
onClick={() => handleToggleProject(true)}
>
@ -138,7 +146,8 @@ const ProjectPage = () => {
<div className="d-flex gap-2">
<button
type="button"
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"
className={`btn btn-sm p-1 ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
title="Card View"
@ -148,7 +157,8 @@ const ProjectPage = () => {
<button
type="button"
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"
className={`btn btn-sm p-1 ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
title="List View"
@ -164,12 +174,16 @@ const ProjectPage = () => {
onClick={() => setOpen(!open)}
>
<i
className={`bx bx-slider-alt fs-5 ${selectedStatuses.length !== PROJECT_STATUS.length ? "text-primary" : ""
className={`bx bx-slider-alt fs-5 ${
selectedStatuses.length !== PROJECT_STATUS.length
? "text-primary"
: ""
}`}
></i>
{selectedStatuses.length !== PROJECT_STATUS.length && (
<span className="badge bg-warning text-white rounded-pill position-absolute"
<span
className="badge bg-warning text-white rounded-pill position-absolute"
style={{
top: "-4px",
right: "-4px",
@ -211,10 +225,7 @@ const ProjectPage = () => {
)}
</div>
{HasManageProject && (
{(HasManageProject || !coreProjects) && (
<button
type="button"
className="btn btn-primary btn-sm d-flex align-items-center my-2"

View File

@ -29,25 +29,11 @@ const ProjectsDisplay = ({
const [projectList, setProjectList] = useState([]);
const debouncedSearch = useDebounce(searchTerm, 500);
const { data, isLoading, isError, error } = useProjects(
ITEMS_PER_PAGE,
9,
currentPage,
debouncedSearch
);
const filteredProjects =
data?.data?.filter((project) => {
const statusId =
project.projectStatusId ?? project?.status?.id ?? project?.statusId;
const matchesStatus = selectedStatuses.includes(statusId);
const matchesSearch = project?.name
?.toLowerCase()
?.includes(searchTerm?.toLowerCase());
return matchesStatus && matchesSearch;
}) ?? [];
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
@ -68,7 +54,7 @@ const ProjectsDisplay = ({
.filter((statusId) => grouped[statusId])
.flatMap((statusId) =>
grouped[statusId].sort((a, b) =>
a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase())
a?.shortName?.toLowerCase()?.localeCompare(b?.shortName?.toLowerCase())
)
);

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({
@ -18,12 +22,13 @@ const GlobalRepository = {
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
},
getProjectCompletionStatus:()=>api.get(`/api/Dashboard/project-completion-status`),
getProjectCompletionStatus: () =>
api.get(`/api/Dashboard/project-completion-status`),
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`);
@ -44,7 +49,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}`);
@ -62,10 +67,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}`);
@ -82,13 +92,26 @@ 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`),
getCollectionOverview: (projectId) =>
api.get(`/api/Dashboard/collection-overview`),
getJobsProgression: (projectId) => api.get(`/api/Dashboard/job/progression${projectId ? `?projectId=${projectId}` : ""}`),
getJobsProgression: (projectId) =>
api.get(
`/api/Dashboard/job/progression${
projectId ? `?projectId=${projectId}` : ""
}`
),
getAttendanceProjectWiseOverview: (date) =>
api.get(`/api/dashboard/project/attendance-overview?date=${date}`),
};
export default GlobalRepository;
export const ReportsRepository = {
getDailyProgressByProject: (projectId,date) =>
api.get(`/api/Market/get/project/report/${projectId}?date=${date}`),
};

View File

@ -0,0 +1,64 @@
import CryptoJS from 'crypto-js';
// The key from your C# Middleware
// In a real app, prefer storing this in process.env.REACT_APP_ENCRYPTION_KEY
const KEY_BASE64 = "u4J7p9Qx2hF5vYtLz8Kq3mN1sG0bRwXyZcD6eH8jFQw=";
/**
* Decrypts the specific format sent by the C# EncryptionMiddleware.
* Format: Base64([IV (16 bytes)] + [Encrypted Data])
* * @param {string} encryptedBase64Str - The raw response text from the API
* @returns {any} - The parsed JSON object or string
*/
export const decryptResponse = (encryptedBase64Str) => {
try {
// 1. Parse the Key
const key = CryptoJS.enc.Base64.parse(KEY_BASE64);
// 2. Parse the incoming Base64 string to a WordArray
const fullWordArray = CryptoJS.enc.Base64.parse(encryptedBase64Str);
// 3. Convert to Hex to easily slice the IV (16 bytes = 32 hex chars)
// This is safer than manipulating WordArray indices directly
const fullHex = CryptoJS.enc.Hex.stringify(fullWordArray);
// 4. Extract IV (First 16 bytes / 32 hex characters)
const ivHex = fullHex.substring(0, 32);
const iv = CryptoJS.enc.Hex.parse(ivHex);
// 5. Extract Ciphertext (The rest of the string)
const cipherTextHex = fullHex.substring(32);
const cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: CryptoJS.enc.Hex.parse(cipherTextHex)
});
// 6. Decrypt
const decrypted = CryptoJS.AES.decrypt(
cipherParams,
key,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
// 7. Convert to UTF-8 String
const decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedString) {
throw new Error("Decryption produced empty result (Wrong Key/IV?)");
}
// 8. Try to parse JSON, otherwise return plain string
try {
return JSON.parse(decryptedString);
} catch {
return decryptedString;
}
} catch (error) {
console.error("Decryption Failed:", error);
return null;
}
};

View File

@ -24,6 +24,11 @@ export const AppColorconfig = {
borderColor: "#eceef1",
},
};
export const AppColors = [
"primary","secodary","warning","info","danger"
]
export const getColorNameFromHex = (hex) => {
const normalizedHex = hex?.replace(/'/g, "").toLowerCase();
const colors = AppColorconfig.colors;
@ -262,3 +267,7 @@ export function calculateTDSPercentage(baseAmount = 0, taxAmount = 0, tdsPercent
}
export function capitalizeFirstLetter(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@ -4,6 +4,7 @@ import axiosRetry from "axios-retry";
import showToast from "../services/toastService";
import { startSignalR, stopSignalR } from "../services/signalRService";
import { BASE_URL } from "./constants";
import { decryptResponse } from "../services/encryption";
const base_Url = BASE_URL;
export const axiosClient = axios.create({
@ -134,7 +135,7 @@ const apiRequest = async (method, url, data = {}, config = {}) => {
params: method === "get" ? data : undefined,
...config,
});
return response.data;
return decryptResponse(response.data);
} catch (error) {
throw error;
}
@ -183,7 +184,6 @@ export const api = {
headers: { ...customHeaders },
authRequired: true,
}),
};
// Redirect helper