Compare commits
59 Commits
Adding_Chi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bc114b6ae0 | |||
| 92a09bfcf0 | |||
| 4f9fa1b7c2 | |||
| 34d5ce9ef9 | |||
| 476fb49e07 | |||
| 7143af1e1e | |||
| a477090cb8 | |||
| f1a5f72db7 | |||
| e8886577d8 | |||
| e10a6ff14c | |||
| a309d13247 | |||
| fa694d8361 | |||
| a9bbd75d6c | |||
| 3ff80ee032 | |||
| e695807e77 | |||
| 630c11985d | |||
| d68cb9e664 | |||
| caeece0660 | |||
| 44f3d8783d | |||
| 3a8c1745f4 | |||
| deba5dfa01 | |||
| bb743d2bb0 | |||
| 6f9eeadc22 | |||
| 6099dd2ea5 | |||
| 7b1b360c78 | |||
| cdaf642eba | |||
| b31195c5a1 | |||
| 9daeffd90e | |||
| ab34ea63fa | |||
| 17ef307ff2 | |||
| ae9ef7938d | |||
| e755faecdc | |||
| 23660379c9 | |||
| 2bf5e9a13f | |||
| 3413806670 | |||
| 8033fdb7e7 | |||
| b62fc82a9c | |||
| 4253ed71eb | |||
| 01fc302011 | |||
| d56aefde02 | |||
| c71fe3a45e | |||
| 0f43c877c4 | |||
| 153ffcdc3e | |||
| e31e4cfc31 | |||
| 8216bf1f2d | |||
| 6b9d7c56bc | |||
| 07ba95e533 | |||
| c1ae9ee55e | |||
| 117d82769a | |||
| 35f1aa8c13 | |||
| cd1ae64753 | |||
| 363a9c5feb | |||
| 69148331f5 | |||
| 65158b9368 | |||
| 96bcdffdca | |||
| b348117f05 | |||
| 1157643916 | |||
| 37212e489e | |||
| b2c0388412 |
7
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"dotenv-webpack": "^8.1.0",
|
||||||
@ -2414,6 +2415,12 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"dotenv-webpack": "^8.1.0",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 500 KiB After Width: | Height: | Size: 52 KiB |
BIN
public/img/hero/bg-011.jpg
Normal file
|
After Width: | Height: | Size: 500 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
@ -31,7 +31,7 @@ const selectedProjectId = useSelectedProject()
|
|||||||
<div className="card-header mb-1 pb-0">
|
<div className="card-header mb-1 pb-0">
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
||||||
<div className="card-title mb-0 text-start">
|
<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>
|
<p className="card-subtitle">Daily Attendance Data</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,7 @@ const AttendanceOverview = () => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="d-flex mt-2 justify-content-between align-items-center mb-3">
|
<div className="d-flex mt-2 justify-content-between align-items-center mb-3">
|
||||||
<div className="card-title mb-0 text-start">
|
<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>
|
<p className="card-subtitle">Role-wise present count</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
|
|||||||
@ -78,7 +78,7 @@ const CollectionOverview = ({ data, isLoading }) => {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
custom: ({ series, seriesIndex, dataPointIndex }) => {
|
custom: ({ series, seriesIndex, dataPointIndex }) => {
|
||||||
return `
|
return `
|
||||||
<div class="px-2 py-1">
|
<div className="px-2 py-1">
|
||||||
<strong>${labels[dataPointIndex]}</strong><br>
|
<strong>${labels[dataPointIndex]}</strong><br>
|
||||||
₹${series[seriesIndex][dataPointIndex].toLocaleString()}
|
₹${series[seriesIndex][dataPointIndex].toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
@ -235,7 +235,7 @@ export const TopicBarChart = ({ data,isLoading }) => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
custom: ({ series, seriesIndex, dataPointIndex }) => {
|
custom: ({ series, seriesIndex, dataPointIndex }) => {
|
||||||
return `
|
return `
|
||||||
<div class="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<span>₹${series[seriesIndex][
|
<span>₹${series[seriesIndex][
|
||||||
dataPointIndex
|
dataPointIndex
|
||||||
].toLocaleString()}</span>
|
].toLocaleString()}</span>
|
||||||
@ -256,8 +256,8 @@ export const TopicBarChart = ({ data,isLoading }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="row p-2">
|
<div className="row p-2">
|
||||||
<div className="col-md-8">
|
<div className="col-md-8">
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
<div className="card-header d-flex align-items-center justify-content-between">
|
||||||
<h5 class="card-title m-0 me-2">Collection Overview</h5>
|
<h5 className="card-title m-0 me-2">Collection Overview</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 d-flex align-items-center text-start px-6">
|
<div className="w-100 d-flex align-items-center text-start px-6">
|
||||||
<p className="text-secondary fs-6 m-0">Due Amount</p>
|
<p className="text-secondary fs-6 m-0">Due Amount</p>
|
||||||
@ -274,7 +274,7 @@ export const TopicBarChart = ({ data,isLoading }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-4 d-flex flex-column gap-2">
|
<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">
|
<div className="p-1 m-1 text-start">
|
||||||
<small className="fw-medium">Overdue Days</small>
|
<small className="fw-medium">Overdue Days</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import {
|
|||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
import CollectionOverview, { TopicBarChart } from "./CollectionOverview";
|
import CollectionOverview, { TopicBarChart } from "./CollectionOverview";
|
||||||
import { CollectionOverviewSkeleton } from "./CollectionOverviewSkeleton";
|
import { CollectionOverviewSkeleton } from "./CollectionOverviewSkeleton";
|
||||||
|
import ProjectWiseTeamCount from "./ProjectWiseTeamCount";
|
||||||
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
// Get the selected project ID from Redux store
|
// Get the selected project ID from Redux store
|
||||||
@ -41,24 +43,24 @@ const Dashboard = () => {
|
|||||||
<div className="container-fluid mt-5">
|
<div className="container-fluid mt-5">
|
||||||
<div className="row gy-4">
|
<div className="row gy-4">
|
||||||
{isAllProjectsSelected && (
|
{isAllProjectsSelected && (
|
||||||
<div className="col-sm-6 col-lg-4">
|
<div className="col-sm-6 col-lg-6">
|
||||||
<Projects />
|
<Projects />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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 />
|
<Teams />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{!isAllProjectsSelected && ( <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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<TasksCard />
|
<TasksCard />
|
||||||
</div>
|
</div>)}
|
||||||
<div className="col-12 col-xl-4 col-md-6">
|
<div className="col-12 col-xl-4 col-md-6">
|
||||||
<div className="card ">
|
<div className="card ">
|
||||||
<ExpenseStatus />
|
<ExpenseStatus />
|
||||||
@ -84,10 +86,13 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
{!isAllProjectsSelected &&
|
{!isAllProjectsSelected &&
|
||||||
(canRegularize || canTeamAttendance || canSelfAttendance) && (
|
(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 />
|
<AttendanceOverview />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isAllProjectsSelected && <div className="col-12 col-md-6 mb-sm-0 mb-4">
|
||||||
|
<ProjectWiseTeamCount />
|
||||||
|
</div>}
|
||||||
|
|
||||||
{!isAllProjectsSelected && (
|
{!isAllProjectsSelected && (
|
||||||
<div className="col-xxl-4 col-lg-4">
|
<div className="col-xxl-4 col-lg-4">
|
||||||
@ -97,7 +102,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isAllProjectsSelected && (
|
{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 />
|
<ServiceJobs />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -118,80 +123,3 @@ const Dashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default 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>
|
|
||||||
|
|||||||
@ -52,6 +52,18 @@ const ExpenseAnalysis = () => {
|
|||||||
legend: { show: false },
|
legend: { show: false },
|
||||||
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
|
dataLabels: { enabled: true, formatter: (val) => `${val.toFixed(0)}%` },
|
||||||
colors: flatColors,
|
colors: flatColors,
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: function (value) {
|
||||||
|
return formatCurrency(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
formatter: function (label) {
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
pie: {
|
pie: {
|
||||||
donut: {
|
donut: {
|
||||||
@ -88,7 +100,7 @@ const ExpenseAnalysis = () => {
|
|||||||
|
|
||||||
<div className="text-end text-sm-end">
|
<div className="text-end text-sm-end">
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<DateRangePicker1 />
|
<DateRangePicker1 pastDays="30" />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -140,7 +152,9 @@ const ExpenseAnalysis = () => {
|
|||||||
className="col-6"
|
className="col-6"
|
||||||
key={idx}
|
key={idx}
|
||||||
style={{
|
style={{
|
||||||
borderLeft: `3px solid ${flatColors[idx % flatColors.length]}`,
|
borderLeft: `3px solid ${
|
||||||
|
flatColors[idx % flatColors.length]
|
||||||
|
}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="d-flex flex-column text-start">
|
<div className="d-flex flex-column text-start">
|
||||||
@ -165,7 +179,6 @@ const ExpenseAnalysis = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -92,45 +92,40 @@ const ExpenseByProject = () => {
|
|||||||
<div className="card shadow-sm h-100 rounded ">
|
<div className="card shadow-sm h-100 rounded ">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="card-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">
|
<div className="text-start">
|
||||||
<h5 className="mb-1 me-6 card-title">Monthly Expense -</h5>
|
<h5 className="mb-1 me-6 card-title">Monthly Expense </h5>
|
||||||
<p className="card-subtitle m-0">{projectName}</p>
|
<div className="row w-100">
|
||||||
</div>
|
<div className="col-6"> <p className="card-subtitle m-0">{projectName}</p></div>
|
||||||
<div className="btn-group mb-5 ms-n8">
|
<div className="col-6 d-flex justify-content-between align-items-center px-0">
|
||||||
<button
|
|
||||||
className="btn btn-sm dropdown-toggle fs-5"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
<select
|
||||||
aria-expanded="false"
|
className="form-select form-select-sm ms-auto mt-sm-0"
|
||||||
|
value={selectedType}
|
||||||
|
onChange={(e) => setSelectedType(e.target.value)}
|
||||||
|
disabled={typeLoading}
|
||||||
|
style={{ maxWidth: "200px" }}
|
||||||
>
|
>
|
||||||
{viewMode}
|
<option value="">All Categories</option>
|
||||||
</button>
|
{expenseCategories?.map((type) => (
|
||||||
<ul className="dropdown-menu dropdown-menu-end ">
|
<option key={type.id} value={type.id}>
|
||||||
<li>
|
{type.name}
|
||||||
<button
|
</option>
|
||||||
className="dropdown-item"
|
))}
|
||||||
onClick={() => {
|
</select>
|
||||||
setViewMode("Category");
|
|
||||||
setSelectedType("");
|
</div>
|
||||||
}}
|
|
||||||
>
|
|
||||||
Category
|
|
||||||
</button>
|
|
||||||
</li>
|
</div>
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={() => {
|
|
||||||
setViewMode("Project");
|
|
||||||
setSelectedType("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Project
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Range Buttons + Expense Dropdown */}
|
{/* Range Buttons + Expense Dropdown */}
|
||||||
@ -148,22 +143,7 @@ const ExpenseByProject = () => {
|
|||||||
{item}
|
{item}
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -92,7 +92,7 @@ const ProjectProgressChart = ({
|
|||||||
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
|
<div className="d-flex flex-wrap justify-content-between align-items-start mb-2">
|
||||||
{/* Left: Title */}
|
{/* Left: Title */}
|
||||||
<div className="card-title text-start">
|
<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>
|
<p className="card-subtitle">Progress Overview by Project</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
112
src/components/Dashboard/ProjectWiseTeamCount.jsx
Normal 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;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useJobsProgression } from "../../hooks/useDashboard_Data";
|
import { useJobsProgression } from "../../hooks/useDashboard_Data";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
@ -7,56 +7,90 @@ import { useServiceProject } from "../../hooks/useServiceProject";
|
|||||||
|
|
||||||
const ServiceJobs = () => {
|
const ServiceJobs = () => {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { data, isLoading, isError } = useJobsProgression(projectId);
|
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useJobsProgression(projectId);
|
||||||
const jobs = data || {};
|
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 = [
|
const tabMapping = [
|
||||||
{ id: "tab-new", label: "My Jobs", key: "allJobs" },
|
{ id: "tab-new", label: "My Jobs", key: "myJobs" },
|
||||||
{ id: "tab-preparing", label: "Assigned", key: "assignedJobs" },
|
|
||||||
{ id: "tab-shipping", label: "In Progress", key: "inProgressJobs" },
|
{ 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 (
|
return (
|
||||||
<div className="">
|
<div>
|
||||||
<div className="card page-min-h">
|
<div className="card page-min-h">
|
||||||
|
{/* Header */}
|
||||||
<div className="card-header d-flex justify-content-between">
|
<div className="card-header d-flex justify-content-between">
|
||||||
<div className="card-title mb-0 text-start">
|
<div className="card-title mb-0 text-start">
|
||||||
<h5 className="mb-1 fw-bold">Service Jobs</h5>
|
<h5 className="mb-1 fw-bold">Service Jobs</h5>
|
||||||
<p className="card-subtitle">
|
<p className="card-subtitle">
|
||||||
{projectLoading ? "Loading..." : projectData?.name || "All Projects"}
|
{projectLoading
|
||||||
|
? "Loading..."
|
||||||
|
: projectData?.name || "All Projects"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card-body p-0">
|
<div className="card-body p-0">
|
||||||
<div className="nav-align-top">
|
<div className="nav-align-top">
|
||||||
|
{/* Tabs */}
|
||||||
{/* ---------------- Tabs ---------------- */}
|
<ul className="nav nav-tabs nav-fill rounded-0 timeline-indicator-advanced">
|
||||||
<ul className="nav nav-tabs nav-fill rounded-0 timeline-indicator-advanced" role="tablist">
|
{tabMapping.map((tab) => (
|
||||||
{tabMapping.map((t, index) => (
|
<li className="nav-item" key={tab.id}>
|
||||||
<li className="nav-item" key={t.id}>
|
|
||||||
<button
|
<button
|
||||||
className={`nav-link ${index === 0 ? "active" : ""}`}
|
type="button"
|
||||||
data-bs-toggle="tab"
|
className={`nav-link ${
|
||||||
data-bs-target={`#${t.id}`}
|
activeTab === tab.id ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
>
|
>
|
||||||
{t.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* ---------------- Tab Content ---------------- */}
|
{/* Content */}
|
||||||
<div className="tab-content border-0 mx-1 text-start">
|
<div className="tab-content border-0 mx-1 text-start">
|
||||||
|
|
||||||
{isLoading && (
|
{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 />
|
<SpinnerLoader />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<p
|
<p
|
||||||
className="text-center"
|
className="text-center"
|
||||||
@ -70,19 +104,19 @@ const ServiceJobs = () => {
|
|||||||
>
|
>
|
||||||
No data found
|
No data found
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
!isError &&
|
!isError &&
|
||||||
tabMapping.map((t, index) => {
|
tabMapping.map((tab) => {
|
||||||
const list = jobs[t.key] || [];
|
const list = jobs[tab.key] || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={t.id}
|
key={tab.id}
|
||||||
className={`tab-pane fade ${index === 0 ? "show active" : ""}`}
|
className={`tab-pane fade ${
|
||||||
id={t.id}
|
activeTab === tab.id ? "show active" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{list.length === 0 ? (
|
{list.length === 0 ? (
|
||||||
<p
|
<p
|
||||||
@ -97,24 +131,19 @@ const ServiceJobs = () => {
|
|||||||
>
|
>
|
||||||
No jobs found
|
No jobs found
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<div className="job-scroll-wrapper">
|
<div className="job-scroll-wrapper">
|
||||||
{list.map((job, i) => (
|
{list.map((job, index) => (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={index}>
|
||||||
<ul className="timeline mb-0">
|
<ul className="timeline mb-0">
|
||||||
|
|
||||||
{/* Assigned By */}
|
|
||||||
<li className="timeline-item ps-6 border-left-dashed">
|
<li className="timeline-item ps-6 border-left-dashed">
|
||||||
<span className="timeline-indicator-advanced timeline-indicator-success border-0 shadow-none">
|
<span className="timeline-indicator-advanced timeline-indicator-success border-0 shadow-none">
|
||||||
<i className="bx bx-check-circle"></i>
|
<i className="bx bx-check-circle"></i>
|
||||||
</span>
|
</span>
|
||||||
<div className="timeline-event ps-1">
|
<div className="timeline-event ps-1">
|
||||||
<div className="timeline-header">
|
<small className="text-success text-uppercase">
|
||||||
<small className="text-success text-uppercase">
|
Assigned By
|
||||||
Assigned By
|
</small>
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<h6 className="my-50">{job.assignedBy}</h6>
|
<h6 className="my-50">{job.assignedBy}</h6>
|
||||||
<p className="text-body mb-0">
|
<p className="text-body mb-0">
|
||||||
{formatUTCToLocalTime(job.assignedAt)}
|
{formatUTCToLocalTime(job.assignedAt)}
|
||||||
@ -122,23 +151,23 @@ const ServiceJobs = () => {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* Project */}
|
|
||||||
<li className="timeline-item ps-6 border-transparent">
|
<li className="timeline-item ps-6 border-transparent">
|
||||||
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
|
<span className="timeline-indicator-advanced timeline-indicator-primary border-0 shadow-none">
|
||||||
<i className="bx bx-map"></i>
|
<i className="bx bx-map"></i>
|
||||||
</span>
|
</span>
|
||||||
<div className="timeline-event ps-1">
|
<div className="timeline-event ps-1">
|
||||||
<div className="timeline-header">
|
<small className="text-primary text-uppercase">
|
||||||
<small className="text-primary text-uppercase">Project</small>
|
Project
|
||||||
</div>
|
</small>
|
||||||
<h6 className="my-50">{job.project}</h6>
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Divider */}
|
{index < list.length - 1 && (
|
||||||
{i < list.length - 1 && (
|
|
||||||
<div className="border-1 border-light border-top border-dashed my-4"></div>
|
<div className="border-1 border-light border-top border-dashed my-4"></div>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@ -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) => {
|
const handleMouseMove = (e) => {
|
||||||
if (!dragging || isDocumentType) return;
|
if (!dragging || isDocumentType) return;
|
||||||
|
|
||||||
@ -105,6 +127,8 @@ const PreviewDocument = ({ files = [] }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
||||||
|
onWheel={handleWheel}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
|
|||||||
@ -13,37 +13,22 @@ const Sidebar = () => {
|
|||||||
id="layout-menu"
|
id="layout-menu"
|
||||||
className="layout-menu menu-vertical menu bg-menu-theme "
|
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">
|
<Link to="/dashboard" className="app-brand-link">
|
||||||
{/* <span className="app-brand-logo rounded-circle app-brand-logo-border">
|
<span className="app-brand-logo demo d-flex align-items-center">
|
||||||
<img
|
<img
|
||||||
className="app-brand-logo-sidebar"
|
|
||||||
src="/img/brand/marco.png"
|
src="/img/brand/marco.png"
|
||||||
alt="logo"
|
width="50"
|
||||||
aria-label="logo image"
|
height="40"
|
||||||
style={{ margin: "5px", paddingRight: "5px" }}
|
alt="OnFieldWork logo"
|
||||||
/>
|
/>
|
||||||
</span> */}
|
</span>
|
||||||
|
|
||||||
<a
|
<span className="app-brand-text">
|
||||||
href="/"
|
<span className="text-blue ms-1">OnField</span>
|
||||||
className="app-brand-link d-flex align-items-center gap-1 fw-bold navbar-brand "
|
<span className="text-green">Work</span>
|
||||||
>
|
<span className="text-dark">.com</span>
|
||||||
<span className="app-brand-logo demo d-flex align-items-center">
|
</span>
|
||||||
<img
|
|
||||||
src="/img/brand/marco.png"
|
|
||||||
width="40"
|
|
||||||
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-dark">.com</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<small className="layout-menu-toggle menu-link text-large ms-auto cursor-pointer">
|
<small className="layout-menu-toggle menu-link text-large ms-auto cursor-pointer">
|
||||||
|
|||||||
@ -33,8 +33,11 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
});
|
});
|
||||||
const isPending = isPendingProject || isPendingTenat;
|
const isPending = isPendingProject || isPendingTenat;
|
||||||
const mergedServices = useMemo(() => {
|
const mergedServices = useMemo(() => {
|
||||||
if (!masterService || !projectServices) return [];
|
const master = Array.isArray(masterService?.data) ? masterService.data : [];
|
||||||
const combined = [...masterService?.data, ...projectServices];
|
const project = Array.isArray(projectServices) ? projectServices : [];
|
||||||
|
|
||||||
|
const combined = [...master, ...project];
|
||||||
|
|
||||||
return combined.filter(
|
return combined.filter(
|
||||||
(item, index, self) => index === self.findIndex((s) => s.id === item.id)
|
(item, index, self) => index === self.findIndex((s) => s.id === item.id)
|
||||||
);
|
);
|
||||||
@ -100,7 +103,8 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
alt="logo"
|
alt="logo"
|
||||||
width={40}
|
width={40}
|
||||||
height={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>
|
||||||
<div className="text-end">
|
<div className="text-end">
|
||||||
<button
|
<button
|
||||||
@ -113,7 +117,10 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* Contact Info */}
|
||||||
<div className="col-md-12 mb-4">
|
<div className="col-md-12 mb-4">
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
@ -132,7 +139,7 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
className="form-label me-2 mb-0 fw-semibold"
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
style={{ minWidth: "130px" }}
|
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>
|
</label>
|
||||||
<div className="text-muted">{orgData.contactNumber}</div>
|
<div className="text-muted">{orgData.contactNumber}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -143,7 +150,7 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
className="form-label me-2 mb-0 fw-semibold"
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
style={{ minWidth: "130px" }}
|
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>
|
</label>
|
||||||
<div className="text-muted text-wrap">{orgData.email}</div>
|
<div className="text-muted text-wrap">{orgData.email}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -166,7 +173,7 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
className="form-label me-1 mb-0 fw-semibold"
|
className="form-label me-1 mb-0 fw-semibold"
|
||||||
style={{ minWidth: "130px" }}
|
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>
|
</label>
|
||||||
<div className="text-muted text-start">{orgData.address}</div>
|
<div className="text-muted text-start">{orgData.address}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -180,7 +187,11 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
<>
|
<>
|
||||||
{/* Organization Type */}
|
{/* Organization Type */}
|
||||||
<div className="mb-3 text-start">
|
<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
|
Organization Type
|
||||||
</Label>
|
</Label>
|
||||||
<div className="d-flex flex-wrap gap-3 mt-1">
|
<div className="d-flex flex-wrap gap-3 mt-1">
|
||||||
@ -214,7 +225,11 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
|
|
||||||
{/* Services */}
|
{/* Services */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Label htmlFor="serviceIds" className="mb-3 fw-semibold" required>
|
<Label
|
||||||
|
htmlFor="serviceIds"
|
||||||
|
className="mb-3 fw-semibold"
|
||||||
|
required
|
||||||
|
>
|
||||||
Select Services
|
Select Services
|
||||||
</Label>
|
</Label>
|
||||||
{mergedServices?.map((service) => (
|
{mergedServices?.map((service) => (
|
||||||
@ -255,8 +270,8 @@ const AssignOrg = ({ setStep }) => {
|
|||||||
{isPending
|
{isPending
|
||||||
? "Please wait..."
|
? "Please wait..."
|
||||||
: flowType === "default"
|
: flowType === "default"
|
||||||
? "Assign to Organization"
|
? "Assign to Organization"
|
||||||
: "Assign to Project"}
|
: "Assign to Project"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -237,13 +237,116 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</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)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="form-label text-start">
|
<div className="form-label text-start">
|
||||||
<div className="row mb-1">
|
<div className="row mb-1">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="row text-start">
|
<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>
|
<div>
|
||||||
<AppFormController
|
<AppFormController
|
||||||
name="organizationId"
|
name="organizationId"
|
||||||
@ -296,109 +399,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
useGroups,
|
useGroups,
|
||||||
useWorkCategoriesMaster,
|
useWorkCategoriesMaster,
|
||||||
} from "../../../hooks/masterHook/useMaster";
|
} 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 showToast from "../../../services/toastService";
|
||||||
import Label from "../../common/Label";
|
import Label from "../../common/Label";
|
||||||
import { useSelectedProject } from "../../../slices/apiDataManager";
|
import { useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
@ -28,12 +28,16 @@ const taskSchema = z.object({
|
|||||||
comment: z.string(),
|
comment: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultModel = {
|
|
||||||
|
|
||||||
|
const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||||
|
const currentService = useCurrentService()
|
||||||
|
const defaultModel = {
|
||||||
id: null,
|
id: null,
|
||||||
buildingID: "",
|
buildingID: "",
|
||||||
floorId: "",
|
floorId: "",
|
||||||
workAreaId: "",
|
workAreaId: "",
|
||||||
serviceId: "",
|
serviceId: currentService ?? "",
|
||||||
activityGroupId: "",
|
activityGroupId: "",
|
||||||
activityID: "",
|
activityID: "",
|
||||||
workCategoryId: "",
|
workCategoryId: "",
|
||||||
@ -41,8 +45,6 @@ const defaultModel = {
|
|||||||
completedWork: 0,
|
completedWork: 0,
|
||||||
comment: "",
|
comment: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskModel = ({ project, onSubmit, onClose }) => {
|
|
||||||
// const { activities, loading: activityLoading } = useActivitiesMaster();
|
// const { activities, loading: activityLoading } = useActivitiesMaster();
|
||||||
const { categories, categoryLoading } = useWorkCategoriesMaster();
|
const { categories, categoryLoading } = useWorkCategoriesMaster();
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
|
|||||||
const { data: assignedServices, isLoading: servicesLoading } =
|
const { data: assignedServices, isLoading: servicesLoading } =
|
||||||
useProjectAssignedServices(projectId);
|
useProjectAssignedServices(projectId);
|
||||||
|
|
||||||
const { control } = useForm({
|
const { control, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serviceId: selectedService || "",
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{showModalBuilding && (
|
{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="col-md-4 col-12 dataTables_length text-start py-2 px-2">
|
||||||
<div className="ms-4 mt-n1">
|
<div className="ms-4 mt-n1">
|
||||||
{!servicesLoading && assignedServices?.length > 0 && (
|
{!servicesLoading && assignedServices?.length > 0 && (
|
||||||
assignedServices.length > 1 ? (
|
<AppFormController
|
||||||
<AppFormController
|
name="serviceId"
|
||||||
name="serviceId"
|
control={control}
|
||||||
control={control}
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<SelectField
|
||||||
<SelectField
|
label="Select Service"
|
||||||
label="Select Service"
|
options={[...(assignedServices ?? [])]}
|
||||||
options={[{ id: "", name: "All Projects" }, ...(assignedServices ?? [])]}
|
placeholder="Choose a Service"
|
||||||
placeholder="Choose a Service"
|
labelKey="name"
|
||||||
required
|
valueKey="id"
|
||||||
labelKey="name"
|
value={field.value}
|
||||||
valueKey="id"
|
onChange={(val) => {
|
||||||
value={field.value}
|
field.onChange(val);
|
||||||
onChange={(val) => {
|
dispatch(setService(val));
|
||||||
field.onChange(val);
|
}}
|
||||||
handleServiceChange(val);
|
isLoading={servicesLoading}
|
||||||
}}
|
/>
|
||||||
isLoading={servicesLoading}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<h5>{assignedServices[0].name}</h5>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -159,8 +159,9 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
|
|||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
<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">
|
<div className="dropdown z-2">
|
||||||
<button
|
<button
|
||||||
@ -238,7 +239,7 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
|
|||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
paginate={paginate}
|
onPageChange={paginate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -170,7 +170,7 @@ const ProjectStatistics = ({ project }) => {
|
|||||||
<h5 className="card-action-title mb-0">
|
<h5 className="card-action-title mb-0">
|
||||||
{" "}
|
{" "}
|
||||||
<i className="fa fa-line-chart rounded-circle text-primary"></i>
|
<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>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import TeamEmployeeList from "./TeamEmployeeList";
|
import TeamEmployeeList from "./TeamEmployeeList";
|
||||||
import { useOrganization } from "../../../hooks/useDirectory";
|
import { useOrganization } from "../../../hooks/useDirectory";
|
||||||
import { useOrganizationsList } from "../../../hooks/useOrganization";
|
import { useOrganizationsList } from "../../../hooks/useOrganization";
|
||||||
@ -14,40 +14,50 @@ const TeamAssignToProject = ({ closeModal }) => {
|
|||||||
const project = useSelectedProject();
|
const project = useSelectedProject();
|
||||||
const { data, isLoading, isError, error } =
|
const { data, isLoading, isError, error } =
|
||||||
useProjectAssignedOrganizationsName(project);
|
useProjectAssignedOrganizationsName(project);
|
||||||
const { control, watch, formState: { errors } } = useForm({
|
const { control, watch, setValue, formState: { errors } } = useForm({
|
||||||
defaultValues: { organizationId: "" },
|
defaultValues: { organizationId: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.length === 1) {
|
||||||
|
setValue("organizationId", data[0].id, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, setValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{/* <p className="fs-5 fs-seminbod ">Assign Employee To Project </p> */}
|
{/* <p className="fs-5 fs-seminbod ">Assign Employee To Project </p> */}
|
||||||
<h5 className="mb-4">Assign Employee To Project</h5>
|
<h5 className="mb-4">Assign Employee To Project</h5>
|
||||||
|
|
||||||
<div className="row align-items-center gx-5 text-start">
|
<div className="row align-items-center gx-5 text-start">
|
||||||
<div className="col-12 col-md-6 mb-2">
|
<div className="col-12 col-md-6 mb-2">
|
||||||
<AppFormController
|
<AppFormController
|
||||||
name="organizationId"
|
name="organizationId"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: "Organization is required" }}
|
rules={{ required: "Organization is required" }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Select Organization"
|
label="Select Organization"
|
||||||
options={data ?? []}
|
options={data ?? []}
|
||||||
placeholder="Choose an Organization"
|
placeholder="Choose an Organization"
|
||||||
required
|
required
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
className="m-0 w-100"
|
className="m-0 w-100"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{errors.organizationId && (
|
|
||||||
<small className="danger-text">{errors.organizationId.message}</small>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
/>
|
||||||
<div className="col-12 col-md-6 mt-n5">
|
{errors.organizationId && (
|
||||||
|
<small className="danger-text">{errors.organizationId.message}</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6 mt-n2">
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<label htmlFor="search" className="form-label mb-1">
|
<label htmlFor="search" className="form-label mb-1">
|
||||||
Search Employee
|
Search Employee
|
||||||
|
|||||||
@ -162,15 +162,23 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
<table className="table" style={{ maxHeight: "80px", overflowY: "auto" }}>
|
<table className="table" style={{ maxHeight: "80px", overflowY: "auto" }}>
|
||||||
<thead className=" position-sticky top-0">
|
<thead className=" position-sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Select</th>
|
||||||
<th>Employee</th>
|
<th>Employee</th>
|
||||||
<th>Service</th>
|
<th>Service</th>
|
||||||
<th>Job Role</th>
|
<th>Job Role</th>
|
||||||
<th>Select</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{employees.map((emp, index) => (
|
{employees.map((emp, index) => (
|
||||||
<tr key={emp.id}>
|
<tr key={emp.id}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
checked={emp.isChecked}
|
||||||
|
onChange={() => handleCheckboxChange(index)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Avatar firstName={emp.firstName} lastName={emp.lastName} />
|
<Avatar firstName={emp.firstName} lastName={emp.lastName} />
|
||||||
@ -179,6 +187,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
value={emp.serviceId}
|
value={emp.serviceId}
|
||||||
@ -221,14 +230,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
<div className="danger-text">{emp.errors.jobRole}</div>
|
<div className="danger-text">{emp.errors.jobRole}</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
checked={emp.isChecked}
|
|
||||||
onChange={() => handleCheckboxChange(index)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const Teams = () => {
|
|||||||
const [selectedEmployee, setSelectedEmployee] = useState(null);
|
const [selectedEmployee, setSelectedEmployee] = useState(null);
|
||||||
const [deleteEmployee, setDeleteEmplyee] = useState(null);
|
const [deleteEmployee, setDeleteEmplyee] = useState(null);
|
||||||
const [activeEmployee, setActiveEmployee] = useState(false);
|
const [activeEmployee, setActiveEmployee] = useState(false);
|
||||||
const { control, watch } = useForm({
|
const { control, watch, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
selectedService: "",
|
selectedService: "",
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
@ -136,6 +136,18 @@ const Teams = () => {
|
|||||||
return () => eventBus.off("employee", employeeHandler);
|
return () => eventBus.off("employee", employeeHandler);
|
||||||
}, [employeeHandler]);
|
}, [employeeHandler]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!servicesLoading && assignedServices?.length === 1) {
|
||||||
|
const serviceId = assignedServices[0].id;
|
||||||
|
|
||||||
|
setValue("selectedService", serviceId, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [assignedServices, servicesLoading, setValue]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{AssigTeam && (
|
{AssigTeam && (
|
||||||
@ -162,37 +174,30 @@ const Teams = () => {
|
|||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="row align-items-center justify-content-between mb-4 g-3">
|
<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">
|
<div className="col-md-6 col-12 d-flex flex-wrap align-items-center gap-3">
|
||||||
{!servicesLoading && assignedServices && (
|
{!servicesLoading && assignedServices?.length > 0 && (
|
||||||
<>
|
<div className="col-12 col-md-6 mb-2 text-start">
|
||||||
{assignedServices.length === 1 && (
|
<AppFormController
|
||||||
<h5 className="mb-2">{assignedServices[0].name}</h5>
|
name="selectedService"
|
||||||
)}
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
{assignedServices.length > 1 && (
|
<SelectField
|
||||||
<div className="col-12 col-md-6 mb-2 text-start">
|
label="Select Service"
|
||||||
<AppFormController
|
options={[ ...assignedServices]}
|
||||||
name="selectedService"
|
placeholder="Choose a Service"
|
||||||
control={control}
|
labelKey="name"
|
||||||
render={({ field }) => (
|
valueKey="id"
|
||||||
<SelectField
|
value={field.value}
|
||||||
label="Select Service"
|
onChange={field.onChange}
|
||||||
options={[{ id: "", name: "All Services" }, ...assignedServices]}
|
isLoading={servicesLoading}
|
||||||
placeholder="Choose a Service"
|
className="w-100"
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
isLoading={servicesLoading}
|
|
||||||
className="w-100"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="form-check form-switch d-flex align-items-center text-nowrap">
|
<div className="form-check form-switch d-flex align-items-center text-nowrap">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
|||||||
// onSubmitTenant(data);
|
// onSubmitTenant(data);
|
||||||
// onNext();
|
// onNext();
|
||||||
const tenantPayload = { ...data, onBoardingDate: moment.utc(data.onBoardingDate, "DD-MM-YYYY").toISOString() }
|
const tenantPayload = { ...data, onBoardingDate: moment.utc(data.onBoardingDate, "DD-MM-YYYY").toISOString() }
|
||||||
// CreateTenant(tenantPayload);
|
CreateTenant(tenantPayload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useController } from "react-hook-form";
|
|||||||
|
|
||||||
const DatePicker = ({
|
const DatePicker = ({
|
||||||
name,
|
name,
|
||||||
|
defaultDate,
|
||||||
control,
|
control,
|
||||||
size="sm",
|
size="sm",
|
||||||
placeholder = "DD-MM-YYYY",
|
placeholder = "DD-MM-YYYY",
|
||||||
|
|||||||
@ -93,6 +93,7 @@ export const DateRangePicker1 = ({
|
|||||||
resetSignal,
|
resetSignal,
|
||||||
defaultRange = true,
|
defaultRange = true,
|
||||||
maxDate = null,
|
maxDate = null,
|
||||||
|
pastDays = 6,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
@ -105,7 +106,7 @@ export const DateRangePicker1 = ({
|
|||||||
const applyDefaultDates = () => {
|
const applyDefaultDates = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const past = 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 format = (d) => flatpickr.formatDate(d, "d-m-Y");
|
||||||
const formattedStart = format(past);
|
const formattedStart = format(past);
|
||||||
|
|||||||
@ -2,10 +2,17 @@ import React from "react";
|
|||||||
import { useDeliverChallane } from "../../hooks/usePurchase";
|
import { useDeliverChallane } from "../../hooks/usePurchase";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
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 DeliverChallanList = ({ purchaseId, viewDocuments }) => {
|
||||||
|
const { setDocumentView } = usePurchaseContext();
|
||||||
const { data, isLoading, isError, error } = useDeliverChallane(purchaseId);
|
const { data, isLoading, isError, error } = useDeliverChallane(purchaseId);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="d-flex justify-content-center align-items-center text-center vh-50">
|
<div className="d-flex justify-content-center align-items-center text-center vh-50">
|
||||||
@ -17,10 +24,12 @@ const DeliverChallanList = ({ purchaseId, viewDocuments }) => {
|
|||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
|
{/* Assuming Error component is used here */}
|
||||||
<Error error={error} />
|
<Error error={error} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoading && data.length === 0)
|
if (!isLoading && data.length === 0)
|
||||||
return (
|
return (
|
||||||
<div className="d-flex justify-content-center align-items-center text-center vh-50">
|
<div className="d-flex justify-content-center align-items-center text-center vh-50">
|
||||||
@ -55,8 +64,29 @@ const DeliverChallanList = ({ purchaseId, viewDocuments }) => {
|
|||||||
{item.description || "-"}
|
{item.description || "-"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Check if attachment exists and open document view on click */}
|
||||||
{item.attachment?.preSignedUrl && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
31
src/components/reports/ActivitiesTable.jsx
Normal 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;
|
||||||
121
src/components/reports/Progress.jsx
Normal 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>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
65
src/components/reports/ReportsDonutCard.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
17
src/components/reports/ReportsLegend.jsx
Normal 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;
|
||||||
20
src/components/reports/TeamStrengthCard.jsx
Normal 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;
|
||||||
176
src/components/reports/report-dpr.jsx
Normal 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;
|
||||||
@ -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 useAppForm = (config) => useForm(config);
|
||||||
export const AppFormProvider = FormProvider;
|
export const AppFormProvider = FormProvider;
|
||||||
export const AppFormController = Controller;
|
export const AppFormController = Controller;
|
||||||
export const useAppFormContext = useFormContext;
|
export const useAppFormContext = useFormContext;
|
||||||
|
export const useAppWatch = useWatch;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import GlobalRepository from "../repositories/GlobalRepository";
|
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 }) => {
|
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
||||||
const [dashboard_data, setDashboard_Data] = useState([]);
|
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
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -122,20 +122,29 @@ const AttendancePage = () => {
|
|||||||
// Conditionally include Regularization tab based on permission
|
// Conditionally include Regularization tab based on permission
|
||||||
...(DoRegularized
|
...(DoRegularized
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
id: "regularization",
|
id: "regularization",
|
||||||
title: "Regularization",
|
title: "Regularization",
|
||||||
content: (
|
content: (
|
||||||
<Regularization
|
<Regularization
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
organizationId={appliedFilters.selectedOrganization}
|
organizationId={appliedFilters.selectedOrganization}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
// --- END: Tab Configuration Array
|
// --- END: Tab Configuration Array
|
||||||
|
useEffect(() => {
|
||||||
|
if (!orgLoading && organizations?.length === 1) {
|
||||||
|
setAppliedFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedOrganization: organizations[0].id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [orgLoading, organizations]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -148,11 +157,11 @@ const AttendancePage = () => {
|
|||||||
{(modelConfig?.action === 0 ||
|
{(modelConfig?.action === 0 ||
|
||||||
modelConfig?.action === 1 ||
|
modelConfig?.action === 1 ||
|
||||||
modelConfig?.action === 2) && (
|
modelConfig?.action === 2) && (
|
||||||
<CheckCheckOutmodel
|
<CheckCheckOutmodel
|
||||||
modeldata={modelConfig}
|
modeldata={modelConfig}
|
||||||
closeModal={closeModal}
|
closeModal={closeModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* For view logs */}
|
{/* For view logs */}
|
||||||
{modelConfig?.action === 6 && (
|
{modelConfig?.action === 6 && (
|
||||||
<AttendLogs Id={modelConfig?.id} closeModal={closeModal} />
|
<AttendLogs Id={modelConfig?.id} closeModal={closeModal} />
|
||||||
@ -178,18 +187,16 @@ const AttendancePage = () => {
|
|||||||
<ul className="nav nav-tabs" role="tablist">
|
<ul className="nav nav-tabs" role="tablist">
|
||||||
{tabsData.map((tab) => (
|
{tabsData.map((tab) => (
|
||||||
<li
|
<li
|
||||||
className={`nav-item ${
|
className={`nav-item ${tab.id === "regularization" && !DoRegularized
|
||||||
tab.id === "regularization" && !DoRegularized
|
? "d-none"
|
||||||
? "d-none"
|
: ""
|
||||||
: ""
|
}`} // Keep the d-none logic for regularization, although it's filtered above
|
||||||
}`} // Keep the d-none logic for regularization, although it's filtered above
|
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === tab.id ? "active" : ""
|
||||||
activeTab === tab.id ? "active" : ""
|
} fs-6`}
|
||||||
} fs-6`}
|
|
||||||
onClick={() => handleTabChange(tab.id)}
|
onClick={() => handleTabChange(tab.id)}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
data-bs-target={`#navs-top-${tab.id}`}
|
data-bs-target={`#navs-top-${tab.id}`}
|
||||||
@ -245,9 +252,8 @@ const AttendancePage = () => {
|
|||||||
{tabsData.map((tab) => (
|
{tabsData.map((tab) => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`tab-pane fade ${
|
className={`tab-pane fade ${activeTab === tab.id ? "show active" : ""
|
||||||
activeTab === tab.id ? "show active" : ""
|
} py-0 ${tab.id === "all" ? "mx-2" : "p-3"}`}
|
||||||
} py-0 ${tab.id === "all" ? "mx-2" : "p-3"}`}
|
|
||||||
id={`navs-top-${tab.id}`}
|
id={`navs-top-${tab.id}`}
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const TaskPlanning = () => {
|
|||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const selectedService = useCurrentService();
|
const selectedService = useCurrentService();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { control } = useForm({
|
const { control,setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serviceFilter: selectedService ?? ""
|
serviceFilter: selectedService ?? ""
|
||||||
},
|
},
|
||||||
@ -39,6 +39,20 @@ const TaskPlanning = () => {
|
|||||||
return <div className="text-center py-5">Loading...</div>;
|
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 (
|
return (
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
@ -51,7 +65,7 @@ const TaskPlanning = () => {
|
|||||||
<div className="card py-2 page-min-h">
|
<div className="card py-2 page-min-h">
|
||||||
<div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start">
|
<div className="col-sm-4 col-md-3 col-12 px-4 py-2 text-start">
|
||||||
{data?.length === 0 ? (
|
{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
|
<AppFormController
|
||||||
name="serviceFilter"
|
name="serviceFilter"
|
||||||
@ -78,6 +92,7 @@ const TaskPlanning = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Planning Component */}
|
{/* Planning Component */}
|
||||||
{selectedProject ? (
|
{selectedProject ? (
|
||||||
<InfraPlanning />
|
<InfraPlanning />
|
||||||
|
|||||||
@ -46,14 +46,14 @@ const DailyProgrssReport = () => {
|
|||||||
filter,
|
filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { control } = useForm({
|
const { control,setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serviceFilter: ""
|
serviceFilter: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const clearFilter = () => {
|
const clearFilter = () => {
|
||||||
updatedRef.current?.onClear();
|
updatedRef.current?.onClear();
|
||||||
};
|
};
|
||||||
const handleFilter = (filterObj) => {
|
const handleFilter = (filterObj) => {
|
||||||
setFilter(filterObj)
|
setFilter(filterObj)
|
||||||
}
|
}
|
||||||
@ -85,6 +85,22 @@ const DailyProgrssReport = () => {
|
|||||||
return updated;
|
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 (
|
return (
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<DailyProgrssContext.Provider value={contextDispatcher}>
|
<DailyProgrssContext.Provider value={contextDispatcher}>
|
||||||
@ -129,7 +145,7 @@ const DailyProgrssReport = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Services"
|
label="Services"
|
||||||
options={[{ id: "", name: "All Projects" }, ...(data ?? [])]}
|
options={[{ id: "", name: "All Services" }, ...(data ?? [])]}
|
||||||
placeholder="Select Service"
|
placeholder="Select Service"
|
||||||
labelKey="name"
|
labelKey="name"
|
||||||
valueKey="id"
|
valueKey="id"
|
||||||
|
|||||||
@ -86,7 +86,7 @@ const LandingPage = () => {
|
|||||||
<li className="nav-item ms-1">
|
<li className="nav-item ms-1">
|
||||||
<a
|
<a
|
||||||
className="btn btn-sm btn-green btn-ovel-small px-3 my-1"
|
className="btn btn-sm btn-green btn-ovel-small px-3 my-1"
|
||||||
href="#"
|
href="/auth/reqest/demo"
|
||||||
>
|
>
|
||||||
Request For Demo
|
Request For Demo
|
||||||
</a>
|
</a>
|
||||||
@ -162,7 +162,7 @@ const LandingPage = () => {
|
|||||||
Make data-driven decisions with real-time project analytics.
|
Make data-driven decisions with real-time project analytics.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="/auth/reqest/demo"
|
||||||
className="btn btn-green btn-square-small btn-lg mt-3 p-3"
|
className="btn btn-green btn-square-small btn-lg mt-3 p-3"
|
||||||
>
|
>
|
||||||
View Demo
|
View Demo
|
||||||
@ -185,7 +185,7 @@ const LandingPage = () => {
|
|||||||
Eliminate Paper Receipts. Take Control of Your Cash Flow.
|
Eliminate Paper Receipts. Take Control of Your Cash Flow.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="/auth/reqest/demo"
|
||||||
className="btn btn-green btn-square-small btn-lg mt-3 p-3"
|
className="btn btn-green btn-square-small btn-lg mt-3 p-3"
|
||||||
>
|
>
|
||||||
View Demo
|
View Demo
|
||||||
@ -320,65 +320,6 @@ const LandingPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
{/* <SubscriptionPlans/> */}
|
{/* <SubscriptionPlans/> */}
|
||||||
<SubscriptionPlans></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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/* <!-- About --> */}
|
{/* <!-- About --> */}
|
||||||
|
|||||||
@ -9,7 +9,8 @@ const SubscriptionPlans = () => {
|
|||||||
const [frequency, setFrequency] = useState(1);
|
const [frequency, setFrequency] = useState(1);
|
||||||
const { data, isLoading, isError, error } = useSubscription(frequency);
|
const { data, isLoading, isError, error } = useSubscription(frequency);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
console.log(data);
|
||||||
const frequencyLabel = (freq) => {
|
const frequencyLabel = (freq) => {
|
||||||
switch (freq) {
|
switch (freq) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -35,9 +36,8 @@ const SubscriptionPlans = () => {
|
|||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-${
|
className={`btn btn-${frequency === idx ? "primary" : "outline-secondary"
|
||||||
frequency === idx ? "primary" : "outline-secondary"
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => setFrequency(idx)}
|
onClick={() => setFrequency(idx)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@ -64,7 +64,7 @@ const SubscriptionPlans = () => {
|
|||||||
<p>{error.name}</p>
|
<p>{error.name}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((plan) => (
|
data.map((plan, index) => (
|
||||||
<div key={plan.id} className="col-xl-4 col-lg-6 col-md-6">
|
<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">
|
<div className="card h-100 shadow-lg border-0 p-3 text-center p-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -102,24 +102,69 @@ const SubscriptionPlans = () => {
|
|||||||
<h6 className="fw-bold text-uppercase border-top pt-3 mb-3 text-center">
|
<h6 className="fw-bold text-uppercase border-top pt-3 mb-3 text-center">
|
||||||
Features
|
Features
|
||||||
</h6>
|
</h6>
|
||||||
<ul className="list-unstyled text-start mb-4 ms-7 fs-6">
|
|
||||||
|
<div className="accordion" id={`planFeatures-${plan.id}`}>
|
||||||
{plan.features?.modules &&
|
{plan.features?.modules &&
|
||||||
Object.values(plan.features.modules).map((mod) =>
|
Object.entries(plan.features.modules)
|
||||||
mod && mod.name ? (
|
.sort(([, a], [, b]) => Number(b.enabled) - Number(a.enabled))
|
||||||
<li
|
.map(([key, mod]) => {
|
||||||
key={mod.id}
|
|
||||||
className="d-flex align-items-center mb-2"
|
if (!mod || !mod.name) return null;
|
||||||
>
|
const isFirst = index === 0;
|
||||||
{mod.enabled ? (
|
|
||||||
<i className="fa-regular fa-circle-check text-success me-2"></i>
|
return (
|
||||||
) : (
|
<div className="accordion-item mb-2" key={`${plan.id}-${mod.id}`}>
|
||||||
<i className="fa-regular fa-circle-xmark text-danger me-2"></i>
|
<h2 id={`heading-${plan.id}-${mod.id}`} className="accordion-header">
|
||||||
)}
|
<button
|
||||||
{mod.name}
|
className="accordion-button py-2 d-flex justify-content-between align-items-center"
|
||||||
</li>
|
type="button"
|
||||||
) : null
|
data-bs-toggle="collapse"
|
||||||
)}
|
data-bs-target={`#collapse-${plan.id}-${mod.id}`}
|
||||||
</ul>
|
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>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted small mb-0">No additional features</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
@ -138,6 +183,7 @@ const SubscriptionPlans = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const ServiceProjectDisplay = ({ listView, selectedStatuses, searchTerm }) => {
|
|||||||
const { manageServiceProject, setManageServiceProject } = useProjectContext();
|
const { manageServiceProject, setManageServiceProject } = useProjectContext();
|
||||||
const debouncedSearch = useDebounce(searchTerm, 500);
|
const debouncedSearch = useDebounce(searchTerm, 500);
|
||||||
const { data, isLoading, isError, error } = useServiceProjects(
|
const { data, isLoading, isError, error } = useServiceProjects(
|
||||||
ITEMS_PER_PAGE,
|
9,
|
||||||
currentPage,
|
currentPage,
|
||||||
debouncedSearch
|
debouncedSearch
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import LoginWithOtp from './LoginWithOtp'
|
import LoginWithOtp from "./LoginWithOtp";
|
||||||
|
|
||||||
const MainLoginWithOTPPage = () => {
|
const MainLoginWithOTPPage = () => {
|
||||||
return (
|
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="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">
|
<div className="w-100 d-flex justify-content-center">
|
||||||
<img
|
<img
|
||||||
src="/img/illustrations/worker_03.png"
|
src="/img/illustrations/registration.jpg"
|
||||||
className="img-fluid"
|
className="img-fluid"
|
||||||
alt="Login image"
|
alt="Login image"
|
||||||
width="70%"
|
width="70%"
|
||||||
@ -20,7 +20,7 @@ const MainLoginWithOTPPage = () => {
|
|||||||
<LoginWithOtp />
|
<LoginWithOtp />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MainLoginWithOTPPage
|
export default MainLoginWithOTPPage;
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import ResetPasswordPage from './ResetPassword'
|
import ResetPasswordPage from "./ResetPassword";
|
||||||
|
|
||||||
const MainResetPasswordPage = () => {
|
const MainResetPasswordPage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="authentication-inner row m-0">
|
<div className="authentication-inner row m-0">
|
||||||
<div className="d-none d-lg-flex col-lg-7 col-xl-8 align-items-center p-5">
|
<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">
|
<div className="w-100 d-flex justify-content-center">
|
||||||
<img
|
<img
|
||||||
src="/img/illustrations/worker_03.png"
|
src="/img/illustrations/registration.jpg"
|
||||||
className="img-fluid"
|
className="img-fluid"
|
||||||
alt="Login image"
|
alt="Login image"
|
||||||
width="70%"
|
width="70%"
|
||||||
@ -20,7 +20,7 @@ const MainResetPasswordPage = () => {
|
|||||||
<ResetPasswordPage />
|
<ResetPasswordPage />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MainResetPasswordPage
|
export default MainResetPasswordPage;
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import Pagination from "../../components/common/Pagination";
|
|||||||
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
|
import handleEmployeeExport from "../../components/Employee/handleEmployeeExport";
|
||||||
import { SpinnerLoader } from "../../components/common/Loader";
|
import { SpinnerLoader } from "../../components/common/Loader";
|
||||||
import ManageReporting from "../../components/Employee/ManageReporting";
|
import ManageReporting from "../../components/Employee/ManageReporting";
|
||||||
|
import { capitalizeFirstLetter } from "../../utils/appUtils";
|
||||||
|
|
||||||
const EmployeeList = () => {
|
const EmployeeList = () => {
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector(
|
||||||
@ -73,9 +74,11 @@ const EmployeeList = () => {
|
|||||||
const [selectedEmployee, setSelectedEmployee] = useState(null);
|
const [selectedEmployee, setSelectedEmployee] = useState(null);
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [selectedEmpFordelete, setSelectedEmpFordelete] = useState(null);
|
const [selectedEmpFordelete, setSelectedEmpFordelete] = useState(null);
|
||||||
const [showManageReportingModal, setShowManageReportingModal] = useState(false);
|
const [showManageReportingModal, setShowManageReportingModal] =
|
||||||
|
useState(false);
|
||||||
const [employeeLodaing, setemployeeLodaing] = useState(false);
|
const [employeeLodaing, setemployeeLodaing] = useState(false);
|
||||||
const ViewTeamMember = useHasUserPermission(VIEW_TEAM_MEMBERS);
|
const ViewTeamMember = useHasUserPermission(VIEW_TEAM_MEMBERS);
|
||||||
|
const ViewAllEmployee = useHasUserPermission(VIEW_ALL_EMPLOYEES);
|
||||||
const { mutate: suspendEmployee, isPending: empLodaing } = useSuspendEmployee(
|
const { mutate: suspendEmployee, isPending: empLodaing } = useSuspendEmployee(
|
||||||
{
|
{
|
||||||
setIsDeleteModalOpen,
|
setIsDeleteModalOpen,
|
||||||
@ -139,11 +142,15 @@ const EmployeeList = () => {
|
|||||||
|
|
||||||
const tableRef = useRef(null);
|
const tableRef = useRef(null);
|
||||||
const handleExport = (type) => {
|
const handleExport = (type) => {
|
||||||
handleEmployeeExport(type, employeeList, filteredData, searchText, tableRef);
|
handleEmployeeExport(
|
||||||
|
type,
|
||||||
|
employeeList,
|
||||||
|
filteredData,
|
||||||
|
searchText,
|
||||||
|
tableRef
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleAllEmployeesToggle = (e) => {
|
const handleAllEmployeesToggle = (e) => {
|
||||||
const isChecked = e.target.checked;
|
const isChecked = e.target.checked;
|
||||||
setShowInactive(false);
|
setShowInactive(false);
|
||||||
@ -172,10 +179,12 @@ const EmployeeList = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && Array.isArray(employees)) {
|
if (!loading && Array.isArray(employees)) {
|
||||||
const sorted = [...employees].sort((a, b) => {
|
const sorted = [...employees].sort((a, b) => {
|
||||||
const nameA = `${a.firstName || ""}${a.middleName || ""}${a.lastName || ""
|
const nameA = `${a.firstName || ""}${a.middleName || ""}${
|
||||||
}`.toLowerCase();
|
a.lastName || ""
|
||||||
const nameB = `${b.firstName || ""}${b.middleName || ""}${b.lastName || ""
|
}`.toLowerCase();
|
||||||
}`.toLowerCase();
|
const nameB = `${b.firstName || ""}${b.middleName || ""}${
|
||||||
|
b.lastName || ""
|
||||||
|
}`.toLowerCase();
|
||||||
return nameA?.localeCompare(nameB);
|
return nameA?.localeCompare(nameB);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -265,8 +274,9 @@ const EmployeeList = () => {
|
|||||||
? "Suspend Employee"
|
? "Suspend Employee"
|
||||||
: "Reactivate Employee"
|
: "Reactivate Employee"
|
||||||
}
|
}
|
||||||
message={`Are you sure you want to ${selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
message={`Are you sure you want to ${
|
||||||
} this employee?`}
|
selectedEmpFordelete?.isActive ? "suspend" : "reactivate"
|
||||||
|
} this employee?`}
|
||||||
onSubmit={(id) =>
|
onSubmit={(id) =>
|
||||||
suspendEmployee({
|
suspendEmployee({
|
||||||
employeeId: id,
|
employeeId: id,
|
||||||
@ -287,7 +297,7 @@ const EmployeeList = () => {
|
|||||||
]}
|
]}
|
||||||
></Breadcrumb>
|
></Breadcrumb>
|
||||||
|
|
||||||
{ViewTeamMember ? (
|
{ViewTeamMember || ViewAllEmployee ? (
|
||||||
// <div className="row">
|
// <div className="row">
|
||||||
<div className="card p-1 page-min-h">
|
<div className="card p-1 page-min-h">
|
||||||
<div className="card-datatable table-responsive pt-5 mx-5 py-10">
|
<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>
|
<i className="bx bx-dots-vertical-rounded bx-md"></i>
|
||||||
</button>
|
</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">
|
<li className="dropdown-item d-flex align-items-center justify-content-between">
|
||||||
<div className="form-check form-switch mb-0">
|
<div className="form-check form-switch mb-0">
|
||||||
<input
|
<input
|
||||||
@ -351,39 +363,53 @@ const EmployeeList = () => {
|
|||||||
role="switch"
|
role="switch"
|
||||||
id="inactiveEmployeesCheckboxMenu"
|
id="inactiveEmployeesCheckboxMenu"
|
||||||
checked={showInactive}
|
checked={showInactive}
|
||||||
onChange={(e) => setShowInactive(e.target.checked)}
|
onChange={(e) =>
|
||||||
|
setShowInactive(e.target.checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ms-0">Show Inactive Employees</span>
|
<span className="ms-0">Show Inactive Employees</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
<li><hr className="dropdown-divider" /></li>
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
|
||||||
<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
|
<i className="bx bx-printer me-2"></i> Print
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<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
|
<i className="bx bx-file me-2"></i> CSV
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<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
|
<i className="bx bxs-file-export me-2"></i> Excel
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<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
|
<i className="bx bxs-file-pdf me-2"></i> PDF
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table
|
<table
|
||||||
@ -460,8 +486,9 @@ const EmployeeList = () => {
|
|||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className={`sorting_disabled ${!Manage_Employee && "d-none"
|
className={`sorting_disabled ${
|
||||||
}`}
|
!Manage_Employee && "d-none"
|
||||||
|
}`}
|
||||||
rowSpan="1"
|
rowSpan="1"
|
||||||
colSpan="1"
|
colSpan="1"
|
||||||
style={{ width: "50px" }}
|
style={{ width: "50px" }}
|
||||||
@ -475,29 +502,25 @@ const EmployeeList = () => {
|
|||||||
{loading && (
|
{loading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="text-center py-5">
|
<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 />
|
<SpinnerLoader />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!loading && displayData?.length === 0 && !searchText ? (
|
||||||
{!loading &&
|
|
||||||
displayData?.length === 0 &&
|
|
||||||
(!searchText) ? (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="border-0 py-3">
|
<td colSpan={8} className="border-0 py-3">
|
||||||
<div className="py-4">
|
<div className="py-4">No Data Found</div>
|
||||||
No Data Found
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!loading &&
|
{!loading && displayData?.length === 0 && searchText ? (
|
||||||
displayData?.length === 0 &&
|
|
||||||
(searchText) ? (
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8} className="border-0 py-3">
|
<td colSpan={8} className="border-0 py-3">
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
@ -526,8 +549,9 @@ const EmployeeList = () => {
|
|||||||
className="text-heading text-truncate cursor-pointer"
|
className="text-heading text-truncate cursor-pointer"
|
||||||
>
|
>
|
||||||
<span className="fw-normal">
|
<span className="fw-normal">
|
||||||
{item.firstName} {item.middleName}{" "}
|
{capitalizeFirstLetter(item.firstName)}{" "}
|
||||||
{item.lastName}
|
{capitalizeFirstLetter(item.middleName)}{" "}
|
||||||
|
{capitalizeFirstLetter(item.lastName)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -540,7 +564,9 @@ const EmployeeList = () => {
|
|||||||
{item.email}
|
{item.email}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="d-block text-start text-muted fst-italic">NA</span>
|
<small className="d-block text-start text-muted small">
|
||||||
|
-
|
||||||
|
</small>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -561,7 +587,9 @@ const EmployeeList = () => {
|
|||||||
{item.joiningDate ? (
|
{item.joiningDate ? (
|
||||||
moment(item.joiningDate).format("DD-MMM-YYYY")
|
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>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@ -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 Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import {
|
import {
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
@ -68,7 +74,7 @@ const ProjectPage = () => {
|
|||||||
|
|
||||||
const handleToggleProject = (value) => {
|
const handleToggleProject = (value) => {
|
||||||
setCoreProjects(value);
|
setCoreProjects(value);
|
||||||
sessionStorage.setItem("whichProjectDisplay", String(value));
|
sessionStorage.setItem("whichProjectDisplay", value ? "true" : "false");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -100,8 +106,9 @@ const ProjectPage = () => {
|
|||||||
{/* Service Project Button */}
|
{/* Service Project Button */}
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => handleToggleProject(false)}
|
||||||
>
|
>
|
||||||
Service Project
|
Service Project
|
||||||
@ -109,8 +116,9 @@ const ProjectPage = () => {
|
|||||||
{/* Organization Project Button */}
|
{/* Organization Project Button */}
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => handleToggleProject(true)}
|
||||||
>
|
>
|
||||||
Infra Project
|
Infra Project
|
||||||
@ -138,8 +146,9 @@ const ProjectPage = () => {
|
|||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => setListView(false)}
|
||||||
title="Card View"
|
title="Card View"
|
||||||
>
|
>
|
||||||
@ -148,8 +157,9 @@ const ProjectPage = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => setListView(true)}
|
||||||
title="List View"
|
title="List View"
|
||||||
>
|
>
|
||||||
@ -164,12 +174,16 @@ const ProjectPage = () => {
|
|||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
>
|
>
|
||||||
<i
|
<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>
|
></i>
|
||||||
|
|
||||||
{selectedStatuses.length !== PROJECT_STATUS.length && (
|
{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={{
|
style={{
|
||||||
top: "-4px",
|
top: "-4px",
|
||||||
right: "-4px",
|
right: "-4px",
|
||||||
@ -186,7 +200,7 @@ const ProjectPage = () => {
|
|||||||
<ul
|
<ul
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
className="dropdown-menu show p-2 text-capitalize"
|
className="dropdown-menu show p-2 text-capitalize"
|
||||||
onMouseDown={(e) => e.stopPropagation()} // IMPORTANT
|
onMouseDown={(e) => e.stopPropagation()} // IMPORTANT
|
||||||
>
|
>
|
||||||
{PROJECT_STATUS.map(({ id, label }) => (
|
{PROJECT_STATUS.map(({ id, label }) => (
|
||||||
<li key={id}>
|
<li key={id}>
|
||||||
@ -200,7 +214,7 @@ const ProjectPage = () => {
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="form-check-label"
|
className="form-check-label"
|
||||||
onClick={(e) => e.stopPropagation()} // OPTIONAL
|
onClick={(e) => e.stopPropagation()} // OPTIONAL
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
@ -211,10 +225,7 @@ const ProjectPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(HasManageProject || !coreProjects) && (
|
||||||
|
|
||||||
|
|
||||||
{HasManageProject && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary btn-sm d-flex align-items-center my-2"
|
className="btn btn-primary btn-sm d-flex align-items-center my-2"
|
||||||
@ -223,9 +234,9 @@ const ProjectPage = () => {
|
|||||||
coreProjects
|
coreProjects
|
||||||
? setMangeProject({ isOpen: true, Project: null }) // Organization Project → Infra
|
? setMangeProject({ isOpen: true, Project: null }) // Organization Project → Infra
|
||||||
: setManageServiceProject({
|
: setManageServiceProject({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
Project: null,
|
Project: null,
|
||||||
}) // Service Project
|
}) // Service Project
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
|
|||||||
@ -29,25 +29,11 @@ const ProjectsDisplay = ({
|
|||||||
const [projectList, setProjectList] = useState([]);
|
const [projectList, setProjectList] = useState([]);
|
||||||
const debouncedSearch = useDebounce(searchTerm, 500);
|
const debouncedSearch = useDebounce(searchTerm, 500);
|
||||||
const { data, isLoading, isError, error } = useProjects(
|
const { data, isLoading, isError, error } = useProjects(
|
||||||
ITEMS_PER_PAGE,
|
9,
|
||||||
currentPage,
|
currentPage,
|
||||||
debouncedSearch
|
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) => {
|
const paginate = (page) => {
|
||||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@ -68,7 +54,7 @@ const ProjectsDisplay = ({
|
|||||||
.filter((statusId) => grouped[statusId])
|
.filter((statusId) => grouped[statusId])
|
||||||
.flatMap((statusId) =>
|
.flatMap((statusId) =>
|
||||||
grouped[statusId].sort((a, b) =>
|
grouped[statusId].sort((a, b) =>
|
||||||
a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase())
|
a?.shortName?.toLowerCase()?.localeCompare(b?.shortName?.toLowerCase())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
282
src/pages/reports/Reports.css
Normal 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);
|
||||||
|
}
|
||||||
@ -1,8 +1,69 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
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 = () => {
|
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;
|
export default Reports;
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { api } from "../utils/axiosClient";
|
import { api } from "../utils/axiosClient";
|
||||||
|
|
||||||
const GlobalRepository = {
|
const GlobalRepository = {
|
||||||
getDashboardProgressionData: ({ days = '', FromDate = '', projectId = '' }) => {
|
getDashboardProgressionData: ({
|
||||||
|
days = "",
|
||||||
|
FromDate = "",
|
||||||
|
projectId = "",
|
||||||
|
}) => {
|
||||||
let params;
|
let params;
|
||||||
if (projectId == null) {
|
if (projectId == null) {
|
||||||
params = new URLSearchParams({
|
params = new URLSearchParams({
|
||||||
@ -18,12 +22,13 @@ const GlobalRepository = {
|
|||||||
|
|
||||||
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
|
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) => {
|
getDashboardAttendanceData: (date, projectId) => {
|
||||||
|
return api.get(
|
||||||
return api.get(`/api/Dashboard/project-attendance/${projectId}?date=${date}`);
|
`/api/Dashboard/project-attendance/${projectId}?date=${date}`
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getDashboardProjectsCardData: () => {
|
getDashboardProjectsCardData: () => {
|
||||||
return api.get(`/api/Dashboard/projects`);
|
return api.get(`/api/Dashboard/projects`);
|
||||||
@ -44,7 +49,7 @@ const GlobalRepository = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getExpenseData: (projectId, startDate, endDate) => {
|
getExpenseData: (projectId, startDate, endDate) => {
|
||||||
let url = `api/Dashboard/expense/type`
|
let url = `api/Dashboard/expense/type`;
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
queryParams.push(`projectId=${projectId}`);
|
queryParams.push(`projectId=${projectId}`);
|
||||||
@ -62,10 +67,15 @@ const GlobalRepository = {
|
|||||||
return api.get(url);
|
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) => {
|
getExpenseDataByProject: (projectId, categoryId, months) => {
|
||||||
let url = `api/Dashboard/expense/monthly`
|
let url = `api/Dashboard/expense/monthly`;
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
queryParams.push(`projectId=${projectId}`);
|
queryParams.push(`projectId=${projectId}`);
|
||||||
@ -82,13 +92,26 @@ const GlobalRepository = {
|
|||||||
return api.get(url);
|
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 default GlobalRepository;
|
||||||
|
|
||||||
|
export const ReportsRepository = {
|
||||||
|
getDailyProgressByProject: (projectId,date) =>
|
||||||
|
api.get(`/api/Market/get/project/report/${projectId}?date=${date}`),
|
||||||
|
};
|
||||||
|
|||||||
64
src/services/encryption.jsx
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -24,6 +24,11 @@ export const AppColorconfig = {
|
|||||||
borderColor: "#eceef1",
|
borderColor: "#eceef1",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const AppColors = [
|
||||||
|
"primary","secodary","warning","info","danger"
|
||||||
|
]
|
||||||
export const getColorNameFromHex = (hex) => {
|
export const getColorNameFromHex = (hex) => {
|
||||||
const normalizedHex = hex?.replace(/'/g, "").toLowerCase();
|
const normalizedHex = hex?.replace(/'/g, "").toLowerCase();
|
||||||
const colors = AppColorconfig.colors;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import axiosRetry from "axios-retry";
|
|||||||
import showToast from "../services/toastService";
|
import showToast from "../services/toastService";
|
||||||
import { startSignalR, stopSignalR } from "../services/signalRService";
|
import { startSignalR, stopSignalR } from "../services/signalRService";
|
||||||
import { BASE_URL } from "./constants";
|
import { BASE_URL } from "./constants";
|
||||||
|
import { decryptResponse } from "../services/encryption";
|
||||||
const base_Url = BASE_URL;
|
const base_Url = BASE_URL;
|
||||||
|
|
||||||
export const axiosClient = axios.create({
|
export const axiosClient = axios.create({
|
||||||
@ -134,7 +135,7 @@ const apiRequest = async (method, url, data = {}, config = {}) => {
|
|||||||
params: method === "get" ? data : undefined,
|
params: method === "get" ? data : undefined,
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
return response.data;
|
return decryptResponse(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -179,11 +180,10 @@ export const api = {
|
|||||||
authRequired: true,
|
authRequired: true,
|
||||||
}),
|
}),
|
||||||
patch: (url, data = {}, customHeaders = {}) =>
|
patch: (url, data = {}, customHeaders = {}) =>
|
||||||
apiRequest("patch", url, data, {
|
apiRequest("patch", url, data, {
|
||||||
headers: { ...customHeaders },
|
headers: { ...customHeaders },
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Redirect helper
|
// Redirect helper
|
||||||
|
|||||||