Compare commits
1 Commits
main
...
migrate_ma
| Author | SHA1 | Date | |
|---|---|---|---|
| d463f37947 |
@ -3,16 +3,6 @@
|
|||||||
--bs-nav-link-font-size: 0.7375rem;
|
--bs-nav-link-font-size: 0.7375rem;
|
||||||
--bg-border-color :#f8f6f6
|
--bg-border-color :#f8f6f6
|
||||||
}
|
}
|
||||||
.offcanvas.offcanvas-wide {
|
|
||||||
width: 700px !important; /* adjust as needed */
|
|
||||||
}
|
|
||||||
.sticky-section {
|
|
||||||
position: sticky;
|
|
||||||
top: var(--sticky-top, 0px) !important;
|
|
||||||
z-index: 1025;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ===========================% Background_Colors %========================================================== */
|
/* ===========================% Background_Colors %========================================================== */
|
||||||
.bg-light-primary {
|
.bg-light-primary {
|
||||||
background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent);
|
background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent);
|
||||||
|
|||||||
16
public/assets/vendor/css/core.css
vendored
16
public/assets/vendor/css/core.css
vendored
@ -89,7 +89,7 @@
|
|||||||
);
|
);
|
||||||
--bs-root-font-size: 16px;
|
--bs-root-font-size: 16px;
|
||||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
--bs-body-font-size: 0.85rem;
|
--bs-body-font-size: 0.875rem;
|
||||||
--bs-body-font-weight: 400;
|
--bs-body-font-weight: 400;
|
||||||
--bs-body-line-height: 1.375;
|
--bs-body-line-height: 1.375;
|
||||||
--bs-body-color: #646e78;
|
--bs-body-color: #646e78;
|
||||||
@ -9060,7 +9060,7 @@ img[data-app-light-img][data-app-dark-img] {
|
|||||||
}
|
}
|
||||||
.table th {
|
.table th {
|
||||||
color: var(--bs-heading-color);
|
color: var(--bs-heading-color);
|
||||||
font-size: 0.8025rem;
|
font-size: 0.8125rem;
|
||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@ -20345,7 +20345,7 @@ li:not(:first-child) .dropdown-item,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fs-6 {
|
.fs-6 {
|
||||||
font-size: 0.8375rem !important;
|
font-size: 0.9375rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fs-tiny {
|
.fs-tiny {
|
||||||
@ -32560,7 +32560,9 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
|
|||||||
.bg-blue {
|
.bg-blue {
|
||||||
background-color:var(--bs-blue)
|
background-color:var(--bs-blue)
|
||||||
}
|
}
|
||||||
|
.text-blue{
|
||||||
|
color:var(--bs-blue)
|
||||||
|
}
|
||||||
.bg-indigo {
|
.bg-indigo {
|
||||||
background-color:var(--bs-indigo)
|
background-color:var(--bs-indigo)
|
||||||
}
|
}
|
||||||
@ -32573,9 +32575,3 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
|
|||||||
.text-red{
|
.text-red{
|
||||||
color:var(--bs-red)
|
color:var(--bs-red)
|
||||||
}
|
}
|
||||||
.text-blue{
|
|
||||||
color:var(--bs-blue)
|
|
||||||
}
|
|
||||||
.text-green{
|
|
||||||
color:var(--bs-green)
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
@ -5,22 +5,19 @@ import { useAuthModal, useModal } from "./hooks/useAuth";
|
|||||||
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
||||||
import ChangePasswordPage from "./pages/authentication/ChangePassword";
|
import ChangePasswordPage from "./pages/authentication/ChangePassword";
|
||||||
import NewCollection from "./components/collections/ManageCollection";
|
import NewCollection from "./components/collections/ManageCollection";
|
||||||
import ServiceProjectTeamAllocation from "./components/ServiceProject/ServiceProjectTeam/ServiceProjectTeamAllocation";
|
|
||||||
|
|
||||||
const ModalProvider = () => {
|
const ModalProvider = () => {
|
||||||
const { isOpen, onClose } = useOrganizationModal();
|
const { isOpen, onClose } = useOrganizationModal();
|
||||||
const { isOpen: isAuthOpen } = useAuthModal();
|
const { isOpen: isAuthOpen } = useAuthModal();
|
||||||
const { isOpen: isChangePass } = useModal("ChangePassword");
|
const {isOpen:isChangePass} = useModal("ChangePassword")
|
||||||
const { isOpen: isCollectionNew } = useModal("newCollection");
|
const {isOpen:isCollectionNew} = useModal("newCollection");
|
||||||
const { isOpen: isServiceTeamAllocation } = useModal("ServiceTeamAllocation");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && <OrganizationModal />}
|
{isOpen && <OrganizationModal />}
|
||||||
{isAuthOpen && <SwitchTenant />}
|
{isAuthOpen && <SwitchTenant />}
|
||||||
{isChangePass && <ChangePasswordPage />}
|
{isChangePass && <ChangePasswordPage /> }
|
||||||
{isCollectionNew && <NewCollection />}
|
{isCollectionNew && <NewCollection/>}
|
||||||
{isServiceTeamAllocation && <ServiceProjectTeamAllocation />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense";
|
import { useExpenseTransactions } from "../../hooks/useExpense";
|
||||||
import Error from "../common/Error";
|
import Error from "../common/Error";
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
import Loader, { SpinnerLoader } from "../common/Loader";
|
import Loader, { SpinnerLoader } from "../common/Loader";
|
||||||
@ -11,10 +11,11 @@ import { employee } from "../../data/masters";
|
|||||||
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
|
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
|
||||||
import { formatFigure } from "../../utils/appUtils";
|
import { formatFigure } from "../../utils/appUtils";
|
||||||
|
|
||||||
const AdvancePaymentList = ({ employeeId, searchString }) => {
|
const AdvancePaymentList = ({ employeeId }) => {
|
||||||
const { setBalance } = useAdvancePaymentContext();
|
const { setBalance } = useAdvancePaymentContext();
|
||||||
const { data, isError, isLoading, error, isFetching } =
|
const { data, isError, isLoading, error, isFetching } =
|
||||||
useExpenseTransactions(employeeId, { enabled: !!employeeId });
|
useExpenseTransactions(employeeId, { enabled: !!employeeId });
|
||||||
|
|
||||||
const records = Array.isArray(data) ? data : [];
|
const records = Array.isArray(data) ? data : [];
|
||||||
|
|
||||||
let currentBalance = 0;
|
let currentBalance = 0;
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Avatar from "../../components/common/Avatar"; // <-- ADD THIS
|
|
||||||
import { useExpenseAllTransactionsList } from '../../hooks/useExpense';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { formatFigure } from '../../utils/appUtils';
|
|
||||||
|
|
||||||
const AdvancePaymentList1 = ({ searchString }) => {
|
|
||||||
|
|
||||||
const { data, isError, isLoading, error } =
|
|
||||||
useExpenseAllTransactionsList(searchString);
|
|
||||||
|
|
||||||
const rows = data || [];
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "employee",
|
|
||||||
label: "Employee Name",
|
|
||||||
align: "text-start",
|
|
||||||
customRender: (r) => (
|
|
||||||
<div className="d-flex align-items-center gap-2" onClick={() => navigate(`/advance-payment/${r.id}`)}
|
|
||||||
style={{ cursor: "pointer" }}>
|
|
||||||
<Avatar firstName={r.firstName} lastName={r.lastName} />
|
|
||||||
|
|
||||||
<span className="fw-medium">
|
|
||||||
{r.firstName} {r.lastName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "jobRoleName",
|
|
||||||
label: "Job Role",
|
|
||||||
align: "text-start",
|
|
||||||
customRender: (r) => (
|
|
||||||
<span className="fw-semibold">
|
|
||||||
{r.jobRoleName}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "balanceAmount",
|
|
||||||
label: "Balance (₹)",
|
|
||||||
align: "text-end",
|
|
||||||
customRender: (r) => (
|
|
||||||
<span className="fw-semibold fs-6">
|
|
||||||
{formatFigure(r.balanceAmount, {
|
|
||||||
// type: "currency",
|
|
||||||
currency: "INR",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isLoading) return <p className="text-center py-4">Loading...</p>;
|
|
||||||
if (isError) return <p className="text-center py-4 text-danger">{error.message}</p>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card-datatable" id="payment-request-table">
|
|
||||||
<div className="mx-2">
|
|
||||||
<table className="table border-top dataTable text-nowrap align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{columns.map((col) => (
|
|
||||||
<th key={col.key} className={`sorting ${col.align}`}>
|
|
||||||
{col.label}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{rows.length > 0 ? (
|
|
||||||
rows.map((row) => (
|
|
||||||
<tr key={row.id} className="align-middle" style={{ height: "40px" }}>
|
|
||||||
{columns.map((col) => (
|
|
||||||
<td key={col.key} className={`d-table-cell ${col.align} py-3`}>
|
|
||||||
{col.customRender
|
|
||||||
? col.customRender(row)
|
|
||||||
: col.getValue(row)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={columns.length} className="text-center border-0 py-3">
|
|
||||||
No Employees Found
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AdvancePaymentList1;
|
|
||||||
@ -54,7 +54,6 @@ const TaskReportFilterPanel = ({ handleFilter }) => {
|
|||||||
<div className="mb-3 w-100">
|
<div className="mb-3 w-100">
|
||||||
<label className="fw-semibold">Choose Date Range:</label>
|
<label className="fw-semibold">Choose Date Range:</label>
|
||||||
<DateRangePicker1
|
<DateRangePicker1
|
||||||
className="w-100"
|
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
startField="dateFrom"
|
startField="dateFrom"
|
||||||
endField="dateTo"
|
endField="dateTo"
|
||||||
|
|||||||
@ -202,7 +202,6 @@ const TaskReportList = () => {
|
|||||||
<span>
|
<span>
|
||||||
Total Pending{" "}
|
Total Pending{" "}
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
id="total_pending_task"
|
|
||||||
title="Total Pending Task"
|
title="Total Pending Task"
|
||||||
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
||||||
>
|
>
|
||||||
@ -214,7 +213,6 @@ const TaskReportList = () => {
|
|||||||
<span>
|
<span>
|
||||||
Reported/Planned{" "}
|
Reported/Planned{" "}
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
id="reportes_and_planned_task"
|
|
||||||
title="Reported and Planned Task"
|
title="Reported and Planned Task"
|
||||||
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -100,9 +100,9 @@ const AttendanceOverview = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white px-4 rounded shadow d-flex flex-column h-100">
|
<div className="bg-white p-4 rounded shadow d-flex flex-column h-100">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="d-flex mt-2 justify-content-between align-items-center mb-3">
|
<div className="d-flex 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-bold">Attendance Overview</h5>
|
||||||
<p className="card-subtitle">Role-wise present count</p>
|
<p className="card-subtitle">Role-wise present count</p>
|
||||||
@ -12,11 +12,11 @@ import Teams from "./Teams";
|
|||||||
import TasksCard from "./Tasks";
|
import TasksCard from "./Tasks";
|
||||||
import ProjectCompletionChart from "./ProjectCompletionChart";
|
import ProjectCompletionChart from "./ProjectCompletionChart";
|
||||||
import ProjectProgressChart from "./ProjectProgressChart";
|
import ProjectProgressChart from "./ProjectProgressChart";
|
||||||
import AttendanceOverview from "./AttendanceOverview";
|
import ProjectOverview from "../Project/ProjectOverview";
|
||||||
|
import AttendanceOverview from "./AttendanceChart";
|
||||||
import ExpenseAnalysis from "./ExpenseAnalysis";
|
import ExpenseAnalysis from "./ExpenseAnalysis";
|
||||||
import ExpenseStatus from "./ExpenseStatus";
|
import ExpenseStatus from "./ExpenseStatus";
|
||||||
import ExpenseByProject from "./ExpenseByProject";
|
import ExpenseByProject from "./ExpenseByProject";
|
||||||
import ProjectStatistics from "../Project/ProjectStatistics";
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
|
|
||||||
@ -29,16 +29,16 @@ const Dashboard = () => {
|
|||||||
<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-4">
|
||||||
<Projects />
|
<Projects />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||||
<Teams />
|
<Teams />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
<div className={`${!isAllProjectsSelected ? "col-sm-6 col-lg-6" : "col-sm-6 col-lg-4"}`}>
|
||||||
<TasksCard />
|
<TasksCard/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAllProjectsSelected && (
|
{isAllProjectsSelected && (
|
||||||
@ -46,31 +46,32 @@ const Dashboard = () => {
|
|||||||
<ProjectCompletionChart />
|
<ProjectCompletionChart />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isAllProjectsSelected && (
|
||||||
|
<div className="col-xxl-6 col-lg-6">
|
||||||
|
<ProjectOverview />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="col-xxl-6 col-lg-6">
|
<div className="col-xxl-6 col-lg-6">
|
||||||
<ProjectProgressChart />
|
<ProjectProgressChart />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-xl-8">
|
||||||
|
<div className="card h-100">
|
||||||
|
<ExpenseAnalysis />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-xl-4 col-md-6">
|
||||||
|
<div className="card h-100">
|
||||||
|
<ExpenseStatus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{!isAllProjectsSelected && (
|
{!isAllProjectsSelected && (
|
||||||
<div className="col-12 col-md-6 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-xxl-4 col-lg-4">
|
|
||||||
<ProjectStatistics />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="col-12 col-xl-4 col-md-6">
|
|
||||||
<div className="card ">
|
|
||||||
<ExpenseStatus />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-xl-8">
|
|
||||||
<div className="card h-100">
|
|
||||||
<ExpenseAnalysis />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<ExpenseByProject />
|
<ExpenseByProject />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import { FormProvider, useForm } from "react-hook-form";
|
|||||||
import { formatCurrency, localToUtc } from "../../utils/appUtils";
|
import { formatCurrency, localToUtc } from "../../utils/appUtils";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
import { SpinnerLoader } from "../common/Loader";
|
||||||
import flatColors from "../Charts/flatColor";
|
|
||||||
|
|
||||||
const ExpenseAnalysis = () => {
|
const ExpenseAnalysis = () => {
|
||||||
const projectId = useSelectedProject();
|
const projectId = useSelectedProject();
|
||||||
const [projectName, setProjectName] = useState("All Project");
|
const [projectName, setProjectName] = useState("All Project");
|
||||||
const { projectNames } = useProjectName();
|
const { projectNames, loading } = useProjectName();
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues: { startDate: "", endDate: "" },
|
defaultValues: { startDate: "", endDate: "" },
|
||||||
@ -51,7 +50,7 @@ const ExpenseAnalysis = () => {
|
|||||||
labels,
|
labels,
|
||||||
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: ["#7367F0", "#28C76F", "#FF9F43", "#EA5455", "#00CFE8", "#FF78B8"],
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
pie: {
|
pie: {
|
||||||
donut: {
|
donut: {
|
||||||
@ -80,19 +79,22 @@ const ExpenseAnalysis = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<div className="card-header d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2">
|
<div className="card-header d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center gap-2">
|
||||||
<div className="text-start">
|
<div className="text-start ">
|
||||||
<h5 className="mb-1 card-title">Expense Breakdown</h5>
|
<h5 className="mb-1 card-title">Expense Breakdown</h5>
|
||||||
|
{/* <p className="card-subtitle mb-0">Category Wise Expense Breakdown</p> */}
|
||||||
<p className="card-subtitle m-0">{projectName}</p>
|
<p className="card-subtitle m-0">{projectName}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-end text-sm-end">
|
<div className="text-end text-sm-end ">
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<DateRangePicker1 />
|
<DateRangePicker1 />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Card body */}
|
||||||
<div className="card-body position-relative">
|
<div className="card-body position-relative">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div
|
<div
|
||||||
@ -112,6 +114,7 @@ const ExpenseAnalysis = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{!isLoading && report.length > 0 && (
|
{!isLoading && report.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{isFetching && (
|
{isFetching && (
|
||||||
@ -120,59 +123,50 @@ const ExpenseAnalysis = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="row">
|
<div className="d-flex justify-content-center mb-3">
|
||||||
{/* Chart Column */}
|
<Chart
|
||||||
<div className="col-12 col-lg-6 d-flex justify-content-center mt-5 mb-3 mb-lg-0">
|
options={donutOptions}
|
||||||
<Chart
|
series={series}
|
||||||
options={donutOptions}
|
type="donut"
|
||||||
series={series}
|
width="100%"
|
||||||
type="donut"
|
height={320}
|
||||||
width="70%"
|
/>
|
||||||
height={320}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Data/Legend Column */}
|
<div className="mb-2 w-100">
|
||||||
<div className="col-12 mt-6 col-lg-6">
|
<div className="row g-2">
|
||||||
<div className="row g-4">
|
{report.map((item, idx) => (
|
||||||
{report.map((item, idx) => (
|
<div
|
||||||
<div
|
className="col-12 col-sm-6 d-flex align-items-start"
|
||||||
className="col-6"
|
key={idx}
|
||||||
key={idx}
|
>
|
||||||
style={{
|
<div className="avatar me-2">
|
||||||
borderLeft: `3px solid ${flatColors[idx % flatColors.length]}`,
|
<span
|
||||||
}}
|
className="avatar-initial rounded-2"
|
||||||
>
|
style={{
|
||||||
<div className="d-flex flex-column text-start">
|
backgroundColor:
|
||||||
<small
|
donutOptions.colors[idx % donutOptions.colors.length],
|
||||||
className="fw-semibold text-wrap text-dark"
|
}}
|
||||||
style={{
|
>
|
||||||
fontSize: "0.8rem",
|
<i className="bx bx-receipt fs-4"></i>
|
||||||
whiteSpace: "normal",
|
</span>
|
||||||
wordBreak: "break-word",
|
|
||||||
lineHeight: "1.2",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.projectName}
|
|
||||||
</small>
|
|
||||||
<span
|
|
||||||
className="fw-semibold text-muted"
|
|
||||||
style={{
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatCurrency(item.totalApprovedAmount)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="d-flex flex-column gap-1 text-start">
|
||||||
))}
|
<small className="fw-semibold">{item.projectName}</small>
|
||||||
</div>
|
<span className="fw-semibold text-muted ms-1">
|
||||||
|
{formatCurrency(item.totalApprovedAmount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,7 +92,7 @@ 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-flex justify-content-start 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>
|
<p className="card-subtitle m-0">{projectName}</p>
|
||||||
|
|||||||
@ -103,7 +103,7 @@ const ExpenseStatus = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<small
|
<small
|
||||||
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-xl"
|
className={`text-royalblue ${countDigit(item?.count || 0) >= 3 ? "text-xl" : "text-2xl"
|
||||||
} text-gray-500`}
|
} text-gray-500`}
|
||||||
>
|
>
|
||||||
{item?.count || 0}
|
{item?.count || 0}
|
||||||
@ -122,7 +122,7 @@ const ExpenseStatus = () => {
|
|||||||
{isManageExpense && (
|
{isManageExpense && (
|
||||||
<div
|
<div
|
||||||
className="d-flex justify-content-between align-items-center cursor-pointer"
|
className="d-flex justify-content-between align-items-center cursor-pointer"
|
||||||
onClick={() => handleNavigate(EXPENSE_STATUS.payment_processed)}
|
onClick={() => handleNavigate(EXPENSE_STATUS.process_pending)}
|
||||||
>
|
>
|
||||||
<div className="d-block">
|
<div className="d-block">
|
||||||
<span
|
<span
|
||||||
@ -137,7 +137,7 @@ const ExpenseStatus = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-xl" : "text-3xl"
|
className={`text-end text-royalblue ${countDigit(data?.totalAmount || 0) > 3 ? "text-" : "text-3xl"
|
||||||
} text-md`}
|
} text-md`}
|
||||||
>
|
>
|
||||||
{formatCurrency(data?.totalAmount || 0)}
|
{formatCurrency(data?.totalAmount || 0)}
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
import HorizontalBarChart from "../Charts/HorizontalBarChart";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
|
||||||
import { useProjectCompletionStatus } from "../../hooks/useDashboard_Data";
|
|
||||||
|
|
||||||
const ProjectCompletionChart = () => {
|
const ProjectCompletionChart = () => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const { data: projects = [], isLoading: loading, isError, error } = useProjects();
|
||||||
const {
|
|
||||||
data: projects,
|
|
||||||
isLoading: loading,
|
// Bar chart logic
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useProjectCompletionStatus();
|
|
||||||
const projectNames = projects?.map((p) => p.name) || [];
|
const projectNames = projects?.map((p) => p.name) || [];
|
||||||
const projectProgress =
|
const projectProgress =
|
||||||
projects?.map((p) => {
|
projects?.map((p) => {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import Label from "../common/Label";
|
|||||||
const ManageContact = ({ contactId, closeModal }) => {
|
const ManageContact = ({ contactId, closeModal }) => {
|
||||||
// fetch master data
|
// fetch master data
|
||||||
const { buckets, loading: bucketsLoaging } = useBuckets();
|
const { buckets, loading: bucketsLoaging } = useBuckets();
|
||||||
const { data: projects, loading: projectLoading } = useProjects();
|
const { data:projects, loading: projectLoading } = useProjects();
|
||||||
const { contactCategory, loading: contactCategoryLoading } =
|
const { contactCategory, loading: contactCategoryLoading } =
|
||||||
useContactCategory();
|
useContactCategory();
|
||||||
const { organizationList } = useOrganization();
|
const { organizationList } = useOrganization();
|
||||||
@ -205,12 +205,12 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
<Label htmlFor={"organization"} required>
|
<Label htmlFor={"organization"} required>
|
||||||
Organization
|
Organization
|
||||||
</Label>
|
</Label>
|
||||||
<InputSuggestions
|
<InputSuggestions
|
||||||
organizationList={organizationList}
|
organizationList={organizationList}
|
||||||
value={watch("organization") || ""}
|
value={watch("organization") || ""}
|
||||||
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
|
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
|
||||||
error={errors.organization?.message}
|
error={errors.organization?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -408,7 +408,6 @@ const ManageContact = ({ contactId, closeModal }) => {
|
|||||||
label="Tags"
|
label="Tags"
|
||||||
options={contactTags}
|
options={contactTags}
|
||||||
isRequired={true}
|
isRequired={true}
|
||||||
placeholder="Enter Tag"
|
|
||||||
/>
|
/>
|
||||||
{errors.tags && (
|
{errors.tags && (
|
||||||
<small className="danger-text">{errors.tags.message}</small>
|
<small className="danger-text">{errors.tags.message}</small>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import EmpOverview from "./EmpOverview";
|
import EmpOverview from "./EmpOverview";
|
||||||
import { useProjectsAllocationByEmployee } from "../../hooks/useProjects";
|
import { useProjectsAllocationByEmployee } from "../../hooks/useProjects";
|
||||||
import EmpReportingManager from "./EmpReportingManager";
|
|
||||||
|
|
||||||
const EmpDashboard = ({ profile }) => {
|
const EmpDashboard = ({ profile }) => {
|
||||||
const {
|
const {
|
||||||
@ -17,7 +16,6 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
{" "}
|
{" "}
|
||||||
<EmpOverview profile={profile}></EmpOverview>
|
<EmpOverview profile={profile}></EmpOverview>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col col-sm-6 pt-5">
|
<div className="col col-sm-6 pt-5">
|
||||||
<div className="card ">
|
<div className="card ">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
@ -82,13 +80,6 @@ const EmpDashboard = ({ profile }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-sm-6 pt-5">
|
|
||||||
<EmpReportingManager
|
|
||||||
employeeId={profile?.id}
|
|
||||||
employee={profile}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useOrganizationHierarchy } from "../../hooks/useEmployees";
|
|
||||||
import GlobalModel from "../common/GlobalModel";
|
|
||||||
import ManageReporting from "./ManageReporting";
|
|
||||||
import Avatar from "../common/Avatar";
|
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
|
||||||
|
|
||||||
const EmpReportingManager = ({ employeeId, employee }) => {
|
|
||||||
const { data, isLoading } = useOrganizationHierarchy(employeeId);
|
|
||||||
const [showManageReportingModal, setShowManageReportingModal] = useState(false);
|
|
||||||
|
|
||||||
if (isLoading)
|
|
||||||
return (
|
|
||||||
<div className="d-flex justify-content-center py-5">
|
|
||||||
<SpinnerLoader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Safe access to primary and secondary managers
|
|
||||||
const primaryManager = data?.find((d) => d.isPrimary)?.reportTo;
|
|
||||||
const secondaryManagers = data?.filter((d) => !d.isPrimary).map((d) => d.reportTo) || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-12 mb-4">
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-body">
|
|
||||||
<small className="card-text text-uppercase text-body-secondary small d-block text-start mb-3">
|
|
||||||
Reporting Manager
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<div className="text-start">
|
|
||||||
{/* Primary Manager */}
|
|
||||||
<div className="row mb-2 align-items-start">
|
|
||||||
<div className="col-auto text-end pe-3"><i className="bx bx-user-circle me-1"></i>Primary Manager<span style={{ marginLeft: "50px" }}>:</span></div>
|
|
||||||
<div className="col text-wrap">
|
|
||||||
{primaryManager
|
|
||||||
? `${primaryManager.firstName || ""} ${primaryManager.lastName || ""}`
|
|
||||||
: "NA"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Secondary Managers */}
|
|
||||||
{secondaryManagers?.length > 0 && (
|
|
||||||
<div className="row mb-3 align-items-start">
|
|
||||||
<div className="col-auto text-end pe-3"><i className="bx bx-group me-1"></i>Secondary Managers<span style={{ marginLeft: "25px" }}>:</span></div>
|
|
||||||
<div className="col text-wrap">
|
|
||||||
{secondaryManagers
|
|
||||||
.map((m) => `${m.firstName || ""} ${m.lastName || ""}`)
|
|
||||||
.join(", ")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Manage Reporting Button */}
|
|
||||||
<div className="mt-5 text-end" >
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
onClick={() => setShowManageReportingModal(true)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-network-chart me-1"></i> Manage Reporting
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Manage Reporting Modal */}
|
|
||||||
{showManageReportingModal && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={showManageReportingModal}
|
|
||||||
closeModal={() => setShowManageReportingModal(false)}
|
|
||||||
>
|
|
||||||
<ManageReporting
|
|
||||||
employee={employee}
|
|
||||||
employeeId={employeeId}
|
|
||||||
onClosed={() => setShowManageReportingModal(false)}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmpReportingManager;
|
|
||||||
|
|
||||||
@ -3,8 +3,8 @@ import { z } from "zod"
|
|||||||
|
|
||||||
const mobileNumberRegex = /^[0-9]\d{9}$/;
|
const mobileNumberRegex = /^[0-9]\d{9}$/;
|
||||||
|
|
||||||
export const employeeSchema =
|
export const employeeSchema =
|
||||||
z.object({
|
z.object({
|
||||||
firstName: z.string().min(1, { message: "First Name is required" }),
|
firstName: z.string().min(1, { message: "First Name is required" }),
|
||||||
middleName: z.string().optional(),
|
middleName: z.string().optional(),
|
||||||
lastName: z.string().min(1, { message: "Last Name is required" }),
|
lastName: z.string().min(1, { message: "Last Name is required" }),
|
||||||
@ -90,46 +90,35 @@ export const employeeSchema =
|
|||||||
.min(1, { message: "Phone Number is required" })
|
.min(1, { message: "Phone Number is required" })
|
||||||
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
||||||
jobRoleId: z.string().min(1, { message: "Role is required" }),
|
jobRoleId: z.string().min(1, { message: "Role is required" }),
|
||||||
organizationId: z.string().min(1, { message: "Organization is required" }),
|
organizationId:z.string().min(1,{message:"Organization is required"}),
|
||||||
hasApplicationAccess: z.boolean().default(false),
|
hasApplicationAccess:z.boolean().default(false),
|
||||||
}).refine((data) => {
|
}).refine((data) => {
|
||||||
if (data.hasApplicationAccess) {
|
if (data.hasApplicationAccess) {
|
||||||
return data.email && data.email.trim() !== "";
|
return data.email && data.email.trim() !== "";
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, {
|
}, {
|
||||||
message: "Email is required when employee has access",
|
message: "Email is required when employee has access",
|
||||||
path: ["email"],
|
path: ["email"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const defatEmployeeObj = {
|
export const defatEmployeeObj = {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
middleName: "",
|
middleName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
currentAddress: "",
|
currentAddress: "",
|
||||||
birthDate: "",
|
birthDate: "",
|
||||||
joiningDate: "",
|
joiningDate: "",
|
||||||
emergencyPhoneNumber: "",
|
emergencyPhoneNumber: "",
|
||||||
emergencyContactPerson: "",
|
emergencyContactPerson: "",
|
||||||
aadharNumber: "",
|
aadharNumber: "",
|
||||||
gender: "",
|
gender: "",
|
||||||
panNumber: "",
|
panNumber: "",
|
||||||
permanentAddress: "",
|
permanentAddress: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
jobRoleId: null,
|
jobRoleId: null,
|
||||||
organizationId: "",
|
organizationId:"",
|
||||||
hasApplicationAccess: false
|
hasApplicationAccess:false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ManageReportingSchema = z.object({
|
|
||||||
primaryNotifyTo: z.array(z.string()).min(1, "Primary Reporting Manager is required"),
|
|
||||||
secondaryNotifyTo: z.array(z.string()).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const defaultManageReporting = {
|
|
||||||
primaryNotifyTo: [],
|
|
||||||
secondaryNotifyTo: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import Label from "../common/Label";
|
|
||||||
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
|
||||||
import { useManageEmployeeHierarchy, useOrganizationHierarchy } from "../../hooks/useEmployees";
|
|
||||||
import { ManageReportingSchema, defaultManageReporting } from "./EmployeeSchema";
|
|
||||||
import Avatar from "../common/Avatar";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
const ManageReporting = ({ onClosed, employee, employeeId }) => {
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
watch,
|
|
||||||
} = useForm({
|
|
||||||
resolver: zodResolver(ManageReportingSchema),
|
|
||||||
defaultValues: defaultManageReporting,
|
|
||||||
});
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { data, isLoading } = useOrganizationHierarchy(employeeId);
|
|
||||||
|
|
||||||
// mutation hook
|
|
||||||
const { mutate: manageHierarchy, isPending } = useManageEmployeeHierarchy(
|
|
||||||
employeeId,
|
|
||||||
onClosed
|
|
||||||
);
|
|
||||||
const primaryValue = watch("primaryNotifyTo");
|
|
||||||
const secondaryValue = watch("secondaryNotifyTo");
|
|
||||||
|
|
||||||
// Prefill hierarchy data
|
|
||||||
useEffect(() => {
|
|
||||||
if (data && Array.isArray(data)) {
|
|
||||||
const primary = data.find((r) => r.isPrimary);
|
|
||||||
const secondary = data.filter((r) => !r.isPrimary);
|
|
||||||
|
|
||||||
reset({
|
|
||||||
primaryNotifyTo: primary ? [primary.reportTo.id] : [],
|
|
||||||
secondaryNotifyTo: secondary.map((r) => r.reportTo.id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
reset(defaultManageReporting);
|
|
||||||
onClosed();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
// Build set of currently selected IDs
|
|
||||||
const selectedIds = new Set([
|
|
||||||
...(formData.primaryNotifyTo || []),
|
|
||||||
...(formData.secondaryNotifyTo || []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Build payload including previous assignments, setting isActive true/false accordingly
|
|
||||||
const payload = (data || []).map((item) => ({
|
|
||||||
reportToId: item.reportTo.id,
|
|
||||||
isPrimary: item.isPrimary,
|
|
||||||
isActive: selectedIds.has(item.reportTo.id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add any new IDs that were not previously assigned
|
|
||||||
if (formData.primaryNotifyTo?.length) {
|
|
||||||
const primaryId = formData.primaryNotifyTo[0];
|
|
||||||
if (!data?.some((d) => d.reportTo.id === primaryId)) {
|
|
||||||
payload.push({
|
|
||||||
reportToId: primaryId,
|
|
||||||
isPrimary: true,
|
|
||||||
isActive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.secondaryNotifyTo?.length) {
|
|
||||||
formData.secondaryNotifyTo.forEach((id) => {
|
|
||||||
if (!data?.some((d) => d.reportTo.id === id)) {
|
|
||||||
payload.push({
|
|
||||||
reportToId: id,
|
|
||||||
isPrimary: false,
|
|
||||||
isActive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
manageHierarchy(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
handleClose();
|
|
||||||
navigate(`/employee/${employee.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
|
|
||||||
<h5 className="m-0 py-1 mb-4">Reporting Manager</h5>
|
|
||||||
|
|
||||||
{/* Employee Info */}
|
|
||||||
<div className="d-flex align-items-center justify-content-start mb-4 ps-0">
|
|
||||||
<div className="me-1 mb-1">
|
|
||||||
<Avatar size="sm" firstName={employee.firstName} lastName={employee.lastName} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Employee Name + Role */}
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<span className="fw-semibold text-dark fs-6 me-1 cursor-pointer" onClick={handleClick}>
|
|
||||||
{`${employee.firstName || ""} ${employee.middleName || ""} ${employee.lastName || ""}`.trim() || "Employee Name NA"}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* External Link Icon (Navigate to Employee Profile) */}
|
|
||||||
<i
|
|
||||||
className="bx bx-link-external text-primary"
|
|
||||||
style={{ fontSize: "1rem", cursor: "pointer" }}
|
|
||||||
title="View Profile"
|
|
||||||
onClick={handleClick}
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-start">
|
|
||||||
{employee.jobRole && (
|
|
||||||
<small className="text-muted">{employee.jobRole}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Primary Reporting Manager */}
|
|
||||||
<div className="mb-4 text-start">
|
|
||||||
<Label className="form-label" required>
|
|
||||||
Primary Reporting Manager
|
|
||||||
</Label>
|
|
||||||
<PmsEmployeeInputTag
|
|
||||||
control={control}
|
|
||||||
name="primaryNotifyTo"
|
|
||||||
placeholder={primaryValue?.length > 0 ? "" : "Search and select primary manager"}
|
|
||||||
forAll={true}
|
|
||||||
disabled={primaryValue?.length > 0}
|
|
||||||
/>
|
|
||||||
{errors.primaryNotifyTo && (
|
|
||||||
<div className="text-danger small mt-1">
|
|
||||||
{errors.primaryNotifyTo.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Secondary Reporting Manager */}
|
|
||||||
<div className="mb-4 text-start">
|
|
||||||
<Label className="form-label">Secondary Reporting Manager</Label>
|
|
||||||
<PmsEmployeeInputTag
|
|
||||||
control={control}
|
|
||||||
name="secondaryNotifyTo"
|
|
||||||
placeholder="Search and add secondary managers"
|
|
||||||
forAll={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<div className="d-flex justify-content-end gap-3 mt-3 mb-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="btn btn-label-secondary btn-sm"
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
{isPending ? "Saving..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageReporting;
|
|
||||||
@ -20,7 +20,7 @@ const Filelist = ({ files, removeFile, expenseToEdit,sm=6,md=4 }) => {
|
|||||||
<i
|
<i
|
||||||
className={`bx ${getIconByFileType(
|
className={`bx ${getIconByFileType(
|
||||||
file?.contentType
|
file?.contentType
|
||||||
)} fs-3 `}
|
)} fs-3 text-primary`}
|
||||||
style={{ minWidth: "30px" }}
|
style={{ minWidth: "30px" }}
|
||||||
></i>
|
></i>
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
|
||||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import useMaster, {
|
import useMaster, {
|
||||||
@ -32,11 +32,6 @@ import Label from "../common/Label";
|
|||||||
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
import EmployeeSearchInput from "../common/EmployeeSearchInput";
|
||||||
import Filelist from "./Filelist";
|
import Filelist from "./Filelist";
|
||||||
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
import { DEFAULT_CURRENCY } from "../../utils/constants";
|
||||||
import SelectEmployeeServerSide, {
|
|
||||||
SelectProjectField,
|
|
||||||
} from "../common/Forms/SelectFieldServerSide";
|
|
||||||
import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject";
|
|
||||||
import { AppFormController } from "../../hooks/appHooks/useAppForm";
|
|
||||||
|
|
||||||
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
||||||
const {
|
const {
|
||||||
@ -45,7 +40,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
error: ExpenseErrorLoad,
|
error: ExpenseErrorLoad,
|
||||||
} = useExpense(expenseToEdit);
|
} = useExpense(expenseToEdit);
|
||||||
const [expenseCategory, setExpenseCategory] = useState();
|
const [expenseCategory, setExpenseCategory] = useState();
|
||||||
const [selectedEmployees, setSelectedEmployees] = useState([]);
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
expenseCategories,
|
expenseCategories,
|
||||||
@ -89,11 +83,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
loading: StatusLoadding,
|
loading: StatusLoadding,
|
||||||
error: stausError,
|
error: stausError,
|
||||||
} = useExpenseStatus();
|
} = useExpenseStatus();
|
||||||
// const {
|
const {
|
||||||
// data: employees,
|
data: employees,
|
||||||
// isLoading: EmpLoading,
|
isLoading: EmpLoading,
|
||||||
// isError: isEmployeeError,
|
isError: isEmployeeError,
|
||||||
// } = useEmployeesNameByProject(selectedproject);
|
} = useEmployeesNameByProject(selectedproject);
|
||||||
|
|
||||||
const files = watch("billAttachments");
|
const files = watch("billAttachments");
|
||||||
const onFileChange = async (e) => {
|
const onFileChange = async (e) => {
|
||||||
@ -156,14 +150,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate: AllocationTeam, isPending1 } =
|
|
||||||
useAllocationServiceProjectTeam(() => {
|
|
||||||
setSelectedEmployees([]);
|
|
||||||
setSeletingEmp({
|
|
||||||
employee: null,
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (expenseToEdit && data) {
|
if (expenseToEdit && data) {
|
||||||
reset({
|
reset({
|
||||||
@ -194,7 +180,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
: [],
|
: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, reset]);
|
}, [data, reset, employees]);
|
||||||
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
|
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
|
||||||
handleClose()
|
handleClose()
|
||||||
);
|
);
|
||||||
@ -237,7 +223,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
</h5>
|
</h5>
|
||||||
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
{/* <div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label className="form-label" required>
|
<Label className="form-label" required>
|
||||||
Select Project
|
Select Project
|
||||||
</Label>
|
</Label>
|
||||||
@ -259,23 +245,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
)}
|
)}
|
||||||
</div> */}
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<SelectProjectField
|
|
||||||
label="Project"
|
|
||||||
required
|
|
||||||
placeholder="Select Project"
|
|
||||||
value={watch("projectId")}
|
|
||||||
onChange={(val) =>
|
|
||||||
setValue("projectId", val, {
|
|
||||||
shouldDirty: true,
|
|
||||||
shouldValidate: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.projectId && (
|
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
@ -338,28 +307,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 text-start">
|
<div className="col-12 col-md-6 text-start">
|
||||||
{/* <Label className="form-label" required>
|
<Label className="form-label" required>
|
||||||
Paid By{" "}
|
Paid By{" "}
|
||||||
</Label> */}
|
</Label>
|
||||||
{/* <EmployeeSearchInput
|
<EmployeeSearchInput
|
||||||
control={control}
|
control={control}
|
||||||
name="paidById"
|
name="paidById"
|
||||||
projectId={null}
|
projectId={null}
|
||||||
forAll={true}
|
forAll={expenseToEdit ? true : false}
|
||||||
/> */}
|
|
||||||
|
|
||||||
<AppFormController
|
|
||||||
name="paidById"
|
|
||||||
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<SelectEmployeeServerSide
|
|
||||||
label="Paid By" required
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
isFullObject={false} // because using ID
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -468,9 +423,10 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.gstNumber.message}</small>
|
<small className="danger-text">{errors.gstNumber.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6 text-start ">
|
<div className="col-md-6 text-start ">
|
||||||
<Label htmlFor="currencyId" className="form-label" required>
|
<Label htmlFor="currencyId" className="form-label" required>
|
||||||
Select Currency
|
Select Currency
|
||||||
</Label>
|
</Label>
|
||||||
@ -496,26 +452,24 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.currencyId.message}</small>
|
<small className="danger-text">{errors.currencyId.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{expenseCategory?.noOfPersonsRequired && (
|
{expenseCategory?.noOfPersonsRequired && (
|
||||||
<div className="col-md-6 text-start">
|
<div className="col-md-6 text-start">
|
||||||
<Label className="form-label" required>
|
<Label className="form-label" required>No. of Persons</Label>
|
||||||
No. of Persons
|
<input
|
||||||
</Label>
|
type="number"
|
||||||
<input
|
id="noOfPersons"
|
||||||
type="number"
|
className="form-control form-control-sm"
|
||||||
id="noOfPersons"
|
{...register("noOfPersons")}
|
||||||
className="form-control form-control-sm"
|
inputMode="numeric"
|
||||||
{...register("noOfPersons")}
|
/>
|
||||||
inputMode="numeric"
|
{errors.noOfPersons && (
|
||||||
/>
|
<small className="danger-text">
|
||||||
{errors.noOfPersons && (
|
{errors.noOfPersons.message}
|
||||||
<small className="danger-text">
|
</small>
|
||||||
{errors.noOfPersons.message}
|
)}
|
||||||
</small>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
|
|||||||
@ -1,80 +1,54 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
const PreviewDocument = ({ imageUrl }) => {
|
const PreviewDocument = ({ imageUrl }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
const [scale, setScale] = useState(1);
|
|
||||||
|
|
||||||
const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3));
|
|
||||||
const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.4));
|
|
||||||
const resetAll = () => {
|
|
||||||
setRotation(0);
|
|
||||||
setScale(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-start gap-3 mb-2">
|
<div className="d-flex justify-content-start">
|
||||||
<i
|
<i
|
||||||
className="bx bx-rotate-right cursor-pointer fs-4"
|
className="bx bx-rotate-right cursor-pointer"
|
||||||
title="Rotate"
|
|
||||||
onClick={() => setRotation((prev) => prev + 90)}
|
onClick={() => setRotation((prev) => prev + 90)}
|
||||||
></i>
|
></i>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="position-relative d-flex flex-column justify-content-center align-items-center"
|
||||||
|
style={{ minHeight: "80vh" }}
|
||||||
|
>
|
||||||
|
|
||||||
<i
|
{loading && (
|
||||||
className="bx bx-zoom-in cursor-pointer fs-4"
|
<div className="text-secondary text-center mb-2">Loading...</div>
|
||||||
title="Zoom In"
|
)}
|
||||||
onClick={zoomIn}
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<i
|
<div className="mb-3 d-flex justify-content-center align-items-center">
|
||||||
className="bx bx-zoom-out cursor-pointer fs-4"
|
<img
|
||||||
title="Zoom Out"
|
src={imageUrl}
|
||||||
onClick={zoomOut}
|
alt="Full View"
|
||||||
></i>
|
className="img-fluid"
|
||||||
|
style={{
|
||||||
|
maxHeight: "80vh",
|
||||||
|
objectFit: "contain",
|
||||||
|
display: loading ? "none" : "block",
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
transition: "transform 0.3s ease",
|
||||||
|
}}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="position-absolute bottom-0 start-0 justify-content-center gap-2">
|
||||||
className="position-relative d-flex flex-column justify-content-center align-items-center overflow-hidden"
|
<button
|
||||||
style={{ minHeight: "80vh" }}
|
className="btn btn-outline-secondary"
|
||||||
>
|
onClick={() => setRotation(0)}
|
||||||
{loading && (
|
title="Reset Rotation"
|
||||||
<div className="text-secondary text-center mb-2">
|
>
|
||||||
Loading...
|
<i className="bx bx-reset"></i> Reset
|
||||||
</div>
|
</button>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-3 d-flex justify-content-center align-items-center">
|
|
||||||
<img
|
|
||||||
src={imageUrl}
|
|
||||||
alt="Full View"
|
|
||||||
className="img-fluid"
|
|
||||||
style={{
|
|
||||||
maxHeight: "80vh",
|
|
||||||
objectFit: "contain",
|
|
||||||
display: loading ? "none" : "block",
|
|
||||||
transform: `rotate(${rotation}deg) scale(${scale})`,
|
|
||||||
transition: "transform 0.3s ease",
|
|
||||||
cursor: "grab",
|
|
||||||
}}
|
|
||||||
onLoad={() => setLoading(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="position-absolute bottom-0 start-0 m-2">
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={resetAll}
|
|
||||||
>
|
|
||||||
<i className="bx bx-reset"></i> Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default PreviewDocument;
|
export default PreviewDocument;
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
import { defaultActionValues, ExpenseActionScheam } from "./ExpenseSchema";
|
||||||
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
import { useExpenseContext } from "../../pages/Expense/ExpensePage";
|
||||||
import {
|
import {
|
||||||
calculateTDSPercentage,
|
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatFigure,
|
formatFigure,
|
||||||
getColorNameFromHex,
|
getColorNameFromHex,
|
||||||
@ -55,22 +54,12 @@ const ViewExpense = ({ ExpenseId }) => {
|
|||||||
setValue,
|
setValue,
|
||||||
reset,
|
reset,
|
||||||
control,
|
control,
|
||||||
watch,
|
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(ActionSchema),
|
resolver: zodResolver(ActionSchema),
|
||||||
defaultValues: defaultActionValues,
|
defaultValues: defaultActionValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseAmount = Number(watch("baseAmount")) || 0;
|
|
||||||
const taxAmount = Number(watch("taxAmount")) || 0;
|
|
||||||
const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|
||||||
|
|
||||||
|
|
||||||
const { grossAmount, tdsAmount, netPayable } = useMemo(() => {
|
|
||||||
return calculateTDSPercentage(baseAmount, taxAmount, tdsPercentage);
|
|
||||||
}, [baseAmount, taxAmount, tdsPercentage]);
|
|
||||||
|
|
||||||
const userPermissions = useSelector(
|
const userPermissions = useSelector(
|
||||||
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
|
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
|
||||||
);
|
);
|
||||||
@ -143,8 +132,9 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
<span>{data?.expenseUId}</span>
|
<span>{data?.expenseUId}</span>
|
||||||
</div>{" "}
|
</div>{" "}
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${getColorNameFromHex(data?.status?.color) || "secondary"
|
className={`badge bg-label-${
|
||||||
}`}
|
getColorNameFromHex(data?.status?.color) || "secondary"
|
||||||
|
}`}
|
||||||
t
|
t
|
||||||
>
|
>
|
||||||
{data?.status?.name}
|
{data?.status?.name}
|
||||||
@ -152,7 +142,7 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 1 */}
|
{/* Row 1 */}
|
||||||
<div className="row text-start">
|
<div className="row text-start">
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
<label
|
<label
|
||||||
className="form-label me-2 mb-0 fw-semibold text-start"
|
className="form-label me-2 mb-0 fw-semibold text-start"
|
||||||
@ -276,7 +266,7 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
{/* Row 5 */}
|
{/* Row 5 */}
|
||||||
|
|
||||||
|
|
||||||
<div className="row text-start">
|
<div className="row text-start">
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
|
|
||||||
<label
|
<label
|
||||||
@ -287,15 +277,15 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 mb-3">
|
<div className="col-md-6 mb-3">
|
||||||
|
|
||||||
<small className="text-muted">
|
<small className="text-muted">
|
||||||
{formatUTCToLocalTime(data?.createdAt, true)}
|
{formatUTCToLocalTime(data?.createdAt, true)}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Created & Paid By */}
|
{/* Created & Paid By */}
|
||||||
{data.createdBy && (
|
{data.createdBy && (
|
||||||
@ -317,8 +307,9 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
lastName={data.createdBy?.lastName}
|
lastName={data.createdBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
{`${data.createdBy?.firstName ?? ""} ${data.createdBy?.lastName ?? ""
|
{`${data.createdBy?.firstName ?? ""} ${
|
||||||
}`.trim() || "N/A"}
|
data.createdBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -346,30 +337,31 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="row text-start">
|
<div className="row text-start">
|
||||||
<div className="col-md-6 text-start mb-3">
|
<div className="col-md-6 text-start mb-3">
|
||||||
<label
|
<label
|
||||||
className="form-label me-2 mb-0 fw-semibold"
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
style={{ minWidth: "130px" }}
|
style={{ minWidth: "130px" }}
|
||||||
>
|
>
|
||||||
Paid By :
|
Paid By :
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6 text-start mb-3">
|
<div className="col-md-6 text-start mb-3">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="xs"
|
size="xs"
|
||||||
classAvatar="m-0 me-1"
|
classAvatar="m-0 me-1"
|
||||||
firstName={data.paidBy?.firstName}
|
firstName={data.paidBy?.firstName}
|
||||||
lastName={data.paidBy?.lastName}
|
lastName={data.paidBy?.lastName}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
{`${data.paidBy?.firstName ?? ""} ${data.paidBy?.lastName ?? ""
|
{`${data.paidBy?.firstName ?? ""} ${
|
||||||
|
data.paidBy?.lastName ?? ""
|
||||||
}`.trim() || "N/A"}
|
}`.trim() || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div className="col-12 text-start mb-2">
|
<div className="col-12 text-start mb-2">
|
||||||
@ -501,7 +493,19 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
projectId={null}
|
projectId={null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-md-6 text-start">
|
||||||
|
<Label className="form-label">TDS Percentage</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("tdsPercentage")}
|
||||||
|
/>
|
||||||
|
{errors.tdsPercentage && (
|
||||||
|
<small className="danger-text">
|
||||||
|
{errors.tdsPercentage.message}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="col-12 col-md-6 text-start">
|
<div className="col-12 col-md-6 text-start">
|
||||||
<Label className="form-label" required>
|
<Label className="form-label" required>
|
||||||
Base Amount
|
Base Amount
|
||||||
@ -532,53 +536,28 @@ const tdsPercentage = Number(watch("tdsPercentage")) || 0;
|
|||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 text-start">
|
|
||||||
<Label className="form-label">TDS Percentage</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("tdsPercentage")}
|
|
||||||
/>
|
|
||||||
{errors.tdsPercentage && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.tdsPercentage.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 d-flex align-items-center gap-4 mb-2 mt-1">
|
|
||||||
<div>
|
|
||||||
<span className="fw-semibold">TDS Amount: </span>
|
|
||||||
<span className="badge bg-label-secondary">{tdsAmount.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="fw-semibold">Net Payable: </span>
|
|
||||||
<span className="badge bg-label-secondary">{netPayable.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
<div className="col-12 mb-3 text-start mt-1">
|
<div className="col-12 mb-3 text-start mt-1">
|
||||||
{((nextStatusWithPermission.length > 0 &&
|
{((nextStatusWithPermission.length > 0 &&
|
||||||
!IsRejectedExpense) ||
|
!IsRejectedExpense) ||
|
||||||
(IsRejectedExpense && isCreatedBy)) && (
|
(IsRejectedExpense && isCreatedBy)) && (
|
||||||
<>
|
<>
|
||||||
<Label className="form-label me-2 mb-0" required>
|
<Label className="form-label me-2 mb-0" required>
|
||||||
Comment
|
Comment
|
||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...register("comment")}
|
{...register("comment")}
|
||||||
rows="2"
|
rows="2"
|
||||||
/>
|
/>
|
||||||
{errors.comment && (
|
{errors.comment && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
{errors.comment.message}
|
{errors.comment.message}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nextStatusWithPermission?.length > 0 &&
|
{nextStatusWithPermission?.length > 0 &&
|
||||||
(!IsRejectedExpense || isCreatedBy) && (
|
(!IsRejectedExpense || isCreatedBy) && (
|
||||||
|
|||||||
@ -46,15 +46,8 @@ const Header = () => {
|
|||||||
pathname
|
pathname
|
||||||
);
|
);
|
||||||
const isExpensePage = /^\/expenses$/.test(pathname);
|
const isExpensePage = /^\/expenses$/.test(pathname);
|
||||||
const isPaymentRequest = /^\/payment-request$/.test(pathname);
|
|
||||||
const isRecurringExpense = /^\/recurring-payment$/.test(pathname);
|
|
||||||
const isAdvancePayment = /^\/advance-payment$/.test(pathname);
|
|
||||||
const isServiceProjectPage = /^\/service-projects\/[0-9a-fA-F-]{36}$/.test(pathname);
|
|
||||||
const isAdvancePayment1 =
|
|
||||||
/^\/advance-payment(\/[0-9a-fA-F-]{36})?$/.test(pathname);
|
|
||||||
|
|
||||||
|
return !(isDirectoryPath || isProfilePage || isExpensePage);
|
||||||
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage || isAdvancePayment1);
|
|
||||||
};
|
};
|
||||||
const allowedProjectStatusIds = [
|
const allowedProjectStatusIds = [
|
||||||
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
|
"603e994b-a27f-4e5d-a251-f3d69b0498ba",
|
||||||
@ -68,6 +61,10 @@ const Header = () => {
|
|||||||
return role ? role.name : "User";
|
return role ? role.name : "User";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleProfilePage = () => {
|
const handleProfilePage = () => {
|
||||||
navigate(`/employee/${profile?.employeeInfo?.id}`);
|
navigate(`/employee/${profile?.employeeInfo?.id}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,19 +25,22 @@ const Sidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</span> */}
|
</span> */}
|
||||||
|
|
||||||
<small className="app-brand-link fw-bold navbar-brand text-green fs-6">
|
<a
|
||||||
|
href="/"
|
||||||
|
className="app-brand-link fw-bold navbar-brand text-green fs-6"
|
||||||
|
>
|
||||||
<span className="app-brand-logo demo">
|
<span className="app-brand-logo demo">
|
||||||
<img src="/img/brand/marco.png" width="50" />
|
<img src="/img/brand/marco.png" width="50" />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-blue">OnField</span>
|
<span className="text-blue">OnField</span>
|
||||||
<span>Work</span>
|
<span>Work</span>
|
||||||
<span className="text-dark">.com</span>
|
<span className="text-dark">.com</span>
|
||||||
</small>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<small className="layout-menu-toggle menu-link text-large ms-auto">
|
<a className="layout-menu-toggle menu-link text-large ms-auto">
|
||||||
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
|
<i className="bx bx-chevron-left bx-sm d-flex align-items-center justify-content-center"></i>
|
||||||
</small>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="menu-inner-shadow"></div>
|
<div className="menu-inner-shadow"></div>
|
||||||
@ -58,7 +61,7 @@ const Sidebar = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{data &&
|
{data &&
|
||||||
data?.data?.map((section) => (
|
data?.data.map((section) => (
|
||||||
<React.Fragment
|
<React.Fragment
|
||||||
key={section.id || section.header || section.items[0]?.id}
|
key={section.id || section.header || section.items[0]?.id}
|
||||||
>
|
>
|
||||||
@ -96,7 +99,7 @@ const MenuItem = (item) => {
|
|||||||
className={`menu-link ${hasSubmenu ? "menu-toggle" : ""}`}
|
className={`menu-link ${hasSubmenu ? "menu-toggle" : ""}`}
|
||||||
target={item.link?.includes("http") ? "_blank" : undefined}
|
target={item.link?.includes("http") ? "_blank" : undefined}
|
||||||
>
|
>
|
||||||
{item.icon && <i className={`menu-icon tf-icons ${item.icon}`}></i>}
|
<i className={`menu-icon tf-icons ${item.icon}`}></i>
|
||||||
<div>{item.name}</div>
|
<div>{item.name}</div>
|
||||||
{item.available === false && (
|
{item.available === false && (
|
||||||
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">
|
<div className="badge bg-label-primary fs-tiny rounded-pill ms-auto">
|
||||||
|
|||||||
@ -135,19 +135,6 @@ const ManagOrg = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-1 text-start">
|
|
||||||
<Label htmlFor="gstNumber">
|
|
||||||
GST Number
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("gstNumber")}
|
|
||||||
/>
|
|
||||||
{errors.gstNumber && (
|
|
||||||
<span className="danger-text">{errors.gstNumber.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-1 text-start">
|
<div className="mb-1 text-start">
|
||||||
<Label htmlFor="email" required>
|
<Label htmlFor="email" required>
|
||||||
Email Address
|
Email Address
|
||||||
@ -212,8 +199,8 @@ const ManagOrg = () => {
|
|||||||
{isCreating || isUpdating
|
{isCreating || isUpdating
|
||||||
? "Please Wait..."
|
? "Please Wait..."
|
||||||
: orgData
|
: orgData
|
||||||
? "Update"
|
? "Update"
|
||||||
: "Submit"}
|
: "Submit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,7 +20,6 @@ export const organizationSchema = z.object({
|
|||||||
serviceIds: z
|
serviceIds: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.min(1, { message: "Service isrequired" }),
|
.min(1, { message: "Service isrequired" }),
|
||||||
gstNumber: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultOrganizationValues = {
|
export const defaultOrganizationValues = {
|
||||||
@ -30,7 +29,6 @@ export const defaultOrganizationValues = {
|
|||||||
address: "",
|
address: "",
|
||||||
email: "",
|
email: "",
|
||||||
serviceIds: [],
|
serviceIds: [],
|
||||||
gstNumber : ""
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assignedOrgToProject = z.object({
|
export const assignedOrgToProject = z.object({
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage";
|
import { usePaymentRequestContext } from "../../pages/PaymentRequest/PaymentRequestPage";
|
||||||
import { calculateTDSPercentage, localToUtc } from "../../utils/appUtils";
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
import { usePaymentMode } from "../../hooks/masterHook/useMaster";
|
import { usePaymentMode } from "../../hooks/masterHook/useMaster";
|
||||||
import Filelist from "../Expenses/Filelist";
|
import Filelist from "../Expenses/Filelist";
|
||||||
|
|
||||||
@ -40,7 +40,6 @@ const ActionPaymentRequest = ({ requestId }) => {
|
|||||||
error: PaymentModeError,
|
error: PaymentModeError,
|
||||||
} = usePaymentMode();
|
} = usePaymentMode();
|
||||||
|
|
||||||
|
|
||||||
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
|
||||||
const [imageLoaded, setImageLoaded] = useState({});
|
const [imageLoaded, setImageLoaded] = useState({});
|
||||||
|
|
||||||
@ -70,10 +69,15 @@ const ActionPaymentRequest = ({ requestId }) => {
|
|||||||
const taxAmount = watch("taxAmount") || 0;
|
const taxAmount = watch("taxAmount") || 0;
|
||||||
const tdsPercentage = watch("tdsPercentage") || 0;
|
const tdsPercentage = watch("tdsPercentage") || 0;
|
||||||
|
|
||||||
|
const grossAmount = baseAmount + taxAmount;
|
||||||
|
|
||||||
|
const tdsAmount = useMemo(() => (baseAmount * tdsPercentage) / 100, [
|
||||||
|
baseAmount,
|
||||||
|
tdsPercentage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const netPayable = useMemo(() => grossAmount - tdsAmount, [grossAmount, tdsAmount]);
|
||||||
|
|
||||||
const { grossAmount, tdsAmount, netPayable } = useMemo(() => {
|
|
||||||
return calculateTDSPercentage(baseAmount, taxAmount, tdsPercentage);
|
|
||||||
}, [baseAmount, taxAmount, tdsPercentage]);
|
|
||||||
|
|
||||||
const userPermissions = useSelector(
|
const userPermissions = useSelector(
|
||||||
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
|
(state) => state?.globalVariables?.loginUser?.featurePermissions || []
|
||||||
@ -176,16 +180,6 @@ const ActionPaymentRequest = ({ requestId }) => {
|
|||||||
const newFiles = files.filter((_, i) => i !== index);
|
const newFiles = files.filter((_, i) => i !== index);
|
||||||
setValue("billAttachments", newFiles, { shouldValidate: true });
|
setValue("billAttachments", newFiles, { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredPaymentModes = useMemo(() => {
|
|
||||||
return PaymentModes?.filter((mode) => {
|
|
||||||
if (mode.name === "Advance Payment" && data?.isAdvancePayment === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}) || [];
|
|
||||||
}, [PaymentModes, data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
|
{IsPaymentProcess && nextStatusWithPermission?.length > 0 && (
|
||||||
@ -230,7 +224,7 @@ const ActionPaymentRequest = ({ requestId }) => {
|
|||||||
{PaymentModeLoading ? (
|
{PaymentModeLoading ? (
|
||||||
<option disabled>Loading...</option>
|
<option disabled>Loading...</option>
|
||||||
) : (
|
) : (
|
||||||
filteredPaymentModes?.map((payment) => (
|
PaymentModes?.map((payment) => (
|
||||||
<option key={payment.id} value={payment.id}>
|
<option key={payment.id} value={payment.id}>
|
||||||
{payment.name}
|
{payment.name}
|
||||||
</option>
|
</option>
|
||||||
@ -434,7 +428,6 @@ const ActionPaymentRequest = ({ requestId }) => {
|
|||||||
<span className="badge bg-label-secondary">{netPayable.toFixed(2)}</span>
|
<span className="badge bg-label-secondary">{netPayable.toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import Filelist from "../Expenses/Filelist";
|
|||||||
import InputSuggestions from "../common/InputSuggestion";
|
import InputSuggestions from "../common/InputSuggestion";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
import { blockUI } from "../../utils/blockUI";
|
import { blockUI } from "../../utils/blockUI";
|
||||||
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
|
|
||||||
|
|
||||||
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
||||||
const {
|
const {
|
||||||
@ -235,10 +234,10 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
|||||||
{/* Project and Category */}
|
{/* Project and Category */}
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
{/* <Label className="form-label" required>
|
<Label className="form-label" required>
|
||||||
Select Project
|
Select Project
|
||||||
</Label> */}
|
</Label>
|
||||||
{/* <select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
{...register("projectId")}
|
{...register("projectId")}
|
||||||
disabled={
|
disabled={
|
||||||
@ -255,23 +254,7 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
|
|||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</select> */}
|
</select>
|
||||||
<SelectProjectField
|
|
||||||
label="Project"
|
|
||||||
required
|
|
||||||
placeholder="Select Project"
|
|
||||||
value={watch("projectId")}
|
|
||||||
onChange={(val) =>
|
|
||||||
setValue("projectId", val, {
|
|
||||||
shouldDirty: true,
|
|
||||||
shouldValidate: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
data?.recurringPayment?.isVariable && !isDraft && !isProcessed
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
import React, { useMemo } from "react";
|
|
||||||
|
|
||||||
const PaymentRequestFilterChips = ({ filters, filterData, removeFilterChip, clearFilter }) => {
|
|
||||||
const data = filterData?.data || filterData || {};
|
|
||||||
|
|
||||||
const filterChips = useMemo(() => {
|
|
||||||
const chips = [];
|
|
||||||
|
|
||||||
const addGroup = (ids, list, label, key) => {
|
|
||||||
if (!ids?.length) return;
|
|
||||||
const items = ids.map((id) => ({
|
|
||||||
id,
|
|
||||||
name: list?.find((i) => i.id === id)?.name || id,
|
|
||||||
}));
|
|
||||||
chips.push({ key, label, items });
|
|
||||||
};
|
|
||||||
|
|
||||||
addGroup(filters.createdByIds, data.createdBy, "Created By", "createdByIds");
|
|
||||||
addGroup(filters.currencyIds, data.currency, "Currency", "currencyIds");
|
|
||||||
addGroup(filters.expenseCategoryIds, data.expenseCategory, "Expense Category", "expenseCategoryIds");
|
|
||||||
addGroup(filters.payees, data.payees, "Payees", "payees");
|
|
||||||
addGroup(filters.projectIds, data.projects, "Projects", "projectIds");
|
|
||||||
addGroup(filters.statusIds, data.status, "Status", "statusIds");
|
|
||||||
|
|
||||||
return chips;
|
|
||||||
}, [filters, filterData]);
|
|
||||||
|
|
||||||
if (!filterChips.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
|
||||||
{filterChips.map((chipGroup) => (
|
|
||||||
<div key={chipGroup.key} className="d-flex align-items-center flex-wrap">
|
|
||||||
<span className="fw-semibold me-2">{chipGroup.label}:</span>
|
|
||||||
{chipGroup.items.map((item) => (
|
|
||||||
<span
|
|
||||||
key={item.id}
|
|
||||||
className="d-flex align-items-center bg-light rounded px-2 py-1 me-1"
|
|
||||||
>
|
|
||||||
<span>{item.name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close btn-close-white btn-sm ms-2"
|
|
||||||
style={{
|
|
||||||
filter: "invert(1) grayscale(1)",
|
|
||||||
opacity: 0.7,
|
|
||||||
fontSize: "0.6rem",
|
|
||||||
}}
|
|
||||||
onClick={() => removeFilterChip(chipGroup.key, item.id)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PaymentRequestFilterChips;
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState, useMemo, forwardRef, useImperativeHandle } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultPaymentRequestFilter, SearchPaymentRequestSchema } from "./PaymentRequestSchema";
|
import { defaultPaymentRequestFilter,SearchPaymentRequestSchema } from "./PaymentRequestSchema";
|
||||||
|
|
||||||
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
@ -13,7 +13,7 @@ import moment from "moment";
|
|||||||
import { usePaymentRequestFilter } from "../../hooks/useExpense";
|
import { usePaymentRequestFilter } from "../../hooks/useExpense";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilterdata, clearFilter }, ref) => {
|
const PaymentRequestFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||||
const { status } = useParams();
|
const { status } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const selectedProjectId = useSelector(
|
const selectedProjectId = useSelector(
|
||||||
@ -38,23 +38,10 @@ const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilte
|
|||||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
const [selectedGroup, setSelectedGroup] = useState(groupByList[6]);
|
||||||
const [resetKey, setResetKey] = useState(0);
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
const dynamicDefaultFilter = useMemo(() => {
|
|
||||||
return {
|
|
||||||
...defaultPaymentRequestFilter,
|
|
||||||
projectIds: defaultPaymentRequestFilter.projectIds || [],
|
|
||||||
statusIds: status ? [status] : defaultPaymentRequestFilter.statusIds || [],
|
|
||||||
createdByIds: defaultPaymentRequestFilter.createdByIds || [],
|
|
||||||
currencyIds: defaultPaymentRequestFilter.currencyIds || [],
|
|
||||||
expenseCategoryIds: defaultPaymentRequestFilter.expenseCategoryIds || [],
|
|
||||||
payees: defaultPaymentRequestFilter.payees || [],
|
|
||||||
startDate: defaultPaymentRequestFilter.startDate,
|
|
||||||
endDate: defaultPaymentRequestFilter.endDate,
|
|
||||||
};
|
|
||||||
}, [status, selectedProjectId]);
|
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(SearchPaymentRequestSchema),
|
resolver: zodResolver(SearchPaymentRequestSchema),
|
||||||
defaultValues: dynamicDefaultFilter,
|
defaultValues: defaultPaymentRequestFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control, handleSubmit, reset, setValue, watch } = methods;
|
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||||
@ -65,28 +52,12 @@ const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilte
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleGroupChange = (e) => {
|
const handleGroupChange = (e) => {
|
||||||
const group = groupByList.find((g) => g.id === e.target.value);
|
const group = groupByList.find((g) => g.id === e.target.value);
|
||||||
if (group) setSelectedGroup(group);
|
if (group) setSelectedGroup(group);
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
resetFieldValue: (name, value) => {
|
|
||||||
// Reset specific field
|
|
||||||
if (value !== undefined) {
|
|
||||||
setValue(name, value);
|
|
||||||
} else {
|
|
||||||
reset({ ...methods.getValues(), [name]: defaultFilter[name] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getValues: methods.getValues, // optional, to read current filter state
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data && setFilterdata) {
|
|
||||||
setFilterdata(data);
|
|
||||||
}
|
|
||||||
}, [data, setFilterdata]);
|
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
onApply({
|
onApply({
|
||||||
@ -226,6 +197,6 @@ const PaymentRequestFilterPanel = forwardRef(({ onApply, handleGroupBy, setFilte
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default PaymentRequestFilterPanel;
|
export default PaymentRequestFilterPanel;
|
||||||
@ -20,9 +20,8 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import Error from "../common/Error";
|
import Error from "../common/Error";
|
||||||
import Pagination from "../common/Pagination";
|
import Pagination from "../common/Pagination";
|
||||||
import PaymentRequestFilterChips from "./PaymentRequestFilterChips";
|
|
||||||
|
|
||||||
const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter, search, groupBy = "submittedBy" }) => {
|
const PaymentRequestList = ({ filters, groupBy = "submittedBy", search }) => {
|
||||||
const { setManageRequest, setVieRequest } = usePaymentRequestContext();
|
const { setManageRequest, setVieRequest } = usePaymentRequestContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
@ -45,8 +44,9 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
displayField = "Status";
|
displayField = "Status";
|
||||||
break;
|
break;
|
||||||
case "submittedBy":
|
case "submittedBy":
|
||||||
key = `${item?.createdBy?.firstName ?? ""} ${item.createdBy?.lastName ?? ""
|
key = `${item?.createdBy?.firstName ?? ""} ${
|
||||||
}`.trim();
|
item.createdBy?.lastName ?? ""
|
||||||
|
}`.trim();
|
||||||
displayField = "Submitted By";
|
displayField = "Submitted By";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
@ -85,7 +85,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
key: "paymentRequestUID",
|
key: "paymentRequestUID",
|
||||||
label: "Request ID",
|
label: "Request ID",
|
||||||
align: "text-start mx-2",
|
align: "text-start mx-2",
|
||||||
getValue: (e) => <div className="d-flex"><span>{e.paymentRequestUID || "N/A"}</span> {e.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xxs" >Adv</span>}</div>,
|
getValue: (e) => e.paymentRequestUID || "N/A",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "title",
|
key: "title",
|
||||||
@ -93,25 +93,40 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
align: "text-start",
|
align: "text-start",
|
||||||
getValue: (e) => e.title || "N/A",
|
getValue: (e) => e.title || "N/A",
|
||||||
},
|
},
|
||||||
|
// { key: "payee", label: "Payee", align: "text-start" },
|
||||||
|
{
|
||||||
|
key: "SubmittedBy",
|
||||||
|
label: "Submitted By",
|
||||||
|
align: "text-start",
|
||||||
|
getValue: (e) =>
|
||||||
|
`${e.createdBy?.firstName ?? ""} ${
|
||||||
|
e.createdBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A",
|
||||||
|
customRender: (e) => (
|
||||||
|
<div
|
||||||
|
className="d-flex align-items-center cursor-pointer"
|
||||||
|
onClick={() => navigate(`/employee/${e.createdBy?.id}`)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
classAvatar="m-0"
|
||||||
|
firstName={e.createdBy?.firstName}
|
||||||
|
lastName={e.createdBy?.lastName}
|
||||||
|
/>
|
||||||
|
<span className="text-truncate">
|
||||||
|
{`${e.createdBy?.firstName ?? ""} ${
|
||||||
|
e.createdBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "createdAt",
|
key: "createdAt",
|
||||||
label: "Created At",
|
label: "Submitted On",
|
||||||
align: "text-start",
|
align: "text-start",
|
||||||
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
getValue: (e) => formatUTCToLocalTime(e?.createdAt),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "payee",
|
|
||||||
label: "Payee",
|
|
||||||
align: "text-start",
|
|
||||||
getValue: (e) => e.payee || "N/A",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "dueDate",
|
|
||||||
label: "Due Date",
|
|
||||||
align: "text-start",
|
|
||||||
getValue: (e) => formatUTCToLocalTime(e?.dueDate),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
label: "Amount",
|
label: "Amount",
|
||||||
@ -128,8 +143,9 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
align: "text-center",
|
align: "text-center",
|
||||||
getValue: (e) => (
|
getValue: (e) => (
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
|
className={`badge bg-label-${
|
||||||
}`}
|
getColorNameFromHex(e?.expenseStatus?.color) || "secondary"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{e?.expenseStatus?.name || "Unknown"}
|
{e?.expenseStatus?.name || "Unknown"}
|
||||||
</span>
|
</span>
|
||||||
@ -155,8 +171,8 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
const header = [
|
const header = [
|
||||||
"Request ID",
|
"Request ID",
|
||||||
"Request Title",
|
"Request Title",
|
||||||
"Created At",
|
"Submitted By",
|
||||||
"Due Date",
|
"Submitted On",
|
||||||
"Amount",
|
"Amount",
|
||||||
"Status",
|
"Status",
|
||||||
"Action",
|
"Action",
|
||||||
@ -165,10 +181,10 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
|
|
||||||
const grouped = groupBy
|
const grouped = groupBy
|
||||||
? Object.fromEntries(
|
? Object.fromEntries(
|
||||||
Object.entries(groupByField(data?.data ?? [], groupBy)).sort(
|
Object.entries(groupByField(data?.data ?? [], groupBy)).sort(
|
||||||
([keyA], [keyB]) => keyA.localeCompare(keyB)
|
([keyA], [keyB]) => keyA.localeCompare(keyB)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
: { All: data?.data ?? [] };
|
: { All: data?.data ?? [] };
|
||||||
|
|
||||||
const IsGroupedByDate = [
|
const IsGroupedByDate = [
|
||||||
@ -224,14 +240,6 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
)}
|
)}
|
||||||
<div className="card page-min-h table-responsive px-sm-4">
|
<div className="card page-min-h table-responsive px-sm-4">
|
||||||
<div className="card-datatable mx-2" id="payment-request-table ">
|
<div className="card-datatable mx-2" id="payment-request-table ">
|
||||||
<div className="col-12 mb-2 mt-2">
|
|
||||||
<PaymentRequestFilterChips
|
|
||||||
filters={filters}
|
|
||||||
filterData={filterData}
|
|
||||||
removeFilterChip={removeFilterChip}
|
|
||||||
clearFilter={clearFilter}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<table className="table border-top dataTable text-nowrap align-middle">
|
<table className="table border-top dataTable text-nowrap align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -262,7 +270,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{items?.map((paymentRequest) => (
|
{items?.map((paymentRequest) => (
|
||||||
<tr key={paymentRequest.id} style={{ height: "40px" }}>
|
<tr key={paymentRequest.id}>
|
||||||
{paymentRequestColumns.map(
|
{paymentRequestColumns.map(
|
||||||
(col) =>
|
(col) =>
|
||||||
(col.isAlwaysVisible || groupBy !== col.key) && (
|
(col.isAlwaysVisible || groupBy !== col.key) && (
|
||||||
|
|||||||
@ -148,7 +148,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
|||||||
<div className="col-12 col-sm-6 ">
|
<div className="col-12 col-sm-6 ">
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
<div className="col-12 d-flex justify-content-between mb-6">
|
<div className="col-12 d-flex justify-content-between mb-6">
|
||||||
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span> {data.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xs" >Advance</span>}</div>
|
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span></div>
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
|
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
|
||||||
}`}
|
}`}
|
||||||
@ -199,7 +199,7 @@ const ViewPaymentRequest = ({ requestId }) => {
|
|||||||
<div className="row text-start">
|
<div className="row text-start">
|
||||||
<div className="col-6 mb-3">
|
<div className="col-6 mb-3">
|
||||||
<label className="form-label me-2 mb-0 fw-semibold text-start">
|
<label className="form-label me-2 mb-0 fw-semibold text-start">
|
||||||
Payee:
|
Supplier:
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 mb-3">
|
<div className="col-6 mb-3">
|
||||||
@ -272,6 +272,27 @@ const ViewPaymentRequest = ({ requestId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{data?.paidBy && (
|
||||||
|
<div className="col-6 text-start">
|
||||||
|
<div className="d-flex flex-column gap-2 text-start">
|
||||||
|
<label className="form-label me-2 mb-0 fw-semibold">
|
||||||
|
Paid By:
|
||||||
|
</label>
|
||||||
|
<div className="d-flex align-items-center ">
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
classAvatar="m-0"
|
||||||
|
firstName={data?.paidBy?.firstName}
|
||||||
|
lastName={data?.paidBy?.lastName}
|
||||||
|
/>
|
||||||
|
<span className="text-muted">
|
||||||
|
{`${data?.paidBy?.firstName ?? ""} ${data?.paidBy?.lastName ?? ""
|
||||||
|
}`.trim() || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="text-start my-2">
|
<div className="text-start my-2">
|
||||||
<label className="fw-semibold form-label">Description : </label>
|
<label className="fw-semibold form-label">Description : </label>
|
||||||
|
|||||||
@ -82,6 +82,7 @@ const EditActivityModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workItem) return;
|
if (!workItem) return;
|
||||||
|
console.log(workItem)
|
||||||
reset({
|
reset({
|
||||||
activityID: String(
|
activityID: String(
|
||||||
workItem?.workItem?.activityId || workItem?.activityMaster?.id
|
workItem?.workItem?.activityId || workItem?.activityMaster?.id
|
||||||
|
|||||||
@ -108,7 +108,7 @@ const ManageProjectInfo = ({ project, onClose }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="p-sm-2 p-2">
|
<div className="p-sm-2 p-2">
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<h5 className="mb-2">{project ? "Edit Infra Project" : "Create Infra Project"}</h5>
|
<h5 className="mb-2">{project ? "Edit Project" : "Create Project"}</h5>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
className="row g-2 text-start"
|
className="row g-2 text-start"
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {
|
import { formatNumber, getDateDifferenceInDays } from "../../utils/dateUtils";
|
||||||
formatNumber,
|
|
||||||
formatUTCToLocalTime,
|
|
||||||
getDateDifferenceInDays,
|
|
||||||
} from "../../utils/dateUtils";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
|
import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
|
||||||
import ManageProjectInfo from "./ManageProjectInfo";
|
import ManageProjectInfo from "./ManageProjectInfo";
|
||||||
@ -21,18 +17,12 @@ import GlobalModel from "../common/GlobalModel";
|
|||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import { useProjectContext } from "../../pages/project/ProjectPage";
|
import { useProjectContext } from "../../pages/project/ProjectPage";
|
||||||
import { useActiveInActiveServiceProject } from "../../hooks/useServiceProject";
|
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
|
||||||
|
|
||||||
const ProjectCard = ({ project, isCore = true }) => {
|
const ProjectCard = ({ project }) => {
|
||||||
const [deleteProject, setDeleteProject] = useState({
|
|
||||||
project: null,
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
const { setMangeProject, setManageServiceProject } = useProjectContext();
|
const { setMangeProject } = useProjectContext();
|
||||||
|
|
||||||
const getProgress = (planned, completed) => {
|
const getProgress = (planned, completed) => {
|
||||||
return (completed * 100) / planned + "%";
|
return (completed * 100) / planned + "%";
|
||||||
@ -44,49 +34,15 @@ const ProjectCard = ({ project, isCore = true }) => {
|
|||||||
const handleClose = () => setShowModal(false);
|
const handleClose = () => setShowModal(false);
|
||||||
|
|
||||||
const handleViewProject = () => {
|
const handleViewProject = () => {
|
||||||
if (isCore) {
|
dispatch(setProjectId(project.id));
|
||||||
dispatch(setProjectId(project.id));
|
navigate(`/projects/details`);
|
||||||
navigate(`/projects/details`);
|
|
||||||
} else {
|
|
||||||
navigate(`/service-projects/${project.id}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const handleViewActivities = () => {
|
const handleViewActivities = () => {
|
||||||
dispatch(setProjectId(project.id));
|
dispatch(setProjectId(project.id));
|
||||||
navigate(`/activities/records?project=${project.id}`);
|
navigate(`/activities/records?project=${project.id}`);
|
||||||
};
|
};
|
||||||
const handleManage = () => {
|
|
||||||
if (isCore) {
|
|
||||||
setMangeProject({
|
|
||||||
isOpen: true,
|
|
||||||
Project: project.id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setManageServiceProject({
|
|
||||||
isOpen: true,
|
|
||||||
project: project.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: DeleteProject, isPending } = useActiveInActiveServiceProject(
|
|
||||||
() => setDeleteProject({ project: null, isOpen: false })
|
|
||||||
);
|
|
||||||
const handleActiveInactive = (projectId) => {
|
|
||||||
DeleteProject(projectId, false);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfirmModal
|
|
||||||
type="delete"
|
|
||||||
header="Delete Project"
|
|
||||||
message="Are you sure you want delete?"
|
|
||||||
onSubmit={handleActiveInactive}
|
|
||||||
onClose={() => setDeleteProject({ project: null, isOpen: false })}
|
|
||||||
loading={isPending}
|
|
||||||
paramData={project.id}
|
|
||||||
isOpen={deleteProject.isOpen}
|
|
||||||
/>
|
|
||||||
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
|
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
|
||||||
<div className={`card cursor-pointer`}>
|
<div className={`card cursor-pointer`}>
|
||||||
<div className="card-header pb-4">
|
<div className="card-header pb-4">
|
||||||
@ -140,31 +96,22 @@ const ProjectCard = ({ project, isCore = true }) => {
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a className="dropdown-item" onClick={handleManage}>
|
<a className="dropdown-item" onClick={() =>
|
||||||
|
setMangeProject({
|
||||||
|
isOpen: true,
|
||||||
|
Project: project.id,
|
||||||
|
})
|
||||||
|
}>
|
||||||
<i className="bx bx-pencil me-2"></i>
|
<i className="bx bx-pencil me-2"></i>
|
||||||
<span className="align-left">Modify</span>
|
<span className="align-left">Modify</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{isCore && (
|
<li onClick={handleViewActivities}>
|
||||||
<li onClick={handleViewActivities}>
|
<a className="dropdown-item">
|
||||||
<a className="dropdown-item">
|
<i className="bx bx-task me-2"></i>
|
||||||
<i className="bx bx-task me-2"></i>
|
<span className="align-left">Activities</span>
|
||||||
<span className="align-left">Activities</span>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{!isCore && (
|
|
||||||
<li
|
|
||||||
onClick={() =>
|
|
||||||
setDeleteProject({ project: project, isOpen: true })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a className="dropdown-item">
|
|
||||||
<i className="bx bx-trash me-2"></i>
|
|
||||||
<span className="align-left">Delete</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,29 +124,21 @@ const ProjectCard = ({ project, isCore = true }) => {
|
|||||||
<span className="text-heading fw-medium">
|
<span className="text-heading fw-medium">
|
||||||
Contact Person:{" "}
|
Contact Person:{" "}
|
||||||
</span>
|
</span>
|
||||||
{isCore
|
{project?.contactPerson ? project.contactPerson : "NA"}
|
||||||
? project?.contactPerson
|
|
||||||
? project.contactPerson
|
|
||||||
: "NA"
|
|
||||||
: project.contactName}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-1">
|
<p className="mb-1">
|
||||||
<span className="text-heading fw-medium">
|
<span className="text-heading fw-medium">Start Date: </span>
|
||||||
{isCore ? "Start Date:" : "Start Date"}{" "}
|
{project.startDate
|
||||||
</span>
|
? moment(project?.startDate).format("DD-MMM-YYYY")
|
||||||
{formatUTCToLocalTime(
|
: "NA"}
|
||||||
isCore ? project?.startDate : project.assignedDate
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
{isCore && (
|
<p className="mb-1">
|
||||||
<p className="mb-1">
|
<span className="text-heading fw-medium">Deadline: </span>
|
||||||
<span className="text-heading fw-medium">Deadline: </span>
|
|
||||||
|
|
||||||
{project?.endDate
|
{project?.endDate
|
||||||
? moment(project?.endDate).format("DD-MMM-YYYY")
|
? moment(project?.endDate).format("DD-MMM-YYYY")
|
||||||
: "NA"}
|
: "NA"}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
<p className="mb-0">{project?.projectAddress}</p>
|
<p className="mb-0">{project?.projectAddress}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -216,7 +155,7 @@ const ProjectCard = ({ project, isCore = true }) => {
|
|||||||
{getProjectStatusName(project?.projectStatusId)}
|
{getProjectStatusName(project?.projectStatusId)}
|
||||||
</span>
|
</span>
|
||||||
</p>{" "}
|
</p>{" "}
|
||||||
{getDateDifferenceInDays(project?.endDate, new Date()) >= 0 && (
|
{getDateDifferenceInDays(project?.endDate,new Date() ) >= 0 && (
|
||||||
<span className="badge bg-label-success ms-auto">
|
<span className="badge bg-label-success ms-auto">
|
||||||
{project?.endDate &&
|
{project?.endDate &&
|
||||||
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
|
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
|
||||||
@ -226,7 +165,7 @@ const ProjectCard = ({ project, isCore = true }) => {
|
|||||||
{getDateDifferenceInDays(project?.endDate, new Date()) < 0 && (
|
{getDateDifferenceInDays(project?.endDate, new Date()) < 0 && (
|
||||||
<span className="badge bg-label-danger ms-auto">
|
<span className="badge bg-label-danger ms-auto">
|
||||||
{project?.endDate &&
|
{project?.endDate &&
|
||||||
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
|
getDateDifferenceInDays(project?.endDate, new Date())}{" "}
|
||||||
Days overdue
|
Days overdue
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,34 +1,70 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from '../../hooks/useProjects'
|
||||||
import Loader from "../common/Loader";
|
import Loader from '../common/Loader'
|
||||||
import ProjectCard from "./ProjectCard";
|
import ProjectCard from './ProjectCard'
|
||||||
import Pagination from "../common/Pagination";
|
|
||||||
|
const ProjectCardView = ({currentItems,setCurrentPage,totalPages }) => {
|
||||||
|
|
||||||
|
|
||||||
const ProjectCardView = ({ data, currentPage, totalPages, paginate }) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="row page-min-h">
|
|
||||||
{data?.length === 0 && (
|
|
||||||
<div
|
|
||||||
className="col-12 d-flex justify-content-center align-items-center"
|
|
||||||
style={{ minHeight: "250px" }}
|
|
||||||
>
|
|
||||||
<p className="text-center text-muted m-0">No Infra projects found.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data?.map((project) => (
|
<div className="row page-min-h">
|
||||||
<ProjectCard key={project.id} project={project} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="col-12 d-flex justify-content-start mt-3">
|
{ currentItems.length === 0 && (
|
||||||
<Pagination
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
currentPage={currentPage}
|
)}
|
||||||
totalPages={totalPages}
|
|
||||||
onPageChange={paginate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectCardView;
|
{currentItems.map((project) => (
|
||||||
|
<ProjectCard
|
||||||
|
key={project.id}
|
||||||
|
project={project}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
|
||||||
|
{ totalPages > 1 && (
|
||||||
|
<nav>
|
||||||
|
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||||
|
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{[...Array(totalPages)].map((_, i) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={`page-item ${currentPage === totalPages && "disabled"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectCardView
|
||||||
|
|||||||
@ -13,9 +13,15 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { useProjectContext } from "../../pages/project/ProjectPage";
|
import { useProjectContext } from "../../pages/project/ProjectPage";
|
||||||
import usePagination from "../../hooks/usePagination";
|
import usePagination from "../../hooks/usePagination";
|
||||||
import Pagination from "../common/Pagination";
|
|
||||||
|
|
||||||
const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
|
const ProjectListView = ({
|
||||||
|
currentItems,
|
||||||
|
selectedStatuses,
|
||||||
|
handleStatusChange,
|
||||||
|
setCurrentPage,
|
||||||
|
totalPages,
|
||||||
|
isLoading,
|
||||||
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setMangeProject } = useProjectContext();
|
const { setMangeProject } = useProjectContext();
|
||||||
@ -126,120 +132,152 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card page-min-h py-4 px-6 shadow-sm">
|
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||||
<div className="table-responsive text-nowrap">
|
|
||||||
<table className="table table-hover align-middle m-0">
|
<div
|
||||||
<thead className="border-bottom ">
|
className="table-responsive text-nowrap page-min-h"
|
||||||
<tr>
|
>
|
||||||
|
<table className="table table-hover align-middle m-0">
|
||||||
|
<thead className="border-bottom ">
|
||||||
|
<tr>
|
||||||
|
{projectColumns.map((col) => (
|
||||||
|
<th key={col.key} colSpan={col.colSpan} className={`${col.className} table_header_border`}>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="text-center py-3">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{currentItems?.map((project) => (
|
||||||
|
<tr key={project.id}>
|
||||||
{projectColumns.map((col) => (
|
{projectColumns.map((col) => (
|
||||||
<th
|
<td
|
||||||
key={col.key}
|
key={col.key}
|
||||||
colSpan={col.colSpan}
|
colSpan={col.colSpan}
|
||||||
className={`${col.className} table_header_border`}
|
className={`${col.className} py-5`}
|
||||||
|
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.getValue
|
||||||
</th>
|
? col.getValue(project)
|
||||||
))}
|
: project[col.key] || "N/A"}
|
||||||
<th className="text-center py-3">Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data?.length > 0 ? (
|
|
||||||
data?.map((project) => (
|
|
||||||
<tr key={project.id}>
|
|
||||||
{projectColumns.map((col) => (
|
|
||||||
<td
|
|
||||||
key={col.key}
|
|
||||||
colSpan={col.colSpan}
|
|
||||||
className={`${col.className} py-5`}
|
|
||||||
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
|
||||||
>
|
|
||||||
{col.getValue
|
|
||||||
? col.getValue(project)
|
|
||||||
: project[col.key] || "N/A"}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td
|
|
||||||
className={`mx-2 ${canManageProject ? "d-sm-table-cell" : "d-none"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="dropdown z-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-offset="0,8"
|
|
||||||
data-bs-placement="top"
|
|
||||||
data-bs-custom-class="tooltip-dark"
|
|
||||||
title="More Action"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
aria-label="click to View details"
|
|
||||||
className="dropdown-item cursor-pointer"
|
|
||||||
>
|
|
||||||
<i className="bx bx-detail me-2"></i>
|
|
||||||
<span className="align-left">View details</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item cursor-pointer"
|
|
||||||
onClick={() =>
|
|
||||||
setMangeProject({
|
|
||||||
isOpen: true,
|
|
||||||
Project: project.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="bx bx-pencil me-2"></i>
|
|
||||||
<span className="align-left">Modify</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li onClick={() => handleViewActivities(project.id)}>
|
|
||||||
<a className="dropdown-item cursor-pointer">
|
|
||||||
<i className="bx bx-task me-2"></i>
|
|
||||||
<span className="align-left">Activities</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr
|
|
||||||
className="no-hover"
|
|
||||||
style={{
|
|
||||||
pointerEvents: "none",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
colSpan={projectColumns.length + 1}
|
|
||||||
className="text-center align-middle"
|
|
||||||
style={{ height: "300px", borderBottom: "none" }}
|
|
||||||
>
|
|
||||||
No Infra projects available
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
))}
|
||||||
)}
|
<td
|
||||||
</tbody>
|
className={`mx-2 ${
|
||||||
</table>
|
canManageProject ? "d-sm-table-cell" : "d-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="dropdown z-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
|
data-bs-custom-class="tooltip-dark"
|
||||||
|
title="More Action"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-label="click to View details"
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
>
|
||||||
|
<i className="bx bx-detail me-2"></i>
|
||||||
|
<span className="align-left">View details</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setMangeProject({
|
||||||
|
isOpen: true,
|
||||||
|
Project: project.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-pencil me-2"></i>
|
||||||
|
<span className="align-left">Modify</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li onClick={() => handleViewActivities(project.id)}>
|
||||||
|
<a className="dropdown-item cursor-pointer">
|
||||||
|
<i className="bx bx-task me-2"></i>
|
||||||
|
<span className="align-left">Activities</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
{isLoading && (
|
||||||
currentPage={currentPage}
|
<div className="py-4">
|
||||||
totalPages={totalPages}
|
{" "}
|
||||||
paginate={paginate}
|
{isLoading && <p className="text-center">Loading...</p>}
|
||||||
/>
|
{!isLoading && filteredProjects.length === 0 && (
|
||||||
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && currentItems.length === 0 && (
|
||||||
|
<div className="py-6">
|
||||||
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && totalPages > 1 && (
|
||||||
|
<nav>
|
||||||
|
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||||
|
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{[...Array(totalPages)].map((_, i) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={`page-item ${
|
||||||
|
currentPage === totalPages && "disabled"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
import ReactApexChart from "react-apexcharts";
|
import ReactApexChart from "react-apexcharts";
|
||||||
import Chart from "react-apexcharts";
|
import Chart from "react-apexcharts";
|
||||||
|
|
||||||
const ProjectStatistics = ({ project }) => {
|
const ProjectOverview = ({ project }) => {
|
||||||
const { data } = useProjects();
|
const { data } = useProjects();
|
||||||
const [current_project, setCurrentProject] = useState(
|
const [current_project, setCurrentProject] = useState(
|
||||||
data?.find((pro) => pro.id == project)
|
data?.find((pro) => pro.id == project)
|
||||||
@ -165,7 +165,7 @@ const ProjectStatistics = ({ project }) => {
|
|||||||
}, [selectedProject]);
|
}, [selectedProject]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card h-100">
|
<div className="card" style={{ minHeight: "490px" }}>
|
||||||
<div className="card-header text-start">
|
<div className="card-header text-start">
|
||||||
<h5 className="card-action-title mb-0">
|
<h5 className="card-action-title mb-0">
|
||||||
{" "}
|
{" "}
|
||||||
@ -173,78 +173,78 @@ const ProjectStatistics = ({ project }) => {
|
|||||||
<span className="ms-2 fw-bold">Project Statistics</span>
|
<span className="ms-2 fw-bold">Project Statistics</span>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<ul className="list-unstyled m-0 p-0">
|
<ul className="list-unstyled m-0 p-0">
|
||||||
<li className="d-flex flex-wrap">
|
<li className="d-flex flex-wrap">
|
||||||
<div className="w-100 d-flex flex-wrap">
|
<div className="w-100 d-flex flex-wrap">
|
||||||
{/* Centered Chart */}
|
{/* Centered Chart */}
|
||||||
<div className="w-100 d-flex justify-content-center mb-3">
|
<div className="w-100 d-flex justify-content-center mb-3">
|
||||||
<div >
|
<div >
|
||||||
<Chart
|
<Chart
|
||||||
options={radialBarOptions}
|
options={radialBarOptions}
|
||||||
series={radialBarOptions.series}
|
series={radialBarOptions.series}
|
||||||
type="radialBar"
|
type="radialBar"
|
||||||
height="100%"
|
height="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Section */}
|
||||||
|
<div className="mb-2" style={{ flex: "1 1 auto" }}>
|
||||||
|
<div>
|
||||||
|
{/* Tasks Planned */}
|
||||||
|
<div className="d-flex align-items-center mb-3">
|
||||||
|
<div className="avatar me-2">
|
||||||
|
<span className="avatar-initial rounded-2 bg-label-primary">
|
||||||
|
<i className="bx bx-check text-primary fs-4"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="d-flex flex-column text-start">
|
||||||
{/* Info Section */}
|
<small className="fw-bold">Tasks Planned</small>
|
||||||
<div className="mb-2" style={{ flex: "1 1 auto" }}>
|
<h5 className="mb-0">
|
||||||
<div>
|
{FormattedNumber(current_project?.plannedWork)}
|
||||||
{/* Tasks Planned */}
|
</h5>
|
||||||
<div className="d-flex align-items-center mb-3">
|
|
||||||
<div className="avatar me-2">
|
|
||||||
<span className="avatar-initial rounded-2 bg-label-primary">
|
|
||||||
<i className="bx bx-check text-primary fs-4"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex flex-column text-start">
|
|
||||||
<small className="fw-bold">Tasks Planned</small>
|
|
||||||
<h5 className="mb-0">
|
|
||||||
{FormattedNumber(current_project?.plannedWork)}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tasks Completed */}
|
|
||||||
<div className="d-flex align-items-center mb-3">
|
|
||||||
<div className="avatar me-2">
|
|
||||||
<span className="avatar-initial rounded-2 bg-label-info">
|
|
||||||
<i className="bx bx-star text-info fs-4"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex flex-column text-start">
|
|
||||||
<small className="fw-bold">Tasks Completed</small>
|
|
||||||
<h5 className="mb-0">
|
|
||||||
{FormattedNumber(current_project?.completedWork)}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Team Size */}
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<div className="avatar me-2">
|
|
||||||
<span className="avatar-initial rounded-2 bg-label-primary">
|
|
||||||
<i className="bx bx-group text-primary fs-4"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex flex-column text-start">
|
|
||||||
<small className="fw-bold">Current Team Size</small>
|
|
||||||
<h5 className="mb-0">
|
|
||||||
{FormattedNumber(current_project?.teamSize)}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
</ul>
|
{/* Tasks Completed */}
|
||||||
|
<div className="d-flex align-items-center mb-3">
|
||||||
|
<div className="avatar me-2">
|
||||||
|
<span className="avatar-initial rounded-2 bg-label-info">
|
||||||
|
<i className="bx bx-star text-info fs-4"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column text-start">
|
||||||
|
<small className="fw-bold">Tasks Completed</small>
|
||||||
|
<h5 className="mb-0">
|
||||||
|
{FormattedNumber(current_project?.completedWork)}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Team Size */}
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<div className="avatar me-2">
|
||||||
|
<span className="avatar-initial rounded-2 bg-label-primary">
|
||||||
|
<i className="bx bx-group text-primary fs-4"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column text-start">
|
||||||
|
<small className="fw-bold">Current Team Size</small>
|
||||||
|
<h5 className="mb-0">
|
||||||
|
{FormattedNumber(current_project?.teamSize)}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectStatistics;
|
export default ProjectOverview;
|
||||||
@ -104,6 +104,7 @@ const hasChanges =
|
|||||||
permission: payloadPermissions,
|
permission: payloadPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Final payload:", payload);
|
||||||
updatePermission(payload);
|
updatePermission(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,11 @@ import {
|
|||||||
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
|
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
|
||||||
import showToast from "../../../services/toastService";
|
import showToast from "../../../services/toastService";
|
||||||
import { useOrganizationEmployees } from "../../../hooks/useOrganization";
|
import { useOrganizationEmployees } from "../../../hooks/useOrganization";
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { changeMaster } from "../../../slices/localVariablesSlice";
|
|
||||||
|
|
||||||
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const debounceSearchTerm = useDebounce(searchTerm, 500);
|
const debounceSearchTerm = useDebounce(searchTerm, 500);
|
||||||
const dispatch = useDispatch();
|
|
||||||
const {
|
const {
|
||||||
data: employeesData = [],
|
data: employeesData = [],
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -47,7 +45,6 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(changeMaster("Job Role"));
|
|
||||||
if (employeesData?.data?.length > 0) {
|
if (employeesData?.data?.length > 0) {
|
||||||
const available = employeesData.data.filter((emp) => {
|
const available = employeesData.data.filter((emp) => {
|
||||||
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
|
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
|
||||||
@ -122,7 +119,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
status: true,
|
status: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
handleAssignEmployee({ payload, actionType: "assign" });
|
handleAssignEmployee({ payload,actionType:"assign"} );
|
||||||
|
|
||||||
setEmployees((prev) =>
|
setEmployees((prev) =>
|
||||||
prev.map((emp) => ({
|
prev.map((emp) => ({
|
||||||
@ -135,26 +132,26 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>);
|
return ( <div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="page-min-h d-flex justify-content-center align-items-center ">
|
<div className="page-min-h d-flex justify-content-center align-items-center ">
|
||||||
{error?.status === 400 ? (
|
{error?.status === 400 ? (
|
||||||
<p className="m-0">Enter employee you want to find.</p>
|
<p className="m-0">Enter employee you want to find.</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="m-0 dange-text">Something went wrong. Please try again later.</p>
|
<p className="m-0 dange-text">Something went wrong. Please try again later.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (employees.length === 0) {
|
if (employees.length === 0) {
|
||||||
return (<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>);
|
return(<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -186,8 +183,9 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleSelectChange(index, "serviceId", e.target.value)
|
handleSelectChange(index, "serviceId", e.target.value)
|
||||||
}
|
}
|
||||||
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${emp.errors.serviceId ? "is-invalid" : ""
|
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
|
||||||
}`}
|
emp.errors.serviceId ? "is-invalid" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">Select Service</option>
|
<option value="">Select Service</option>
|
||||||
{services?.map((s) => (
|
{services?.map((s) => (
|
||||||
@ -207,8 +205,9 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleSelectChange(index, "jobRole", e.target.value)
|
handleSelectChange(index, "jobRole", e.target.value)
|
||||||
}
|
}
|
||||||
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${emp.errors.jobRole ? "is-invalid" : ""
|
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
|
||||||
}`}
|
emp.errors.jobRole ? "is-invalid" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<option value="">Select Job Role</option>
|
<option value="">Select Job Role</option>
|
||||||
{jobRoles?.map((r) => (
|
{jobRoles?.map((r) => (
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import InputSuggestions from "../common/InputSuggestion";
|
|||||||
import { useEmployeesName } from "../../hooks/useEmployees";
|
import { useEmployeesName } from "../../hooks/useEmployees";
|
||||||
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
|
||||||
import HoverPopup from "../common/HoverPopup";
|
import HoverPopup from "../common/HoverPopup";
|
||||||
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
|
|
||||||
|
|
||||||
const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
||||||
const {
|
const {
|
||||||
@ -132,9 +131,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
}
|
}
|
||||||
}, [currencyData, requestToEdit, setValue]);
|
}, [currencyData, requestToEdit, setValue]);
|
||||||
|
|
||||||
const StrikeDate = watch("strikeDate");
|
const StrikeDate = watch("strikeDate")
|
||||||
|
|
||||||
const onSubmit = (fromdata) => {
|
const onSubmit = (fromdata) => {
|
||||||
|
console.log(fromdata);
|
||||||
let payload = {
|
let payload = {
|
||||||
...fromdata,
|
...fromdata,
|
||||||
strikeDate: fromdata.strikeDate
|
strikeDate: fromdata.strikeDate
|
||||||
@ -164,7 +164,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
{/* Project and Category */}
|
{/* Project and Category */}
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
{/* <select
|
<Label className="form-label" required>
|
||||||
|
Select Project
|
||||||
|
</Label>
|
||||||
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
{...register("projectId")}
|
{...register("projectId")}
|
||||||
>
|
>
|
||||||
@ -178,19 +181,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</select> */}
|
</select>
|
||||||
<SelectProjectField
|
|
||||||
label="Select Project"
|
|
||||||
required
|
|
||||||
placeholder="Select Project"
|
|
||||||
value={watch("projectId")}
|
|
||||||
onChange={(val) =>
|
|
||||||
setValue("projectId", val, {
|
|
||||||
shouldDirty: true,
|
|
||||||
shouldValidate: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.projectId && (
|
{errors.projectId && (
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
<small className="danger-text">{errors.projectId.message}</small>
|
||||||
)}
|
)}
|
||||||
@ -245,24 +236,20 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6 mt-2">
|
<div className="col-md-6 mt-2">
|
||||||
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
|
<div className="d-flex justify-content-start align-items-center gap-2">
|
||||||
<Label htmlFor="isVariable" className="form-label mb-0" required>
|
<Label htmlFor="isVariable" className="form-label mb-0" required>
|
||||||
Payment Type
|
Payment Type
|
||||||
</Label>
|
</Label>
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
title="Payment Type"
|
title="Payment Type"
|
||||||
id="payment_type"
|
|
||||||
content={
|
content={
|
||||||
<div className=" w-50">
|
<p>
|
||||||
<p>
|
Choose whether the payment amount varies or remains fixed each cycle.
|
||||||
Choose whether the payment amount varies or remains fixed
|
<br />
|
||||||
each cycle.
|
<strong>Is Variable:</strong> Amount changes per cycle.
|
||||||
<br />
|
<br />
|
||||||
<strong>Is Variable:</strong> Amount changes per cycle.
|
<strong>Fixed:</strong> Amount stays constant.
|
||||||
<br />
|
</p>
|
||||||
<strong>Fixed:</strong> Amount stays constant.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
||||||
@ -283,10 +270,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
checked={field.value === true}
|
checked={field.value === true}
|
||||||
onChange={() => field.onChange(true)}
|
onChange={() => field.onChange(true)}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label htmlFor="isVariableTrue" className="form-check-label">
|
||||||
htmlFor="isVariableTrue"
|
|
||||||
className="form-check-label"
|
|
||||||
>
|
|
||||||
Is Variable
|
Is Variable
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@ -299,10 +283,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
checked={field.value === false}
|
checked={field.value === false}
|
||||||
onChange={() => field.onChange(false)}
|
onChange={() => field.onChange(false)}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label htmlFor="isVariableFalse" className="form-check-label">
|
||||||
htmlFor="isVariableFalse"
|
|
||||||
className="form-check-label"
|
|
||||||
>
|
|
||||||
Fixed
|
Fixed
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@ -314,6 +295,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.isVariable.message}</small>
|
<small className="danger-text">{errors.isVariable.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date and Amount */}
|
{/* Date and Amount */}
|
||||||
@ -406,15 +388,13 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
</Label>
|
</Label>
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
title="Frequency"
|
title="Frequency"
|
||||||
id="frequency"
|
|
||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
Defines how often payments or billing occur, such as
|
Defines how often payments or billing occur, such as monthly, quarterly, or annually.
|
||||||
monthly, quarterly, or annually.
|
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
||||||
</HoverPopup>
|
</HoverPopup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -463,26 +443,21 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
|
|
||||||
{/* Payment Buffer Days and End Date */}
|
{/* Payment Buffer Days and End Date */}
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
|
<div className="d-flex justify-content-start align-items-center gap-2">
|
||||||
<Label
|
<Label htmlFor="paymentBufferDays" className="form-label mb-0" required>
|
||||||
htmlFor="paymentBufferDays"
|
|
||||||
className="form-label mb-0 "
|
|
||||||
required
|
|
||||||
>
|
|
||||||
Payment Buffer Days
|
Payment Buffer Days
|
||||||
</Label>
|
</Label>
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
title="Payment Buffer Days"
|
title="Payment Buffer Days"
|
||||||
id="payment_buffer_days"
|
|
||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
Number of extra days allowed after the due date before
|
Number of extra days allowed after the due date before payment is considered late.
|
||||||
payment is considered late.
|
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
||||||
</HoverPopup>
|
</HoverPopup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -503,21 +478,21 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
|
<div className="d-flex justify-content-start align-items-center gap-2">
|
||||||
<Label htmlFor="endDate" className="form-label mb-0" required>
|
<Label htmlFor="endDate" className="form-label mb-0" required>
|
||||||
End Date
|
End Date
|
||||||
</Label>
|
</Label>
|
||||||
<HoverPopup
|
<HoverPopup
|
||||||
title="End Date"
|
title="End Date"
|
||||||
id="end_date"
|
|
||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
The date when the last payment in the recurrence occurs.
|
The date when the last payment in the recurrence occurs.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
|
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
|
||||||
</HoverPopup>
|
</HoverPopup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -532,8 +507,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
<small className="danger-text">{errors.endDate.message}</small>
|
<small className="danger-text">{errors.endDate.message}</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
<div className="row my-2 text-start">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Label htmlFor="notifyTo" className="form-label" required>
|
<Label htmlFor="notifyTo" className="form-label" required>
|
||||||
@ -592,8 +569,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
|
|||||||
{createPending || isPending
|
{createPending || isPending
|
||||||
? "Please wait...."
|
? "Please wait...."
|
||||||
: requestToEdit
|
: requestToEdit
|
||||||
? "Update"
|
? "Update"
|
||||||
: "Save as Draft"}
|
: "Save as Draft"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -166,7 +166,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
console.log("Tanish",filteredData)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{IsDeleteModalOpen && (
|
{IsDeleteModalOpen && (
|
||||||
@ -203,7 +203,7 @@ console.log("Tanish",filteredData)
|
|||||||
<tr
|
<tr
|
||||||
key={recurringExpense.id}
|
key={recurringExpense.id}
|
||||||
className="align-middle"
|
className="align-middle"
|
||||||
style={{ height: "40px" }}
|
style={{ height: "50px" }}
|
||||||
>
|
>
|
||||||
{recurringExpenseColumns.map((col) => (
|
{recurringExpenseColumns.map((col) => (
|
||||||
<td
|
<td
|
||||||
|
|||||||
@ -1,272 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
daysLeft,
|
|
||||||
getJobStatusBadge,
|
|
||||||
getNextBadgeColor,
|
|
||||||
} from "../../utils/appUtils";
|
|
||||||
import { useServiceProjectJobs, useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
|
|
||||||
import { ITEMS_PER_PAGE, JOBS_STATUS_IDS } from "../../utils/constants";
|
|
||||||
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
|
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import ProjectPage from "../../pages/project/ProjectPage";
|
|
||||||
import { useServiceProjectJobContext } from "./Jobs";
|
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
|
||||||
|
|
||||||
const JobList = ({ isArchive }) => {
|
|
||||||
const { setSelectedJob, setManageJob } = useServiceProjectJobContext();
|
|
||||||
const { mutate: UpdateJob,isPending } = useUpdateServiceProjectJob(() => {
|
|
||||||
});
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const { data, isLoading, isError, error } = useServiceProjectJobs(
|
|
||||||
ITEMS_PER_PAGE,
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
projectId,
|
|
||||||
isArchive
|
|
||||||
);
|
|
||||||
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
|
|
||||||
const [archiveJobId, setArchiveJobId] = useState(null);
|
|
||||||
const [isArchiveAction, setIsArchiveAction] = useState(true);
|
|
||||||
|
|
||||||
const handleArchive = () => {
|
|
||||||
const payload = [
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/isArchive",
|
|
||||||
value: isArchiveAction,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
UpdateJob({
|
|
||||||
id: archiveJobId,
|
|
||||||
payload,
|
|
||||||
isArchiveAction,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsArchiveModalOpen(false);
|
|
||||||
setArchiveJobId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const jobGrid = [
|
|
||||||
{
|
|
||||||
key: "jobTicketUId",
|
|
||||||
label: "Job Id",
|
|
||||||
getValue: (e) => <span>{e?.jobTicketUId || "N/A"}</span>,
|
|
||||||
align: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "title",
|
|
||||||
label: "Title",
|
|
||||||
getValue: (e) => (
|
|
||||||
<span
|
|
||||||
className={`fw-semibold text-truncate d-inline-block ${!isArchive ? "cursor-pointer" : ""}`}
|
|
||||||
style={{
|
|
||||||
maxWidth: "100%",
|
|
||||||
width: "210px",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isArchive) {
|
|
||||||
setSelectedJob({ showCanvas: true, job: e?.id });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={e?.title}
|
|
||||||
>
|
|
||||||
{e?.title}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
isAlwaysVisible: true,
|
|
||||||
className: "text-start",
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
key: "dueDate",
|
|
||||||
label: "Due On",
|
|
||||||
getValue: (e) => formatUTCToLocalTime(e.startDate),
|
|
||||||
isAlwaysVisible: true,
|
|
||||||
className: "text-start d-none d-sm-table-cell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
label: "Status",
|
|
||||||
getValue: (e) => {
|
|
||||||
return (
|
|
||||||
<span className={`badge ${getJobStatusBadge(e?.status?.id)}`}>
|
|
||||||
{e?.status?.displayName}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isAlwaysVisible: true,
|
|
||||||
className: "text-start d-none d-sm-table-cell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "daysLeft",
|
|
||||||
label: "Days Left",
|
|
||||||
getValue: (e) => {
|
|
||||||
const { days, color } = daysLeft(e.startDate, e.dueDate);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={`badge bg-${color}`}>
|
|
||||||
{days !== null ? `${days} days` : "N/A"}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isAlwaysVisible: true,
|
|
||||||
className: "text-start d-none d-sm-table-cell",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const canArchive = (statusId) => {
|
|
||||||
const closedId = JOBS_STATUS_IDS.find((s) => s.label === "Closed")?.id;
|
|
||||||
const reviewDoneId = JOBS_STATUS_IDS.find((s) => s.label === "Review Done")?.id;
|
|
||||||
|
|
||||||
return statusId === closedId || statusId === reviewDoneId;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isArchiveModalOpen && (
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isArchiveModalOpen}
|
|
||||||
type={isArchiveAction ? "archive" : "Un-archive"}
|
|
||||||
header={isArchiveAction ? "Archive Job" : "Restore Job"}
|
|
||||||
message={
|
|
||||||
isArchiveAction
|
|
||||||
? "Are you sure you want to archive this job?"
|
|
||||||
: "Are you sure you want to restore this job?"
|
|
||||||
}
|
|
||||||
onSubmit={handleArchive}
|
|
||||||
onClose={() => setIsArchiveModalOpen(false)}
|
|
||||||
loading={isPending}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
<div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive">
|
|
||||||
<table
|
|
||||||
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive"
|
|
||||||
aria-describedby="DataTables_Table_0_info"
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{jobGrid.map((col) => (
|
|
||||||
<th
|
|
||||||
key={col.key}
|
|
||||||
className={`${col.align || "text-center"} ${col.className || ""
|
|
||||||
}`}
|
|
||||||
scope="col"
|
|
||||||
>
|
|
||||||
<div className={col.className}>{col.label}</div>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
<th className="sorting_disabled text-center" aria-label="Actions">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{Array.isArray(data?.data) && data.data.length > 0 ? (
|
|
||||||
data.data.map((row, i) => (
|
|
||||||
<tr key={i} className="text-start">
|
|
||||||
{jobGrid.map((col) => (
|
|
||||||
<td
|
|
||||||
key={col.key}
|
|
||||||
className={col.className}
|
|
||||||
// onClick={() =>
|
|
||||||
// setSelectedJob({ showCanvas: true, job: row?.id })
|
|
||||||
// }
|
|
||||||
onClick={() => {
|
|
||||||
if (!isArchive) {
|
|
||||||
setSelectedJob({ showCanvas: true, job: e?.id });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
|
|
||||||
>
|
|
||||||
{col.getValue(row)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td>
|
|
||||||
<div className="dropdown text-center">
|
|
||||||
<button
|
|
||||||
className="btn btn-icon dropdown-toggle hide-arrow"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
<i className="bx bx-dots-vertical-rounded bx-md"></i>
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
|
||||||
|
|
||||||
{!isArchive && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="dropdown-item py-1"
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedJob({ showCanvas: true, job: row?.id })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="bx bx-detail bx-sm"></i> View
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="dropdown-item py-1"
|
|
||||||
onClick={() =>
|
|
||||||
setManageJob({ isOpen: true, jobId: row?.id })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="bx bx-edit bx-sm"></i> Edit
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isArchive && (
|
|
||||||
<button
|
|
||||||
className="dropdown-item py-1"
|
|
||||||
onClick={() => {
|
|
||||||
setArchiveJobId(row.id);
|
|
||||||
setIsArchiveAction(false);
|
|
||||||
setIsArchiveModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="bx bx-reset bx-sm"></i> Restore
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isArchive && canArchive(row?.status?.id) && (
|
|
||||||
<button
|
|
||||||
className="dropdown-item py-1"
|
|
||||||
onClick={() => {
|
|
||||||
setArchiveJobId(row.id);
|
|
||||||
setIsArchiveAction(true);
|
|
||||||
setIsArchiveModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="bx bx-archive bx-sm"></i> Archive
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr style={{ height: "200px" }}>
|
|
||||||
<td
|
|
||||||
colSpan={jobGrid.length + 1}
|
|
||||||
className="text-center border-0 align-middle"
|
|
||||||
>
|
|
||||||
{isLoading ? <SpinnerLoader /> : "Not Found Jobs."}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default JobList;
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
|
||||||
import JobList from "./JobList";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useServiceProjects } from "../../hooks/useServiceProject";
|
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
|
||||||
import OffcanvasComponent from "../common/OffcanvasComponent";
|
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import ManageJob from "./ServiceProjectJob/ManageJob";
|
|
||||||
import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket";
|
|
||||||
import GlobalModel from "../common/GlobalModel";
|
|
||||||
import PreviewDocument from "../Expenses/PreviewDocument";
|
|
||||||
|
|
||||||
export const JonContext = createContext();
|
|
||||||
export const useServiceProjectJobContext = () => {
|
|
||||||
const context = useContext(JonContext);
|
|
||||||
if (!context) {
|
|
||||||
showToast("Something went wrong", "warning");
|
|
||||||
window.location = "/dashboard";
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
const Jobs = () => {
|
|
||||||
const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null });
|
|
||||||
const [showArchive, setShowArchive] = useState(false);
|
|
||||||
|
|
||||||
const [showCanvas, setShowCanvas] = useState(false);
|
|
||||||
const [selectedProject, setSelectedProject] = useState(null);
|
|
||||||
const [selectJob, setSelectedJob] = useState({
|
|
||||||
showCanvas: false,
|
|
||||||
job: null,
|
|
||||||
});
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { data } = useServiceProjects(ITEMS_PER_PAGE, 1);
|
|
||||||
|
|
||||||
const contextProvider = {
|
|
||||||
setSelectedJob,
|
|
||||||
setSelectedProject,
|
|
||||||
setManageJob,
|
|
||||||
manageJob,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<JonContext.Provider value={contextProvider}>
|
|
||||||
<OffcanvasComponent
|
|
||||||
id="customCanvas"
|
|
||||||
title="Job"
|
|
||||||
placement="end"
|
|
||||||
show={selectJob.showCanvas}
|
|
||||||
onClose={() => setSelectedJob({ showCanvas: false, job: null })}
|
|
||||||
>
|
|
||||||
<ManageJobTicket Job={selectJob} />
|
|
||||||
</OffcanvasComponent>
|
|
||||||
<OffcanvasComponent
|
|
||||||
id="customCanvas1"
|
|
||||||
title={`${manageJob.jobId ? "Update a Job" : "Create a new Job"} `}
|
|
||||||
placement="end"
|
|
||||||
show={manageJob.isOpen}
|
|
||||||
onClose={() => setManageJob({ isOpen: false, jobId: null })}
|
|
||||||
>
|
|
||||||
<ManageJob Job={manageJob.jobId} />
|
|
||||||
</OffcanvasComponent>
|
|
||||||
<div className="card page-min-h my-2 px-7 pb-4">
|
|
||||||
<div className="row align-items-center py-4">
|
|
||||||
|
|
||||||
{/* LEFT — Tabs */}
|
|
||||||
<div className="col-12 col-md-6 text-start">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn btn-sm ${showArchive ? "btn-secondary" : "btn-outline-secondary"}`}
|
|
||||||
onClick={() => setShowArchive(!showArchive)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-archive bx-sm me-1 mt-1"></i> Archived
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* RIGHT — New Job button */}
|
|
||||||
<div className="col-12 col-md-6 d-flex justify-content-md-end mt-2 mt-md-0">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
onClick={() => setManageJob({ isOpen: true, jobId: null })}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle bx-md me-2"></i>New Job
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Job List */}
|
|
||||||
<JobList filterByProject={selectedProject} isArchive={showArchive} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</JonContext.Provider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Jobs;
|
|
||||||
@ -1,328 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { defaultProjectValues } from "./ServiceProjectSchema";
|
|
||||||
import Label from "../common/Label";
|
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
useGlobalServices,
|
|
||||||
useServices,
|
|
||||||
} from "../../hooks/masterHook/useMaster";
|
|
||||||
import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
|
|
||||||
import DatePicker from "../common/DatePicker";
|
|
||||||
|
|
||||||
import SelectMultiple from "../common/SelectMultiple";
|
|
||||||
import { projectSchema } from "./ServiceProjectSchema";
|
|
||||||
import {
|
|
||||||
useOrganization,
|
|
||||||
useOrganizationModal,
|
|
||||||
useOrganizationsList,
|
|
||||||
} from "../../hooks/useOrganization";
|
|
||||||
import { error } from "pdf-lib";
|
|
||||||
import {
|
|
||||||
useCreateServiceProject,
|
|
||||||
useServiceProject,
|
|
||||||
useUpdateServiceProject,
|
|
||||||
} from "../../hooks/useServiceProject";
|
|
||||||
|
|
||||||
const ManageServiceProject = ({ serviceProjectId, onClose }) => {
|
|
||||||
const {
|
|
||||||
data: projectdata,
|
|
||||||
isLoading: isProjectLoading,
|
|
||||||
isProjectError,
|
|
||||||
} = useServiceProject(serviceProjectId);
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
const methods = useForm({
|
|
||||||
resolver: zodResolver(projectSchema),
|
|
||||||
defaultValues: defaultProjectValues,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = methods;
|
|
||||||
const { data, isError, isLoading } = useServices();
|
|
||||||
const { data: organization, isLoading: isLoadingOrganization } =
|
|
||||||
useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
|
||||||
|
|
||||||
const { mutate: CreateServiceProject, isPending } = useCreateServiceProject(
|
|
||||||
() => {
|
|
||||||
reset();
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const { mutate: UpdateServiceProject, isPending: isUpdating } =
|
|
||||||
useUpdateServiceProject(() => {
|
|
||||||
reset();
|
|
||||||
onClose();
|
|
||||||
});
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
|
|
||||||
if (serviceProjectId) {
|
|
||||||
let existingServiceIds = projectdata?.services?.map((s) => s.id) || [];
|
|
||||||
|
|
||||||
const oldServices = projectdata.services.map((service) => ({
|
|
||||||
serviceId: service.id,
|
|
||||||
isActive: formData.services.includes(service.id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const newServices = formData.services
|
|
||||||
.filter((s) => !existingServiceIds.includes(s))
|
|
||||||
.map((service) => ({ serviceId: service, isActive: true }));
|
|
||||||
|
|
||||||
formData.services = [...oldServices, ...newServices];
|
|
||||||
} else {
|
|
||||||
formData.services = formData.services.map((service) => ({
|
|
||||||
serviceId: service,
|
|
||||||
isActive: true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceProjectId) {
|
|
||||||
let payload = { ...formData, id: serviceProjectId };
|
|
||||||
|
|
||||||
UpdateServiceProject({ id: serviceProjectId, payload });
|
|
||||||
} else {
|
|
||||||
CreateServiceProject(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectdata) {
|
|
||||||
const activeServiceIds = (projectdata.services || [])
|
|
||||||
.map((s) => s.serviceId || s.id)
|
|
||||||
.filter(Boolean);
|
|
||||||
reset({
|
|
||||||
name: projectdata.name,
|
|
||||||
shortName: projectdata.shortName,
|
|
||||||
clientId: projectdata.client.id,
|
|
||||||
assignedDate: projectdata.assignedDate,
|
|
||||||
address: projectdata.address,
|
|
||||||
statusId: projectdata.status.id,
|
|
||||||
contactEmail: projectdata.contactEmail,
|
|
||||||
contactPhone: projectdata.contactPhone,
|
|
||||||
contactName: projectdata.contactName,
|
|
||||||
services: activeServiceIds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [projectdata]);
|
|
||||||
|
|
||||||
const { onOpen: openOrgModal } = useOrganizationModal();
|
|
||||||
return (
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<form className="px-3 py-2" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className=" text-center">
|
|
||||||
<h5>{serviceProjectId ? "Update Service Project" : "Create Service Project"}</h5>
|
|
||||||
</div>
|
|
||||||
<div className="row text-start">
|
|
||||||
<div className="col-12 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Client
|
|
||||||
</Label>
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<select
|
|
||||||
className="select2 form-select form-select-sm flex-grow-1"
|
|
||||||
aria-label="Default select example"
|
|
||||||
{...register("clientId", {
|
|
||||||
required: "Client is required",
|
|
||||||
valueAsNumber: false,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<option>Loading...</option>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<option value="">Select Client</option>
|
|
||||||
{organization?.data?.map((org) => (
|
|
||||||
<option key={org.id} value={org.id}>
|
|
||||||
{org.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle bx-xs cursor-pointer text-primary"
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
openOrgModal({ startStep: 2 }); // Step 4 = ManagOrg
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors?.clientId && (
|
|
||||||
<span className="danger-text">{errors.clientId.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Project Name
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("name")}
|
|
||||||
placeholder="Enter Project Name.."
|
|
||||||
/>
|
|
||||||
{errors?.name && (
|
|
||||||
<span className="danger-text">{errors.name.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Short Name (Short Project Name)
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("shortName")}
|
|
||||||
placeholder="Enter Project Short Name.."
|
|
||||||
/>
|
|
||||||
{errors?.shortName && (
|
|
||||||
<span className="danger-text">{errors.shortName.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Select Status
|
|
||||||
</Label>
|
|
||||||
<select
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
{...register("statusId")}
|
|
||||||
>
|
|
||||||
<option>Select Service</option>
|
|
||||||
{PROJECT_STATUS?.map((status) => (
|
|
||||||
<option key={status.id} value={status.id}>{status.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{errors?.statusId && (
|
|
||||||
<span className="danger-text">{errors.statusId.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 mb-2">
|
|
||||||
<SelectMultiple
|
|
||||||
options={data?.data}
|
|
||||||
isLoading={isLoading}
|
|
||||||
name="services"
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
label="Select Service"
|
|
||||||
/>
|
|
||||||
{errors?.services && (
|
|
||||||
<span className="danger-text">{errors.services.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Contact Person
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("contactName")}
|
|
||||||
placeholder="Enter Employee name.."
|
|
||||||
onInput={(e) => {
|
|
||||||
e.target.value = e.target.value.replace(/[^A-Za-z ]/g, "");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{errors?.contactName && (
|
|
||||||
<span className="danger-text">{errors.contactName.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Contact Email
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("contactEmail")}
|
|
||||||
placeholder="Enter Employee Email.."
|
|
||||||
/>
|
|
||||||
{errors?.contactEmail && (
|
|
||||||
<span className="danger-text">{errors.contactEmail.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Contact Number
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
maxLength={10}
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("contactPhone")}
|
|
||||||
placeholder="Enter Employee Contact.."
|
|
||||||
onInput={(e) => {
|
|
||||||
e.target.value = e.target.value.replace(/[^0-9]/g, "");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{errors?.contactPhone && (
|
|
||||||
<span className="danger-text">{errors.contactPhone.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Assign Date
|
|
||||||
</Label>
|
|
||||||
<DatePicker
|
|
||||||
name="assignedDate"
|
|
||||||
className="w-100"
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-12 mb-2">
|
|
||||||
<Label htmlFor="address" required>
|
|
||||||
Project Address
|
|
||||||
</Label>
|
|
||||||
<div className="input-group">
|
|
||||||
<textarea
|
|
||||||
name="address"
|
|
||||||
className="form-control"
|
|
||||||
// maxLength={maxAddressLength}
|
|
||||||
{...register("address")}
|
|
||||||
placeholder="Enter Project Address.."
|
|
||||||
// onChange={(e) => {
|
|
||||||
// setAddressLength(e.target.value.length);
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-end" style={{ fontSize: "12px" }}>
|
|
||||||
{/* {maxAddressLength - addressLength} characters left */}
|
|
||||||
</div>
|
|
||||||
{errors.address && (
|
|
||||||
<div className="danger-text text-start">
|
|
||||||
{errors.address.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-end gap-4">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-secondary"
|
|
||||||
disabled={isPending || isUpdating}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
disabled={isPending || isUpdating}
|
|
||||||
>
|
|
||||||
{isPending || isUpdating
|
|
||||||
? "Please wait..."
|
|
||||||
: serviceProjectId
|
|
||||||
? "Update"
|
|
||||||
: "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageServiceProject;
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { formatUTCToLocalTime } from '../../utils/dateUtils'
|
|
||||||
|
|
||||||
const ServiceProfile = ({data,setIsOpenModal}) => {
|
|
||||||
return (
|
|
||||||
<div className="card mb-4 h-100">
|
|
||||||
<div className="card-header text-start">
|
|
||||||
<h5 className="card-action-title mb-0 ps-1">
|
|
||||||
{" "}
|
|
||||||
<i className="fa fa-building rounded-circle text-primary"></i>
|
|
||||||
<span className="ms-2 fw-bold">Project Profile</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
<ul className="list-unstyled my-3 ps-0 text-start">
|
|
||||||
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
|
|
||||||
<i className="bx bx-cog"></i>
|
|
||||||
<span className="fw-medium mx-2 text-nowrap">Name:</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-grow-1 text-start text-wrap">
|
|
||||||
{data.name}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-fingerprint"></i>
|
|
||||||
<span className="fw-medium mx-2">Nick Name:</span>
|
|
||||||
</div>
|
|
||||||
<span>{data.shortName}</span>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-check"></i>
|
|
||||||
<span className="fw-medium mx-2">Assign Date:</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-trophy"></i>
|
|
||||||
<span className="fw-medium mx-2">Status:</span>
|
|
||||||
</div>
|
|
||||||
<span>{data?.status.status}</span>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
<div className="d-flex align-items-center" style={{ width: '150px' }}>
|
|
||||||
<i className="bx bx-user"></i>
|
|
||||||
<span className="fw-medium mx-2">Contact:</span>
|
|
||||||
</div>
|
|
||||||
<span>{data.contactName}</span>
|
|
||||||
</li>
|
|
||||||
<li className="d-flex mb-3">
|
|
||||||
{/* Label section with icon */}
|
|
||||||
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
|
|
||||||
<i className="bx bx-flag mt-1"></i>
|
|
||||||
<span className="fw-medium mx-2 text-nowrap">Address:</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content section that wraps nicely */}
|
|
||||||
<div className="flex-grow-1 text-start text-wrap">
|
|
||||||
{data.address}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li className="d-flex justify-content-center mt-4">
|
|
||||||
|
|
||||||
<a className="d-flex justify-content-center mt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#edit-project-modal"
|
|
||||||
onClick={() => setIsOpenModal(true)}
|
|
||||||
>
|
|
||||||
Modify Details
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ServiceProfile
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useBranchDetails } from "../../../hooks/useServiceProject";
|
|
||||||
import { SpinnerLoader } from "../../common/Loader";
|
|
||||||
import Error from "../../common/Error";
|
|
||||||
import { BranchDetailsSkeleton } from "../ServiceProjectSeketon";
|
|
||||||
|
|
||||||
const BranchDetails = ({ branch }) => {
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const { data, isLoading, isError, error } = useBranchDetails(branch);
|
|
||||||
|
|
||||||
if (isLoading) return <BranchDetailsSkeleton />;
|
|
||||||
if (isError) return <Error error={error} />;
|
|
||||||
|
|
||||||
let contactInfo = [];
|
|
||||||
try {
|
|
||||||
contactInfo = JSON.parse(data?.contactInformation || "[]");
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const googleMapUrl = data?.googleMapUrl || data?.locationLink;
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
if (!googleMapUrl) return;
|
|
||||||
await navigator.clipboard.writeText(googleMapUrl);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-100">
|
|
||||||
<div className="d-flex mb-2 align-items-center">
|
|
||||||
<i className="bx bx-buildings bx-sm me-2 text-primary"></i>
|
|
||||||
<span className="fw-semibold">Branch Details</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DetailRow label="Branch Name" value={data?.branchName} />
|
|
||||||
|
|
||||||
<DetailRow label="Type" value={data?.branchType} />
|
|
||||||
|
|
||||||
<DetailRow label="Address" value={data?.address} />
|
|
||||||
|
|
||||||
{/* Contact persons */}
|
|
||||||
{contactInfo.map((person, index) => (
|
|
||||||
<div key={index} className="mb-2">
|
|
||||||
<div className="fw-medium text-primary">{person.contactPerson}</div>
|
|
||||||
<DetailRow label="Role" value={person.designation} />
|
|
||||||
<DetailRow label="Emails" value={person.contactEmails.join(", ")} />
|
|
||||||
<DetailRow label="Phone" value={person.contactNumbers.join(", ")} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Map Link */}
|
|
||||||
{googleMapUrl && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<a
|
|
||||||
href={googleMapUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary text-decoration-underline"
|
|
||||||
>
|
|
||||||
View on Google Maps
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-secondry border ms-2"
|
|
||||||
onClick={handleCopy}
|
|
||||||
>
|
|
||||||
<i className={`bx bx-xs me-1 ${copied ? "bxs-copy-alt":"bx-copy-alt"} `} ></i> {copied ? "Copied!" : "Copy"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DetailRow = ({ label, value }) => (
|
|
||||||
<div className="d-flex mb-1">
|
|
||||||
<div className="text-secondary" style={{ width: "90px", flexShrink: 0 }}>
|
|
||||||
{label}:
|
|
||||||
</div>
|
|
||||||
<div className="" style={{ wordBreak: "break-word" }}>
|
|
||||||
{value || "N/A"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default BranchDetails;
|
|
||||||
@ -1,358 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { useProjectName } from "../../../hooks/useProjects";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import Label from "../../common/Label";
|
|
||||||
import {
|
|
||||||
useBranchDetails,
|
|
||||||
useBranchTypes,
|
|
||||||
useCreateBranch,
|
|
||||||
useServiceProjects,
|
|
||||||
useUpdateBranch,
|
|
||||||
} from "../../../hooks/useServiceProject";
|
|
||||||
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { BranchSchema, defaultBranches } from "../ServiceProjectSchema";
|
|
||||||
import InputSuggessionField from "../../common/Forms/InputSuggesstionField";
|
|
||||||
import InputSuggestions from "../../common/InputSuggestion";
|
|
||||||
|
|
||||||
const ManageBranch = ({ closeModal, BranchToEdit = null }) => {
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error: requestError,
|
|
||||||
} = useBranchDetails(BranchToEdit);
|
|
||||||
const { data: branchTypes } = useBranchTypes();
|
|
||||||
const [contacts, setContacts] = React.useState([
|
|
||||||
{
|
|
||||||
contactPerson: "",
|
|
||||||
designation: "",
|
|
||||||
contactEmails: [""],
|
|
||||||
contactNumbers: [""],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const schema = BranchSchema();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = useAppForm({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues: {
|
|
||||||
...defaultBranches,
|
|
||||||
projectId: projectId || "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
reset();
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (BranchToEdit && data) {
|
|
||||||
reset({
|
|
||||||
branchName: data.branchName || "",
|
|
||||||
projectId: data.project?.id || projectId || "",
|
|
||||||
address: data.address || "",
|
|
||||||
branchType: data.branchType || "",
|
|
||||||
googleMapUrl: data.googleMapUrl || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.contactInformation) {
|
|
||||||
try {
|
|
||||||
setContacts(JSON.parse(data.contactInformation));
|
|
||||||
} catch {
|
|
||||||
setContacts([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
const { mutate: CreateServiceBranch, isPending: createPending } =
|
|
||||||
useCreateBranch(() => {
|
|
||||||
handleClose();
|
|
||||||
});
|
|
||||||
const { mutate: ServiceBranchUpdate, isPending } = useUpdateBranch(() =>
|
|
||||||
handleClose()
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (formdata) => {
|
|
||||||
let payload = {
|
|
||||||
...data,
|
|
||||||
...formdata,
|
|
||||||
projectId,
|
|
||||||
contactInformation: JSON.stringify(contacts), // ← important
|
|
||||||
};
|
|
||||||
|
|
||||||
if (BranchToEdit) {
|
|
||||||
ServiceBranchUpdate({ id: data.id, payload });
|
|
||||||
} else {
|
|
||||||
CreateServiceBranch(payload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container p-3">
|
|
||||||
<h5 className="m-0">
|
|
||||||
{BranchToEdit ? "Update Branch" : "Create Branch"}
|
|
||||||
</h5>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="row my-2 text-start">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<Label htmlFor="branchName" className="form-label" required>
|
|
||||||
Branch Name
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="branchName"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("branchName")}
|
|
||||||
placeholder="Enter Branch"
|
|
||||||
/>
|
|
||||||
{errors.branchName && (
|
|
||||||
<small className="danger-text">{errors.branchName.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
<Label htmlFor="branchType" className="form-label" required>
|
|
||||||
Branch Type
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<InputSuggestions
|
|
||||||
organizationList={branchTypes}
|
|
||||||
value={watch("branchType") || ""}
|
|
||||||
onChange={(val) =>
|
|
||||||
setValue("branchType", val, { shouldValidate: true })
|
|
||||||
}
|
|
||||||
error={errors.branchType?.message}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<Label htmlFor="googleMapUrl" className="form-label">
|
|
||||||
Google Map URL
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="googleMapUrl"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("googleMapUrl")}
|
|
||||||
/>
|
|
||||||
{errors.googleMapUrl && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors.googleMapUrl.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
|
||||||
<div className="col-12">
|
|
||||||
<Label className="form-label" required>
|
|
||||||
Contact Persons
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
{contacts.map((item, index) => (
|
|
||||||
<div key={index} className="border rounded p-2 mb-3">
|
|
||||||
<div className="d-flex justify-content-end py-1">
|
|
||||||
{" "}
|
|
||||||
<div className="col-md-1 d-flex align-items-center">
|
|
||||||
<i
|
|
||||||
className="bx bx-trash text-danger cursor-pointer"
|
|
||||||
onClick={() =>
|
|
||||||
setContacts(contacts.filter((_, i) => i !== index))
|
|
||||||
}
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row mb-2">
|
|
||||||
<div className=" col-md-6">
|
|
||||||
<Label className="form-label">Contact Name</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Contact Name"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
value={item.contactPerson}
|
|
||||||
onChange={(e) => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactPerson = e.target.value;
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-md-6">
|
|
||||||
<Label className="form-label">Designation</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Designation"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
value={item.designation}
|
|
||||||
onChange={(e) => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].designation = e.target.value;
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Numbers Section */}
|
|
||||||
<Label className="form-label">Contact Numbers</Label>
|
|
||||||
|
|
||||||
{item.contactNumbers.map((num, numIndex) => (
|
|
||||||
<div
|
|
||||||
key={numIndex}
|
|
||||||
className="d-flex gap-2 mb-2 align-items-center"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Number"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
maxLength={10}
|
|
||||||
value={num}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value.replace(/\D/g, ""); // remove non-digit characters
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactNumbers[numIndex] = value;
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Show PLUS only on last row */}
|
|
||||||
{numIndex === item.contactNumbers.length - 1 ? (
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
|
|
||||||
onClick={() => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactNumbers.push("");
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
) : (
|
|
||||||
<i
|
|
||||||
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
|
|
||||||
onClick={() => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactNumbers.splice(numIndex, 1);
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
{/* Emails Section */}
|
|
||||||
<Label className="form-label">Contact Emails</Label>
|
|
||||||
|
|
||||||
{item.contactEmails.map((email, emailIndex) => (
|
|
||||||
<div
|
|
||||||
key={emailIndex}
|
|
||||||
className="d-flex gap-2 mb-2 align-items-center"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactEmails[emailIndex] = e.target.value;
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Show PLUS only on the last row */}
|
|
||||||
{emailIndex === item.contactEmails.length - 1 ? (
|
|
||||||
<i
|
|
||||||
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
|
|
||||||
onClick={() => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactEmails.push("");
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
) : (
|
|
||||||
<i
|
|
||||||
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
|
|
||||||
onClick={() => {
|
|
||||||
const list = [...contacts];
|
|
||||||
list[index].contactEmails.splice(emailIndex, 1);
|
|
||||||
setContacts(list);
|
|
||||||
}}
|
|
||||||
></i>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-primary mt-2"
|
|
||||||
onClick={() =>
|
|
||||||
setContacts([
|
|
||||||
...contacts,
|
|
||||||
{
|
|
||||||
contactPerson: "",
|
|
||||||
designation: "",
|
|
||||||
contactEmails: [""], // ← important
|
|
||||||
contactNumbers: [""], // ← important
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus"></i> Add Contact
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row my-2 text-start">
|
|
||||||
<div className="col-12">
|
|
||||||
<Label htmlFor="address" className="form-label" required>
|
|
||||||
Address
|
|
||||||
</Label>
|
|
||||||
<textarea
|
|
||||||
id="address"
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
{...register("address")}
|
|
||||||
/>
|
|
||||||
{errors.address && (
|
|
||||||
<small className="danger-text">{errors.address.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-end gap-3">
|
|
||||||
<button
|
|
||||||
type="reset"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="btn btn-label-secondary btn-sm mt-3"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="submit" className="btn btn-primary btn-sm mt-3">
|
|
||||||
{isPending ? "Please wait..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageBranch;
|
|
||||||
@ -1,283 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import GlobalModel from "../../common/GlobalModel";
|
|
||||||
import ManageBranch from "./ManageBranch";
|
|
||||||
import { useBranches, useDeleteBranch } from "../../../hooks/useServiceProject";
|
|
||||||
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
|
||||||
import { useDebounce } from "../../../utils/appUtils";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import Pagination from "../../common/Pagination";
|
|
||||||
import ConfirmModal from "../../common/ConfirmModal";
|
|
||||||
import { SpinnerLoader } from "../../common/Loader";
|
|
||||||
|
|
||||||
const ServiceBranch = () => {
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
||||||
const [showInactive, setShowInactive] = useState(false);
|
|
||||||
const [manageState, setManageState] = useState({
|
|
||||||
IsOpen: false,
|
|
||||||
branchId: null,
|
|
||||||
});
|
|
||||||
const { mutate: DeleteBranch, isPending } = useDeleteBranch();
|
|
||||||
const [deletingId, setDeletingId] = useState(null);
|
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const debouncedSearch = useDebounce(search, 500);
|
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useBranches(
|
|
||||||
projectId,
|
|
||||||
!showInactive,
|
|
||||||
ITEMS_PER_PAGE - 12,
|
|
||||||
currentPage,
|
|
||||||
debouncedSearch
|
|
||||||
);
|
|
||||||
|
|
||||||
const paginate = (page) => {
|
|
||||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
|
||||||
setCurrentPage(page);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "branchName",
|
|
||||||
label: "Name",
|
|
||||||
align: "text-start",
|
|
||||||
getValue: (e) => e?.branchName || "N/A",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleDelete = (id) => {
|
|
||||||
setDeletingId(id);
|
|
||||||
DeleteBranch(
|
|
||||||
{ id, isActive: showInactive },
|
|
||||||
{
|
|
||||||
onSettled: () => {
|
|
||||||
setDeletingId(null);
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{IsDeleteModalOpen && (
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={IsDeleteModalOpen}
|
|
||||||
type={!showInactive ? "delete" : "undo"}
|
|
||||||
header={!showInactive ? "Delete Branch" : "Restore Branch"}
|
|
||||||
message={
|
|
||||||
!showInactive
|
|
||||||
? "Are you sure you want delete?"
|
|
||||||
: "Are you sure you want restore?"
|
|
||||||
}
|
|
||||||
onSubmit={handleDelete}
|
|
||||||
onClose={() => setIsDeleteModalOpen(false)}
|
|
||||||
loading={isPending}
|
|
||||||
paramData={deletingId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="card h-100 table-responsive px-sm-4">
|
|
||||||
<div className="card-datatable" id="payment-request-table">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="row align-items-center justify-content-between mt-3 mx-1">
|
|
||||||
<div className="col-md-4 col-sm-12 ms-n3 text-start ">
|
|
||||||
<h5 className="mb-0">
|
|
||||||
<i className="bx bx-buildings text-primary"></i>
|
|
||||||
<span className="ms-2 fw-bold">Branchs</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Flex container for toggle + button */}
|
|
||||||
<div className="col-md-8 col-sm-12 text-end">
|
|
||||||
<div className="d-flex justify-content-end gap-2">
|
|
||||||
<div className="form-check form-switch d-inline-flex align-items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input mt-1"
|
|
||||||
id="inactiveEmployeesCheckbox"
|
|
||||||
checked={showInactive}
|
|
||||||
onChange={() => setShowInactive(!showInactive)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
|
||||||
className="ms-2 text-xs"
|
|
||||||
>
|
|
||||||
{!showInactive ? "Show Deleted" : "Hide Deleted"}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-end">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setManageState({
|
|
||||||
IsOpen: true,
|
|
||||||
branchId: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="bx bx-sm bx-plus-circle me-2"></i>
|
|
||||||
Add Branch
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx-2 mt-3">
|
|
||||||
<table className="table border-top text-nowrap align-middle table-borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{columns.map((col) => (
|
|
||||||
<th key={col.key} className={col.align}>
|
|
||||||
{col.label}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
<th className="text-center">Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{isLoading && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={columns.length + 1}
|
|
||||||
className="text-center py-5"
|
|
||||||
style={{
|
|
||||||
height: "200px",
|
|
||||||
verticalAlign: "middle",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="d-flex justify-content-center align-items-center w-100 h-100">
|
|
||||||
<SpinnerLoader />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isError && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={columns.length + 1}
|
|
||||||
className="text-center text-danger py-5"
|
|
||||||
>
|
|
||||||
{error?.message || "Error loading branches"}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading &&
|
|
||||||
!isError &&
|
|
||||||
data?.data?.length > 0 &&
|
|
||||||
data.data.map((branch) => (
|
|
||||||
<tr key={branch.id} style={{ height: "35px" }}>
|
|
||||||
{columns.map((col) => (
|
|
||||||
<td key={col.key} className={`${col.align} py-3`}>
|
|
||||||
{col.getValue(branch)}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td className="text-center">
|
|
||||||
<div className="dropdown z-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
>
|
|
||||||
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end w-auto">
|
|
||||||
{!showInactive ? (
|
|
||||||
<>
|
|
||||||
<li
|
|
||||||
onClick={() =>
|
|
||||||
setManageState({
|
|
||||||
IsOpen: true,
|
|
||||||
branchId: branch.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
|
||||||
<i className="bx bx-edit text-primary bx-xs me-2"></i>
|
|
||||||
Modify
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
setDeletingId(branch.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
|
||||||
<i className="bx bx-trash text-danger bx-xs me-2"></i>
|
|
||||||
Delete
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<li
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
setDeletingId(branch.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a className="dropdown-item px-2 cursor-pointer py-1">
|
|
||||||
<i className="bx bx-undo text-danger me-2"></i>
|
|
||||||
Restore
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!isLoading &&
|
|
||||||
!isError &&
|
|
||||||
(!data?.data || data.data.length === 0) && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={columns.length + 1}
|
|
||||||
className="text-center py-12"
|
|
||||||
>
|
|
||||||
No Branch Found
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{data?.data?.length > 0 && (
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={data.totalPages}
|
|
||||||
onPageChange={paginate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{manageState.IsOpen && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen
|
|
||||||
size="md"
|
|
||||||
closeModal={() =>
|
|
||||||
setManageState({ IsOpen: false, branchId: null })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ManageBranch
|
|
||||||
key={manageState.branchId ?? "new"}
|
|
||||||
BranchToEdit={manageState.branchId}
|
|
||||||
closeModal={() =>
|
|
||||||
setManageState({ IsOpen: false, branchId: null })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceBranch;
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import SelectField from "../../common/Forms/SelectField";
|
|
||||||
import Error from "../../common/Error";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
AppFormController,
|
|
||||||
AppFormProvider,
|
|
||||||
useAppForm,
|
|
||||||
} from "../../../hooks/appHooks/useAppForm";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { closePopup } from "../../../slices/localVariablesSlice";
|
|
||||||
import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject";
|
|
||||||
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
|
|
||||||
|
|
||||||
export const ChangeStatusSchema = z.object({
|
|
||||||
statusId: z.string().min(1, { message: "Please select status" }),
|
|
||||||
});
|
|
||||||
const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
|
|
||||||
const { data, isLoading, isError, error } = useJobStatus(statusId, projectId);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const methods = useAppForm({
|
|
||||||
resolver: zodResolver(ChangeStatusSchema),
|
|
||||||
defaultValues: { statusId: "" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { errors },
|
|
||||||
} = methods;
|
|
||||||
|
|
||||||
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
|
|
||||||
handleClose();
|
|
||||||
});
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
const payload =
|
|
||||||
[
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/statusId",
|
|
||||||
value: formData.statusId,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
UpdateStatus({ id: jobId, payload });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
dispatch(closePopup(popUpId));
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<AppFormProvider {...methods}>
|
|
||||||
<div className="d-flex mb-2">
|
|
||||||
<span className="fs-6 fw-medium">
|
|
||||||
Change Status
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="mb-2">
|
|
||||||
<AppFormController
|
|
||||||
name="statusId"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<SelectField
|
|
||||||
label="Select Status"
|
|
||||||
options={data ?? []}
|
|
||||||
placeholder="Choose a Status"
|
|
||||||
required
|
|
||||||
labelKeyKey="name"
|
|
||||||
valueKeyKey="id"
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="m-0"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{errors.statusId && (
|
|
||||||
<small className="danger-text">{errors.statusId.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex flex-row justify-content-end gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="btn btn-label-secondary btn-sm"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="submit" className="btn btn-primary btn-sm">
|
|
||||||
{isPending ? "Please wait" : "Save"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</AppFormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChangeStatus;
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Avatar from "../../common/Avatar";
|
|
||||||
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { JobCommentSchema } from "../ServiceProjectSchema";
|
|
||||||
import {
|
|
||||||
useAddCommentJob,
|
|
||||||
useJobComments,
|
|
||||||
} from "../../../hooks/useServiceProject";
|
|
||||||
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
|
||||||
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
|
||||||
import Filelist from "../../Expenses/Filelist";
|
|
||||||
import { formatFileSize, getIconByFileType } from "../../../utils/appUtils";
|
|
||||||
|
|
||||||
const JobComments = ({ data }) => {
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
} = useAppForm({
|
|
||||||
resolver: zodResolver(JobCommentSchema),
|
|
||||||
defaultValues: { comment: "", attachments: [] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: comments,
|
|
||||||
fetchNextPage,
|
|
||||||
hasNextPage,
|
|
||||||
isFetchingNextPage,
|
|
||||||
} = useJobComments(data?.id, ITEMS_PER_PAGE, 1);
|
|
||||||
const jobComments = comments?.pages?.flatMap((p) => p?.data ?? []) ?? [];
|
|
||||||
|
|
||||||
const { mutate: AddComment, isPending } = useAddCommentJob(() => reset());
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
formData.jobTicketId = data?.id;
|
|
||||||
AddComment(formData);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.style.setProperty("--sticky-top", `-25px`);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const files = watch("attachments");
|
|
||||||
const onFileChange = async (e) => {
|
|
||||||
const newFiles = Array.from(e.target.files);
|
|
||||||
if (newFiles.length === 0) return;
|
|
||||||
|
|
||||||
const existingFiles = Array.isArray(watch("attachments"))
|
|
||||||
? watch("attachments")
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const parsedFiles = await Promise.all(
|
|
||||||
newFiles.map(async (file) => {
|
|
||||||
const base64Data = await toBase64(file);
|
|
||||||
return {
|
|
||||||
fileName: file.name,
|
|
||||||
base64Data,
|
|
||||||
contentType: file.type,
|
|
||||||
fileSize: file.size,
|
|
||||||
description: "",
|
|
||||||
isActive: true,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const combinedFiles = [
|
|
||||||
...existingFiles,
|
|
||||||
...parsedFiles.filter(
|
|
||||||
(newFile) =>
|
|
||||||
!existingFiles?.some(
|
|
||||||
(f) =>
|
|
||||||
f.fileName === newFile.fileName && f.fileSize === newFile.fileSize
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
setValue("attachments", combinedFiles, {
|
|
||||||
shouldDirty: true,
|
|
||||||
shouldValidate: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toBase64 = (file) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => resolve(reader.result.split(",")[1]);
|
|
||||||
reader.onerror = (error) => reject(error);
|
|
||||||
});
|
|
||||||
const removeFile = (index) => {
|
|
||||||
const newFiles = files.filter((_, i) => i !== index);
|
|
||||||
setValue("attachments", newFiles, { shouldValidate: true });
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="row">
|
|
||||||
<div className="sticky-section bg-white p-3">
|
|
||||||
<h6 className="m-0 fw-semibold mb-3">Add Comment</h6>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="d-flex">
|
|
||||||
<Avatar firstName={"A"} lastName={"D"} />
|
|
||||||
|
|
||||||
<div className="flex-grow-1">
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Write your comment..."
|
|
||||||
{...register("comment")}
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
{errors?.comment && (
|
|
||||||
<small className="danger-text">
|
|
||||||
{errors?.comment?.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow-1 ms-10 mt-2">
|
|
||||||
{files?.length > 0 && (
|
|
||||||
<Filelist files={files} removeFile={removeFile} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
|
|
||||||
<div
|
|
||||||
onClick={() => document.getElementById("attachments").click()}
|
|
||||||
className="cursor-pointer"
|
|
||||||
style={{ whiteSpace: "nowrap" }}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".pdf,.jpg,.jpeg,.png"
|
|
||||||
id="attachments"
|
|
||||||
multiple
|
|
||||||
className="d-none"
|
|
||||||
{...register("attachments")}
|
|
||||||
onChange={(e) => {
|
|
||||||
onFileChange(e);
|
|
||||||
e.target.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
|
|
||||||
Add Attachment
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-sm px-1 py-1" // smaller padding + slightly smaller font
|
|
||||||
type="submit"
|
|
||||||
disabled={!watch("comment")?.trim() || isPending}
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div className="card-body p-0">
|
|
||||||
<div className="list-group p-0 m-0">
|
|
||||||
{jobComments?.map((item) => {
|
|
||||||
const user = item?.createdBy;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="d-flex align-items-start mt-2 mx-0 px-0">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
firstName={user?.firstName}
|
|
||||||
lastName={user?.lastName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="w-100">
|
|
||||||
<div className="d-flex flex-row align-items-center gap-3 w-100">
|
|
||||||
<span className="fw-semibold">
|
|
||||||
{user?.firstName} {user?.lastName}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-secondary">
|
|
||||||
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-muted text-secondary">
|
|
||||||
{user?.jobRoleName}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-wrap">
|
|
||||||
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
|
|
||||||
|
|
||||||
<div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6">
|
|
||||||
{item.attachments?.map((file) => (
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<i
|
|
||||||
className={`bx bx-xxl ${getIconByFileType(
|
|
||||||
file?.contentType
|
|
||||||
)} fs-3`}
|
|
||||||
></i>
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<p className="m-0">{file.fileName}</p>
|
|
||||||
<small className="text-secondary">
|
|
||||||
{formatFileSize(file.fileSize)}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default JobComments;
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Avatar from "../../common/Avatar";
|
|
||||||
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
|
||||||
|
|
||||||
|
|
||||||
const JobStatusLog = ({ data }) => {
|
|
||||||
return (
|
|
||||||
<div className="card shadow-none p-0">
|
|
||||||
<div className="card-body p-0">
|
|
||||||
<div className="list-group">
|
|
||||||
{data?.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="list-group-item border-0 border-bottom p-1"
|
|
||||||
>
|
|
||||||
<div className="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<span className="fw-semibold">
|
|
||||||
{item.nextStatus?.displayName ??
|
|
||||||
item.status?.displayName ??
|
|
||||||
"Status"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-secondary">
|
|
||||||
{formatUTCToLocalTime(item?.updatedAt,true)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex align-items-start mt-2 mx-0 px-0">
|
|
||||||
<Avatar
|
|
||||||
firstName={item.updatedBy?.firstName}
|
|
||||||
lastName={item.updatedBy?.lastName}
|
|
||||||
/>
|
|
||||||
<div className="">
|
|
||||||
<div className="d-flex flex-row gap-3">
|
|
||||||
<span className="fw-semibold">
|
|
||||||
{item.updatedBy?.firstName} {item.updatedBy?.lastName}
|
|
||||||
</span>
|
|
||||||
<span className="text-secondary">
|
|
||||||
{/* <em>{formatUTCToLocalTime(item?.createdAt, true)}</em> */}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted text-secondary">
|
|
||||||
{item?.updatedBy?.jobRoleName}
|
|
||||||
</div>
|
|
||||||
<div className="text-wrap">
|
|
||||||
<p className="mb-1 mt-2 text-muted">{item.comment}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default JobStatusLog;
|
|
||||||
@ -1,288 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Breadcrumb from "../../common/Breadcrumb";
|
|
||||||
import Label from "../../common/Label";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { defaultJobValue, jobSchema } from "../ServiceProjectSchema";
|
|
||||||
import {
|
|
||||||
useBranches,
|
|
||||||
useCreateServiceProjectJob,
|
|
||||||
useJobTags,
|
|
||||||
useServiceProjectJobDetails,
|
|
||||||
useServiceProjects,
|
|
||||||
useUpdateServiceProjectJob,
|
|
||||||
} from "../../../hooks/useServiceProject";
|
|
||||||
import { ITEMS_PER_PAGE } from "../../../utils/constants";
|
|
||||||
import DatePicker from "../../common/DatePicker";
|
|
||||||
import PmsEmployeeInputTag from "../../common/PmsEmployeeInputTag";
|
|
||||||
import TagInput from "../../common/TagInput";
|
|
||||||
import { localToUtc } from "../../../utils/appUtils";
|
|
||||||
import SelectField from "../../common/Forms/SelectField";
|
|
||||||
import {
|
|
||||||
AppFormController,
|
|
||||||
AppFormProvider,
|
|
||||||
useAppForm,
|
|
||||||
} from "../../../hooks/appHooks/useAppForm";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
|
|
||||||
import { useServiceProjectJobContext } from "../Jobs";
|
|
||||||
import { SelectFieldSearch } from "../../common/Forms/SelectFieldServerSide";
|
|
||||||
|
|
||||||
const ManageJob = ({ Job }) => {
|
|
||||||
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const methods = useAppForm({
|
|
||||||
resolver: zodResolver(jobSchema),
|
|
||||||
defaultValues: defaultJobValue,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
} = methods;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: JobTags,
|
|
||||||
isLoading: isTagLoading,
|
|
||||||
isError: isTagError,
|
|
||||||
error: tagError,
|
|
||||||
} = useJobTags();
|
|
||||||
const {
|
|
||||||
data: JobData,
|
|
||||||
isLoading: isJobLoading,
|
|
||||||
isError: isJobError,
|
|
||||||
error: jobError,
|
|
||||||
} = useServiceProjectJobDetails(Job);
|
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useJobStatus(
|
|
||||||
JobData?.status?.id,
|
|
||||||
projectId
|
|
||||||
);
|
|
||||||
const { mutate: CreateJob, isPending } = useCreateServiceProjectJob(() => {
|
|
||||||
reset();
|
|
||||||
});
|
|
||||||
const { mutate: UpdateJob, isPending: Updating } = useUpdateServiceProjectJob(
|
|
||||||
() => {
|
|
||||||
setManageJob({ isOpen: false, jobId: null });
|
|
||||||
setSelectedJob({ showCanvas: true, job: Job });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
if (Job) {
|
|
||||||
const existingEmployeeIds = JobData?.assignees?.map((e) => e.id) || [];
|
|
||||||
|
|
||||||
const oldEmployees = JobData.assignees.map((e) => ({
|
|
||||||
employeeId: e.id,
|
|
||||||
isActive: formData.assignees.includes(e.id),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const newEmployees = formData.assignees
|
|
||||||
.filter((id) => !existingEmployeeIds.includes(id))
|
|
||||||
.map((id) => ({
|
|
||||||
employeeId: id,
|
|
||||||
isActive: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const updatedEmployees = [...oldEmployees, ...newEmployees];
|
|
||||||
|
|
||||||
const payload = [
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/title",
|
|
||||||
value: formData.title,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/description",
|
|
||||||
value: formData.description,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/startDate",
|
|
||||||
value: localToUtc(formData.startDate) ?? JobData.startDate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/dueDate",
|
|
||||||
value: localToUtc(formData.dueDate) ?? JobData.dueDate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/tags",
|
|
||||||
value: formData.tags,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/assignees",
|
|
||||||
value: updatedEmployees,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "replace",
|
|
||||||
path: "/statusId",
|
|
||||||
value: formData.statusId,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
UpdateJob({ id: Job, payload });
|
|
||||||
} else {
|
|
||||||
formData.assignees = formData.assignees.map((emp) => ({
|
|
||||||
employeeId: emp,
|
|
||||||
isActive: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
formData.startDate = localToUtc(formData.startDate);
|
|
||||||
formData.dueDate = localToUtc(formData.dueDate);
|
|
||||||
formData.projectId = projectId;
|
|
||||||
|
|
||||||
CreateJob(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!JobData && !Job) {
|
|
||||||
reset({
|
|
||||||
...defaultJobValue,
|
|
||||||
projectId: projectId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!JobData || !Job) return;
|
|
||||||
|
|
||||||
const assignedEmployees = (JobData.assignees || []).map((e) => e.id);
|
|
||||||
|
|
||||||
reset({
|
|
||||||
title: JobData.title ?? "",
|
|
||||||
description: JobData.description ?? "",
|
|
||||||
projectId: JobData.project?.id ?? projectId,
|
|
||||||
assignees: assignedEmployees,
|
|
||||||
startDate: JobData.startDate ?? null,
|
|
||||||
dueDate: JobData.dueDate ?? null,
|
|
||||||
tags: JobData.tags ?? [],
|
|
||||||
statusId: JobData.status.id,
|
|
||||||
projectBranchId : JobData?.projectBranch?.id
|
|
||||||
});
|
|
||||||
}, [JobData, Job, projectId]);
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<AppFormProvider {...methods}>
|
|
||||||
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="col-12 col-md-12 mb-2 ">
|
|
||||||
<Label required>Title</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...register("title")}
|
|
||||||
className="form-control form-control"
|
|
||||||
placeholder="Enter Title"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
|
||||||
<Label required>Start Date</Label>
|
|
||||||
<DatePicker
|
|
||||||
name="startDate"
|
|
||||||
control={control}
|
|
||||||
placeholder="DD-MM-YYYY"
|
|
||||||
className="w-full"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
|
||||||
<Label required>Due Date</Label>
|
|
||||||
<DatePicker
|
|
||||||
control={control}
|
|
||||||
minDate={watch("startDate")}
|
|
||||||
name="dueDate"
|
|
||||||
className="w-full"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
|
||||||
<Label>Select Employee</Label>
|
|
||||||
<PmsEmployeeInputTag
|
|
||||||
control={control}
|
|
||||||
name="assignees"
|
|
||||||
placeholder="Select employee"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{Job && (
|
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
|
||||||
<AppFormController
|
|
||||||
name="statusId"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<SelectField
|
|
||||||
label="Select Status"
|
|
||||||
options={data ?? []}
|
|
||||||
placeholder="Choose a Status"
|
|
||||||
required
|
|
||||||
labelKeyKey="name"
|
|
||||||
valueKeyKey="id"
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="m-0"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{errors.statusId && (
|
|
||||||
<small className="danger-text">{errors.statusId.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
|
||||||
<TagInput
|
|
||||||
options={JobTags?.data}
|
|
||||||
name="tags"
|
|
||||||
label="Tag"
|
|
||||||
placeholder="Enter Tag"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-md-6 mb-2 mb-md-4">
|
|
||||||
<SelectFieldSearch
|
|
||||||
label="Select Branch"
|
|
||||||
placeholder="Select Branch"
|
|
||||||
value={watch("projectBranchId")}
|
|
||||||
onChange={(val) => setValue("projectBranchId", val)}
|
|
||||||
valueKey="id"
|
|
||||||
labelKey="branchName"
|
|
||||||
hookParams={[projectId, true, 10, 1]}
|
|
||||||
useFetchHook={useBranches}
|
|
||||||
isMultiple={false}
|
|
||||||
disabled={Job}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12">
|
|
||||||
<Label required>Description</Label>
|
|
||||||
<textarea
|
|
||||||
{...register("description")}
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex flex-row-reverse my-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
disabled={isPending || Updating}
|
|
||||||
>
|
|
||||||
{isPending || Updating
|
|
||||||
? "Please wait..."
|
|
||||||
: Job
|
|
||||||
? "Update"
|
|
||||||
: "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</AppFormProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageJob;
|
|
||||||
@ -1,256 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { useServiceProjectJobDetails } from "../../../hooks/useServiceProject";
|
|
||||||
import { SpinnerLoader } from "../../common/Loader";
|
|
||||||
import Error from "../../common/Error";
|
|
||||||
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
|
||||||
import Avatar from "../../common/Avatar";
|
|
||||||
import EmployeeAvatarGroup from "../../common/EmployeeAvatarGroup";
|
|
||||||
import { daysLeft, getJobStatusBadge } from "../../../utils/appUtils";
|
|
||||||
import HoverPopup from "../../common/HoverPopup";
|
|
||||||
import ChangeStatus from "./ChangeStatus";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { STATUS_JOB_CLOSED } from "../../../utils/constants";
|
|
||||||
import Tooltip from "../../common/Tooltip";
|
|
||||||
import BranchDetails from "../ServiceProjectBranch/BranchDetails";
|
|
||||||
import { JobDetailsSkeleton } from "../ServiceProjectSeketon";
|
|
||||||
import JobComments from "./JobComments";
|
|
||||||
import JobStatusLog from "./JobStatusLog";
|
|
||||||
|
|
||||||
const ManageJobTicket = ({ Job }) => {
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const { data, isLoading, isError, error } = useServiceProjectJobDetails(
|
|
||||||
Job?.job
|
|
||||||
);
|
|
||||||
const drawerRef = useRef();
|
|
||||||
const tabsData = [
|
|
||||||
{
|
|
||||||
id: "comment",
|
|
||||||
title: "Comments",
|
|
||||||
icon: "bx bx-comment me-2",
|
|
||||||
active: true,
|
|
||||||
content: <JobComments data={data} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "history",
|
|
||||||
title: "History",
|
|
||||||
icon: "bx bx-time me-2",
|
|
||||||
active: false,
|
|
||||||
content: <JobStatusLog data={data?.updateLogs} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isLoading) return <JobDetailsSkeleton />;
|
|
||||||
if (isError)
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Error error={error} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className=" text-start position-relative"
|
|
||||||
ref={drawerRef}
|
|
||||||
style={{ overflow: "visible" }}
|
|
||||||
>
|
|
||||||
<div className="col-12">
|
|
||||||
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
|
|
||||||
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
|
|
||||||
<p className="mb-0">
|
|
||||||
<span className="fw-medium me-1">Job Id :</span>
|
|
||||||
{data?.jobTicketUId || "N/A"}
|
|
||||||
</p>
|
|
||||||
<div className="d-flex flex-column align-items-end gap-3 mb-3">
|
|
||||||
<div className="d-flex flex-row gap-2 position-relative">
|
|
||||||
<span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
|
|
||||||
{data?.status?.displayName}
|
|
||||||
</span>
|
|
||||||
{STATUS_JOB_CLOSED !== data?.status?.id && (
|
|
||||||
<HoverPopup
|
|
||||||
id="STATUS_CHANEG"
|
|
||||||
Mode="click"
|
|
||||||
className=""
|
|
||||||
align="right"
|
|
||||||
content={
|
|
||||||
<ChangeStatus
|
|
||||||
statusId={data?.status?.id}
|
|
||||||
projectId={projectId}
|
|
||||||
jobId={Job?.job}
|
|
||||||
popUpId="STATUS_CHANEG"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
text={"Change Status"}
|
|
||||||
placement="left"
|
|
||||||
children={
|
|
||||||
<i className="bx bx-edit bx-sm cursor-pointer"></i>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</HoverPopup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex flex-wrap">
|
|
||||||
<p>{data?.description || "N/A"}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<div className="d-flex flex-row gap-1 text-secondry">
|
|
||||||
<i className="bx bx-calendar"></i>{" "}
|
|
||||||
<span>
|
|
||||||
Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex flex-row gap-2 text-wraps">
|
|
||||||
{data?.tags?.map((tag, ind) => (
|
|
||||||
<span
|
|
||||||
key={`${ind}0-${tag?.name}`}
|
|
||||||
className="badge bg-label-primary"
|
|
||||||
>
|
|
||||||
{tag?.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-md-between ">
|
|
||||||
<div className="d-flex flex-row gap-5">
|
|
||||||
<span className="text-secondry">
|
|
||||||
<i className="bx bx-calendar"></i> Start Date :{" "}
|
|
||||||
{formatUTCToLocalTime(data?.startDate)}
|
|
||||||
</span>{" "}
|
|
||||||
<i className="bx bx-right-arrow-alt"></i>{" "}
|
|
||||||
<span className="text-secondry">
|
|
||||||
<i className="bx bx-calendar"></i> Due on :{" "}
|
|
||||||
{formatUTCToLocalTime(data?.startDate)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{data?.dueDate &&
|
|
||||||
(() => {
|
|
||||||
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<span className="text-secondry me-1">Days Left:</span>
|
|
||||||
<span className={`badge bg-${color}`}>
|
|
||||||
{days !== null ? `${days} days` : "N/A"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
{data?.projectBranch && (
|
|
||||||
<div className="d-flex gap-3 my-2 position-relative" ref={drawerRef} >
|
|
||||||
<span className="text-secondary">
|
|
||||||
<i className="bx bx-buildings"></i> Branch Name:
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<HoverPopup
|
|
||||||
id="BRANCH_DETAILS"
|
|
||||||
Mode="click"
|
|
||||||
align="auto"
|
|
||||||
boundaryRef={drawerRef}
|
|
||||||
content={<BranchDetails branch={data?.projectBranch?.id} />}
|
|
||||||
>
|
|
||||||
<span className="text-decoration-underline cursor-pointer">
|
|
||||||
{data?.projectBranch?.branchName}
|
|
||||||
</span>
|
|
||||||
</HoverPopup>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="border-top my-1">
|
|
||||||
<p className="m-0 py-1">
|
|
||||||
<i className="bx bx-group"></i> Peoples
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Created By */}
|
|
||||||
<div className="d-flex justify-content-between align-items-start w-100">
|
|
||||||
<p className="text-secondary m-0 me-3">Created By</p>
|
|
||||||
|
|
||||||
<div className="flex-grow-1 d-flex align-items-center gap-2">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
firstName={data?.createdBy?.firstName}
|
|
||||||
lastName={data?.createdBy?.lastName}
|
|
||||||
/>
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<p className="m-0 text-truncate">
|
|
||||||
{data?.createdBy?.firstName} {data?.createdBy?.lastName}
|
|
||||||
</p>
|
|
||||||
<small className="text-secondary text-xs">
|
|
||||||
{data?.createdBy?.jobRoleName}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Assigned To */}
|
|
||||||
<div className="d-flex flex-column flex-md-row align-items-start w-100 mt-2">
|
|
||||||
<p className="text-secondary m-0 me-3">Assigned To</p>
|
|
||||||
|
|
||||||
<div className="flex-grow-1">
|
|
||||||
<div className="d-flex flex-wrap gap-3">
|
|
||||||
{data?.assignees?.map((emp) => (
|
|
||||||
<div key={emp.id} className="d-flex align-items-center">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
firstName={emp.firstName}
|
|
||||||
lastName={emp.lastName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="d-flex flex-column ms-2 text-truncate">
|
|
||||||
<span className="text-truncate">
|
|
||||||
{emp.firstName} {emp.lastName}
|
|
||||||
</span>
|
|
||||||
<small className="text-secondary text-xs text-truncate">
|
|
||||||
{emp.jobRoleName}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nav-align-top nav-tabs-shadow p-0 shadow-none">
|
|
||||||
<ul className="nav nav-tabs" role="tablist">
|
|
||||||
{tabsData.map((tab) => (
|
|
||||||
<li className="nav-item" key={tab.id}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`nav-link ${tab.active ? "active" : ""}`}
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
data-bs-target={`#tab-${tab.id}`}
|
|
||||||
role="tab"
|
|
||||||
>
|
|
||||||
<i className={tab.icon} /> {tab.title}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className="tab-content p-1 px-sm-3">
|
|
||||||
{tabsData.map((tab) => (
|
|
||||||
<div
|
|
||||||
key={tab.id}
|
|
||||||
className={`tab-pane fade ${tab.active ? "show active" : ""}`}
|
|
||||||
id={`tab-${tab.id}`}
|
|
||||||
role="tabpanel"
|
|
||||||
>
|
|
||||||
{tab.content}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageJobTicket;
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { JobCommentSchema } from "../ServiceProjectSchema";
|
|
||||||
import Avatar from "../../common/Avatar";
|
|
||||||
import Filelist from "../../Expenses/Filelist";
|
|
||||||
|
|
||||||
const UpdateJobComment = () => {
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
} = useAppForm({
|
|
||||||
resolver: zodResolver(JobCommentSchema),
|
|
||||||
defaultValues: { comment: "", attachments: [] },
|
|
||||||
});
|
|
||||||
const onSubmit = () => {};
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="d-flex">
|
|
||||||
<Avatar firstName={"A"} lastName={"D"} />
|
|
||||||
|
|
||||||
<div className="flex-grow-1">
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Write your comment..."
|
|
||||||
{...register("comment")}
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
{errors?.comment && (
|
|
||||||
<small className="danger-text">{errors?.comment?.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div className="flex-grow-1 ms-10 mt-2">
|
|
||||||
{files?.length > 0 && (
|
|
||||||
<Filelist files={} removeFile={removeFile} />
|
|
||||||
)}
|
|
||||||
</div> */}
|
|
||||||
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
|
|
||||||
<div
|
|
||||||
onClick={() => document.getElementById("attachments").click()}
|
|
||||||
className="cursor-pointer"
|
|
||||||
style={{ whiteSpace: "nowrap" }}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".pdf,.jpg,.jpeg,.png"
|
|
||||||
id="attachments"
|
|
||||||
multiple
|
|
||||||
className="d-none"
|
|
||||||
{...register("attachments")}
|
|
||||||
onChange={(e) => {
|
|
||||||
onFileChange(e);
|
|
||||||
e.target.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
|
|
||||||
Add Attachment
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-sm px-1 py-1"
|
|
||||||
type="submit"
|
|
||||||
disabled={!watch("comment")?.trim() || isPending}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateJobComment;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const ServiceProjectNav = ({ onPillClick, activePill }) => {
|
|
||||||
const ProjectTab = [
|
|
||||||
{ key: "profile", icon: "bx bx-user", label: "Profile" },
|
|
||||||
{
|
|
||||||
key: "jobs",
|
|
||||||
icon: "bx bx-briefcase-alt",
|
|
||||||
label: "Jobs",
|
|
||||||
link: "/service/job",
|
|
||||||
},
|
|
||||||
{ key: "teams", icon: "bx bx-group", label: "Teams" },
|
|
||||||
|
|
||||||
{
|
|
||||||
key: "directory",
|
|
||||||
icon: "bx bxs-contact",
|
|
||||||
label: "Directory",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="table-responsive">
|
|
||||||
<div className="nav-align-top">
|
|
||||||
<ul className="nav nav-tabs">
|
|
||||||
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
|
|
||||||
<li key={tab.key} className="nav-item cursor-pointer">
|
|
||||||
<a
|
|
||||||
className={`nav-link ${
|
|
||||||
activePill === tab.key ? "active cursor-pointer" : ""
|
|
||||||
} fs-6`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onPillClick(tab.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className={`${tab.icon} bx-sm me-1_5`}></i>
|
|
||||||
<span className="d-none d-md-inline ">{tab.label}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceProjectNav;
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { useServiceProject } from "../../hooks/useServiceProject";
|
|
||||||
import { formatUTCToLocalTime } from "../../utils/dateUtils";
|
|
||||||
import ManageServiceProject from "./ManageServiceProject";
|
|
||||||
import GlobalModel from "../common/GlobalModel";
|
|
||||||
import { SpinnerLoader } from "../common/Loader";
|
|
||||||
import ServiceBranch from "./ServiceProjectBranch/ServiceBranch";
|
|
||||||
import ServiceProfile from "./ServiceProfile";
|
|
||||||
|
|
||||||
const ServiceProjectProfile = () => {
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const [IsOpenModal, setIsOpenModal] = useState(false);
|
|
||||||
const { data, isLoading, isError, error } = useServiceProject(projectId);
|
|
||||||
if (isLoading)
|
|
||||||
return (
|
|
||||||
<div className="py-8">
|
|
||||||
<SpinnerLoader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{IsOpenModal && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={IsOpenModal}
|
|
||||||
closeModal={() => setIsOpenModal(false)}
|
|
||||||
>
|
|
||||||
<ManageServiceProject
|
|
||||||
serviceProjectId={projectId}
|
|
||||||
onClose={() => setIsOpenModal(false)}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="row py-2">
|
|
||||||
<div className="col-md-6 col-lg-5 order-2 mb-6">
|
|
||||||
<ServiceProfile data={data} setIsOpenModal={setIsOpenModal}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-md-6 col-lg-7 order-2 mb-6">
|
|
||||||
<ServiceBranch />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceProjectProfile;
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import { PROJECT_STATUS } from "../../utils/constants";
|
|
||||||
|
|
||||||
//#region Service Project
|
|
||||||
export const projectSchema = z.object({
|
|
||||||
name: z.string().trim().min(1, "Name is required"),
|
|
||||||
|
|
||||||
shortName: z.string().trim().min(1, "Short name is required"),
|
|
||||||
|
|
||||||
clientId: z.string().trim().min(1, { message: "Client is required" }),
|
|
||||||
|
|
||||||
services: z
|
|
||||||
.array(z.string())
|
|
||||||
.min(1, { message: "At least one service required" }),
|
|
||||||
|
|
||||||
address: z.string().trim().min(1, "Address is required"),
|
|
||||||
|
|
||||||
assignedDate: z.string().trim().min(1, { message: "Date is required" }),
|
|
||||||
|
|
||||||
statusId: z.string().trim().min(1, "Status is required"),
|
|
||||||
|
|
||||||
contactName: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, "Contact name is required")
|
|
||||||
.regex(/^[A-Za-z\s]+$/, "Contact name can contain only letters"),
|
|
||||||
|
|
||||||
contactPhone: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.regex(/^\d+$/, "Phone number must contain only digits")
|
|
||||||
.min(7, "Invalid phone number")
|
|
||||||
.max(15, "Phone number too long"),
|
|
||||||
|
|
||||||
contactEmail: z.string().trim().email("Invalid email address"),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const defaultProjectValues = {
|
|
||||||
name: "",
|
|
||||||
shortName: "",
|
|
||||||
clientId: "",
|
|
||||||
services: [],
|
|
||||||
address: "",
|
|
||||||
assignedDate: "",
|
|
||||||
statusId: PROJECT_STATUS.find((s) => s.label === "Active")?.id,
|
|
||||||
contactName: "",
|
|
||||||
contactPhone: "",
|
|
||||||
contactEmail: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region JobSchema
|
|
||||||
|
|
||||||
export const TagSchema = z.object({
|
|
||||||
name: z.string().min(1, "Tag name is required"),
|
|
||||||
isActive: z.boolean().default(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const jobSchema = z.object({
|
|
||||||
title: z.string().min(1, "Title is required"),
|
|
||||||
description: z.string().min(1, "Description is required"),
|
|
||||||
projectId: z.string().min(1, "Project is required"),
|
|
||||||
|
|
||||||
assignees: z.array(z.string()).optional(),
|
|
||||||
|
|
||||||
startDate: z.string(),
|
|
||||||
dueDate: z.string(),
|
|
||||||
|
|
||||||
tags: z.array(TagSchema).optional().default([]),
|
|
||||||
|
|
||||||
statusId: z.string().optional().nullable(),
|
|
||||||
|
|
||||||
projectBranchId: z.string().optional().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
||||||
const ALLOWED_TYPES = [
|
|
||||||
"application/pdf",
|
|
||||||
"image/png",
|
|
||||||
"image/jpg",
|
|
||||||
"image/jpeg",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const JobCommentSchema = z.object({
|
|
||||||
comment: z.string().min(1, { message: "Please leave comment" }),
|
|
||||||
attachments: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
fileName: z.string().min(1),
|
|
||||||
base64Data: z.string().nullable(),
|
|
||||||
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
|
|
||||||
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
|
|
||||||
}),
|
|
||||||
documentId: z.string().optional(),
|
|
||||||
fileSize: z.number().max(MAX_FILE_SIZE),
|
|
||||||
description: z.string().optional(),
|
|
||||||
isActive: z.boolean().default(true),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
.default([]),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const defaultJobValue = {
|
|
||||||
title: "",
|
|
||||||
description: "",
|
|
||||||
projectId: "",
|
|
||||||
assignees: [],
|
|
||||||
statusId: null,
|
|
||||||
startDate: null,
|
|
||||||
dueDate: null,
|
|
||||||
tags: [],
|
|
||||||
branchId: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Branch
|
|
||||||
|
|
||||||
export const BranchSchema = () =>
|
|
||||||
z.object({
|
|
||||||
projectId: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, { message: "Project is required" }),
|
|
||||||
|
|
||||||
branchName: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, { message: "Branch Name is required" }),
|
|
||||||
|
|
||||||
contactInformation: z.string().optional(),
|
|
||||||
address: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, { message: "Address is required" }),
|
|
||||||
|
|
||||||
branchType: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, { message: "Branch Type is required" }),
|
|
||||||
|
|
||||||
googleMapUrl: z
|
|
||||||
.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const defaultBranches = {
|
|
||||||
branchName: "",
|
|
||||||
projectId: "",
|
|
||||||
contactInformation: "",
|
|
||||||
address: "",
|
|
||||||
branchType: "",
|
|
||||||
googleMapUrl: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const SkeletonLine = ({ height = 18, width = "100%", className = "" }) => (
|
|
||||||
<div
|
|
||||||
className={`skeleton ${className}`}
|
|
||||||
style={{
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
borderRadius: "4px",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const BranchDetailsSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<div className="w-100">
|
|
||||||
<div className="d-flex mb-3">
|
|
||||||
<SkeletonLine height={22} width="280px" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-2">
|
|
||||||
<div className="col-4">
|
|
||||||
<SkeletonLine height={16} width="70%" />
|
|
||||||
</div>
|
|
||||||
<div className="col-8">
|
|
||||||
<SkeletonLine height={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-2">
|
|
||||||
<div className="col-4">
|
|
||||||
<SkeletonLine height={16} width="70%" />
|
|
||||||
</div>
|
|
||||||
<div className="col-8">
|
|
||||||
<SkeletonLine height={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-2">
|
|
||||||
<div className="col-4">
|
|
||||||
<SkeletonLine height={16} width="70%" />
|
|
||||||
</div>
|
|
||||||
<div className="col-8">
|
|
||||||
<SkeletonLine height={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-2">
|
|
||||||
<div className="col-4">
|
|
||||||
<SkeletonLine height={16} width="70%" />
|
|
||||||
</div>
|
|
||||||
<div className="col-8">
|
|
||||||
<SkeletonLine height={16} width="90%" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-2">
|
|
||||||
<div className="col-4">
|
|
||||||
<SkeletonLine height={16} width="70%" />
|
|
||||||
</div>
|
|
||||||
<div className="col-8 d-flex gap-2 align-items-center">
|
|
||||||
<SkeletonLine height={16} width="60%" />
|
|
||||||
<SkeletonLine height={16} width="20px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const JobDetailsSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<div className="row text-start">
|
|
||||||
<div className="col-12">
|
|
||||||
{/* Title */}
|
|
||||||
<SkeletonLine height={24} width="50%" />
|
|
||||||
|
|
||||||
{/* Job ID + Status */}
|
|
||||||
<div className="d-flex justify-content-between align-items-start flex-wrap mb-3 mt-2">
|
|
||||||
<SkeletonLine height={18} width="30%" />
|
|
||||||
<div className="d-flex flex-row gap-2">
|
|
||||||
<SkeletonLine height={22} width="70px" />
|
|
||||||
<SkeletonLine height={22} width="22px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<SkeletonLine height={40} width="100%" />
|
|
||||||
|
|
||||||
{/* Created Date */}
|
|
||||||
<div className="d-flex my-3">
|
|
||||||
<SkeletonLine height={16} width="40%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Start / Due Date */}
|
|
||||||
<div className="d-flex justify-content-between mb-4">
|
|
||||||
<SkeletonLine height={16} width="50%" />
|
|
||||||
<SkeletonLine height={22} width="70px" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Branch Name */}
|
|
||||||
<div className="d-flex flex-row gap-3 my-2">
|
|
||||||
<SkeletonLine height={16} width="30%" />
|
|
||||||
<SkeletonLine height={16} width="40%" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Created By */}
|
|
||||||
<div className="row align-items-center my-3">
|
|
||||||
<div className="col-12 col-md-auto mb-2">
|
|
||||||
<SkeletonLine height={16} width="80px" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col d-flex align-items-center gap-2">
|
|
||||||
<SkeletonLine height={30} width="30px" /> {/* Avatar */}
|
|
||||||
<SkeletonLine height={16} width="40%" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Assigned To */}
|
|
||||||
<div className="row mt-2">
|
|
||||||
<div className="col-12 col-md-auto mb-2">
|
|
||||||
<SkeletonLine height={16} width="90px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="d-flex gap-3 mb-3">
|
|
||||||
<SkeletonLine height={35} width="80px" />
|
|
||||||
<SkeletonLine height={35} width="80px" />
|
|
||||||
<SkeletonLine height={35} width="80px" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SkeletonLine height={150} width="100%" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { useModal } from '../../../hooks/useAuth'
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import ServiceProjectTeamList from './ServiceProjectTeamList';
|
|
||||||
|
|
||||||
|
|
||||||
const ProjectTeam = () => {
|
|
||||||
const {onOpen} = useModal("ServiceTeamAllocation");
|
|
||||||
const {projectId}= useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='card page-min-h mt-2 px-4'>
|
|
||||||
<div className='row text-end'>
|
|
||||||
<div className='col-12'>
|
|
||||||
<div className='p-2'><button onClick={()=>onOpen({projectId:projectId})} className='btn btn-sm btn-primary'>Manage Employee</button></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ServiceProjectTeamList/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProjectTeam
|
|
||||||
@ -1,229 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import moment from "moment";
|
|
||||||
import { formatNumber, formatUTCToLocalTime, getDateDifferenceInDays } from "../../../utils/dateUtils";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import ManageProjectInfo from "../../Project/ManageProjectInfo";
|
|
||||||
import ProjectRepository from "../../../repositories/ProjectRepository";
|
|
||||||
import { cacheData, getCachedData } from "../../../slices/apiDataManager";
|
|
||||||
import showToast from "../../../services/toastService";
|
|
||||||
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
|
|
||||||
import { MANAGE_PROJECT } from "../../../utils/constants";
|
|
||||||
import GlobalModel from "../../common/GlobalModel";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { setProjectId } from "../../../slices/localVariablesSlice";
|
|
||||||
import { useProjectContext } from "../../../pages/project/ProjectPage";
|
|
||||||
import { useActiveInActiveServiceProject } from "../../../hooks/useServiceProject";
|
|
||||||
import ConfirmModal from "../../common/ConfirmModal";
|
|
||||||
import { getProjectStatusColor, getProjectStatusName } from "../../../utils/projectStatus";
|
|
||||||
|
|
||||||
const ServiceProjectCard = ({ project, isCore = true }) => {
|
|
||||||
const [deleteProject, setDeleteProject] = useState({
|
|
||||||
project: null,
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
|
||||||
const { setMangeProject, setManageServiceProject } = useProjectContext();
|
|
||||||
|
|
||||||
const handleClose = () => setShowModal(false);
|
|
||||||
|
|
||||||
const handleViewProject = () => {
|
|
||||||
if (isCore) {
|
|
||||||
dispatch(setProjectId(project.id));
|
|
||||||
navigate(`/projects/details`);
|
|
||||||
} else {
|
|
||||||
navigate(`/service-projects/${project.id}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleManage = () => {
|
|
||||||
if (isCore) {
|
|
||||||
setMangeProject({
|
|
||||||
isOpen: true,
|
|
||||||
Project: project.id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setManageServiceProject({
|
|
||||||
isOpen: true,
|
|
||||||
project: project.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: DeleteProject, isPending } = useActiveInActiveServiceProject(
|
|
||||||
() => setDeleteProject({ project: null, isOpen: false })
|
|
||||||
);
|
|
||||||
const handleActiveInactive = (projectId) => {
|
|
||||||
DeleteProject(projectId, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ConfirmModal
|
|
||||||
type="delete"
|
|
||||||
header="Delete Project"
|
|
||||||
message="Are you sure you want delete?"
|
|
||||||
onSubmit={handleActiveInactive}
|
|
||||||
onClose={() => setDeleteProject({ project: null, isOpen: false })}
|
|
||||||
loading={isPending}
|
|
||||||
paramData={project.id}
|
|
||||||
isOpen={deleteProject.isOpen}
|
|
||||||
/>
|
|
||||||
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
|
|
||||||
<div className={`card cursor-pointer`}>
|
|
||||||
<div className="card-header pb-4">
|
|
||||||
<div className="d-flex align-items-start">
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<div className="avatar me-4">
|
|
||||||
<i
|
|
||||||
className="rounded-circle bx bx-building-house"
|
|
||||||
style={{ fontSize: "xx-large" }}
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
<div className="me-2">
|
|
||||||
<h5
|
|
||||||
className="mb-0 stretched-link text-heading text-start"
|
|
||||||
onClick={handleViewProject}
|
|
||||||
>
|
|
||||||
{project?.shortName ? project?.shortName : project?.name}
|
|
||||||
</h5>
|
|
||||||
<div className="client-info text-body text-start">
|
|
||||||
<span>{project?.shortName ? project?.name : ""}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`ms-auto ${!ManageProject && "d-none"}`}>
|
|
||||||
<div className="dropdown z-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-offset="0,8"
|
|
||||||
data-bs-placement="top"
|
|
||||||
data-bs-custom-class="tooltip-dark"
|
|
||||||
title="More Action"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
aria-label="click to View details"
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={handleViewProject}
|
|
||||||
>
|
|
||||||
<i className="bx bx-detail me-2"></i>
|
|
||||||
<span className="align-left">View details</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a className="dropdown-item" onClick={handleManage}>
|
|
||||||
<i className="bx bx-pencil me-2"></i>
|
|
||||||
<span className="align-left">Modify</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{!isCore && (
|
|
||||||
<li
|
|
||||||
onClick={() =>
|
|
||||||
setDeleteProject({ project: project, isOpen: true })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a className="dropdown-item">
|
|
||||||
<i className="bx bx-trash me-2"></i>
|
|
||||||
<span className="align-left">Delete</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card View */}
|
|
||||||
|
|
||||||
<div className="card-body pb-1">
|
|
||||||
<div className="d-flex align-items-center flex-wrap">
|
|
||||||
<div className="text-start mb-4 ms-2">
|
|
||||||
<div className="ms-11 mb-3">
|
|
||||||
<p className="mb-0">
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
"badge rounded-pill " +
|
|
||||||
getProjectStatusColor(project?.status?.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{getProjectStatusName(project?.status?.id)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mb-1">
|
|
||||||
<span className="text-heading fw-medium">Contact Person: </span>
|
|
||||||
{project?.contactName}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mb-1">
|
|
||||||
<span className="text-heading fw-medium">Assigned Date: </span>
|
|
||||||
{formatUTCToLocalTime(project?.assignedDate)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mb-0">
|
|
||||||
<span className="text-heading fw-medium">Address: </span>
|
|
||||||
{project?.address}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<p className="mb-0">{project?.projectAddress}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body border-top text-start ms-2">
|
|
||||||
<div className="mt-n3">
|
|
||||||
<p className="mb-0">
|
|
||||||
<i className="bx bx-group bx-sm me-1_5"></i>
|
|
||||||
{project?.teamMemberCount} Employees
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Heading */}
|
|
||||||
<div className="mt-3">
|
|
||||||
<h5 className="mb-3 text-heading fw-semibold">Jobs</h5>
|
|
||||||
|
|
||||||
{/* Job details */}
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<p className="mb-2">
|
|
||||||
<i className="bx bx-briefcase bx-sm me-1"></i>
|
|
||||||
{project?.assignedJobsCount} Assigned Jobs
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mb-2">
|
|
||||||
<i className="bx bx-task bx-sm me-1"></i>
|
|
||||||
{project?.activeJobsCount} Active Jobs
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mb-2">
|
|
||||||
<i className="bx bx-time-five bx-sm me-1"></i>
|
|
||||||
{project?.jobsPassedDueDateCount} Job Passes Due Date
|
|
||||||
</p>
|
|
||||||
<p className="mb-2">
|
|
||||||
<i className="bx bx-pause-circle bx-sm me-1"></i>
|
|
||||||
{project?.onHoldJobsCount} On Hold Jobs
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceProjectCard;
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { MANAGE_PROJECT, PROJECT_STATUS } from "../../../utils/constants";
|
|
||||||
import { useProjects } from "../../../hooks/useProjects";
|
|
||||||
import { formatNumber, formatUTCToLocalTime } from "../../../utils/dateUtils";
|
|
||||||
import ProgressBar from "../../common/ProgressBar";
|
|
||||||
import {
|
|
||||||
getProjectStatusColor,
|
|
||||||
getProjectStatusName,
|
|
||||||
} from "../../../utils/projectStatus";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { setProjectId } from "../../../slices/localVariablesSlice";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
|
|
||||||
import { useProjectContext } from "../../../pages/project/ProjectPage";
|
|
||||||
import usePagination from "../../../hooks/usePagination";
|
|
||||||
import Pagination from "../../common/Pagination";
|
|
||||||
|
|
||||||
const ServiceProjectList = ({
|
|
||||||
data,
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
paginate,
|
|
||||||
isCore = true,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { setMangeProject, setManageServiceProject } = useProjectContext();
|
|
||||||
const handleClose = () => setShowModal(false);
|
|
||||||
|
|
||||||
// check Permissions
|
|
||||||
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
|
||||||
|
|
||||||
const projectColumns = [
|
|
||||||
{
|
|
||||||
key: "projectName",
|
|
||||||
label: "Project Name",
|
|
||||||
className: "text-start py-3",
|
|
||||||
getValue: (p) => (
|
|
||||||
<div
|
|
||||||
className="text-primary cursor-pointer fw-bold py-3"
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(setProjectId(p.id));
|
|
||||||
navigate(`/service-projects/${p.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{p.shortName ? `${p.name} (${p.shortName})` : p.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "client.contactPerson",
|
|
||||||
label: "Contact Person",
|
|
||||||
className: "text-start small",
|
|
||||||
getValue: (p) => p.client?.contactPerson || "N/A",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "assignedDate",
|
|
||||||
label: "Assign Date",
|
|
||||||
className: "text-center small",
|
|
||||||
getValue: (p) => formatUTCToLocalTime(p.assignedDate),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
label: "Status",
|
|
||||||
className: "text-center small",
|
|
||||||
getValue: (p) => (
|
|
||||||
<span className={`badge ${getProjectStatusColor(p.status?.id)}`}>
|
|
||||||
{p.status?.status}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleViewProject = (p) => {
|
|
||||||
if (isCore) {
|
|
||||||
dispatch(setProjectId(p.id));
|
|
||||||
navigate(`/projects/details`);
|
|
||||||
} else {
|
|
||||||
navigate(`/service-projects/${p.id}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManage = (p) => {
|
|
||||||
if (isCore) {
|
|
||||||
setMangeProject({
|
|
||||||
isOpen: true,
|
|
||||||
Project: p.id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setManageServiceProject({
|
|
||||||
isOpen: true,
|
|
||||||
project: p.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="card page-min-h py-4 px-6 shadow-sm">
|
|
||||||
<div className="table-responsive text-nowrap page-min-h">
|
|
||||||
<table className="table table-hover align-middle m-0">
|
|
||||||
<thead className="border-bottom ">
|
|
||||||
<tr>
|
|
||||||
{projectColumns.map((col) => (
|
|
||||||
<th
|
|
||||||
key={col.key}
|
|
||||||
colSpan={col.colSpan}
|
|
||||||
className={`${col.className} table_header_border`}
|
|
||||||
>
|
|
||||||
{col.label}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
<th className="text-center py-3">Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data?.length > 0 ? (
|
|
||||||
data.map((project) => (
|
|
||||||
<tr key={project.id}>
|
|
||||||
{projectColumns.map((col) => (
|
|
||||||
<td
|
|
||||||
key={col.key}
|
|
||||||
colSpan={col.colSpan}
|
|
||||||
className={`${col.className} py-5`}
|
|
||||||
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
|
||||||
>
|
|
||||||
{col.getValue
|
|
||||||
? col.getValue(project)
|
|
||||||
: project[col.key] || "N/A"}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
<td
|
|
||||||
className={`mx-2 ${
|
|
||||||
canManageProject ? "d-sm-table-cell" : "d-none"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="dropdown z-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-offset="0,8"
|
|
||||||
data-bs-placement="top"
|
|
||||||
data-bs-custom-class="tooltip-dark"
|
|
||||||
title="More Action"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
aria-label="click to View details"
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={() => handleViewProject(project)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-detail me-2"></i>
|
|
||||||
<span className="align-left">View details</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={() => handleManage(project)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-pencil me-2"></i>
|
|
||||||
<span className="align-left">Modify</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr
|
|
||||||
className="no-hover"
|
|
||||||
style={{
|
|
||||||
pointerEvents: "none",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
colSpan={projectColumns.length + 1}
|
|
||||||
className="text-center align-middle"
|
|
||||||
style={{ height: "300px", borderBottom: "none" }}
|
|
||||||
>
|
|
||||||
No Service projects available
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
paginate={paginate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceProjectList;
|
|
||||||
@ -1,267 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useModal } from "../../../hooks/useAuth";
|
|
||||||
import Modal from "../../common/Modal";
|
|
||||||
import Label from "../../common/Label";
|
|
||||||
import { useTeamRole } from "../../../hooks/masterHook/useMaster";
|
|
||||||
import SelectField from "../../common/Forms/SelectField";
|
|
||||||
import SelectFieldServerSide from "../../common/Forms/SelectFieldServerSide";
|
|
||||||
import SelectEmployeeServerSide from "../../common/Forms/SelectFieldServerSide";
|
|
||||||
import Avatar from "../../common/Avatar";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { EmployeeChip } from "../../common/Chips";
|
|
||||||
import {
|
|
||||||
useAllocationServiceProjectTeam,
|
|
||||||
useServiceProjectTeam,
|
|
||||||
} from "../../../hooks/useServiceProject";
|
|
||||||
import { SpinnerLoader } from "../../common/Loader";
|
|
||||||
import showToast from "../../../services/toastService";
|
|
||||||
import ConfirmModal from "../../common/ConfirmModal";
|
|
||||||
|
|
||||||
const ServiceProjectTeamAllocation = () => {
|
|
||||||
const { isOpen, onClose, data: Project } = useModal("ServiceTeamAllocation");
|
|
||||||
const [deletingEmp, setSeletingEmp] = useState({
|
|
||||||
employee: null,
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
data: Team,
|
|
||||||
isLoading: isTeamLoading,
|
|
||||||
isError: isTeamError,
|
|
||||||
error: teamError,
|
|
||||||
} = useServiceProjectTeam(Project?.projectId, true);
|
|
||||||
const [isAddEmployee, setIsAddEmployee] = useState(false);
|
|
||||||
const [nonDuplicateEmployee, setNonDuplicateEmployee] = useState([]);
|
|
||||||
const [selectedTeam, setSelectTeam] = useState(null);
|
|
||||||
const [selectedEmployees, setSelectedEmployees] = useState([]);
|
|
||||||
const { data, isLoading } = useTeamRole();
|
|
||||||
|
|
||||||
const TeamAllocationColumns = [
|
|
||||||
{
|
|
||||||
key: "team",
|
|
||||||
label: "Role",
|
|
||||||
getValue: (e) => e?.teamName || "-",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const handleRemove = (id) => {
|
|
||||||
setSelectedEmployees((prev) => prev.filter((e) => e.id !== id));
|
|
||||||
};
|
|
||||||
const { mutate: AllocationTeam, isPending } = useAllocationServiceProjectTeam(
|
|
||||||
() => {
|
|
||||||
setSelectedEmployees([]);
|
|
||||||
setSeletingEmp({
|
|
||||||
employee: null,
|
|
||||||
isOpen: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const AssignEmployee = () => {
|
|
||||||
let payload = selectedEmployees.map((e) => {
|
|
||||||
return {
|
|
||||||
projectId: Project?.projectId,
|
|
||||||
employeeId: e.id,
|
|
||||||
teamRoleId: selectedTeam,
|
|
||||||
isActive: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
AllocationTeam({ payload, isActive: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeAllocation = (emp) => {
|
|
||||||
let payload = [
|
|
||||||
{
|
|
||||||
projectId: Project?.projectId,
|
|
||||||
employeeId: emp.employee.id,
|
|
||||||
teamRoleId: emp.teamRole.id,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
AllocationTeam({ payload: payload, isActive: false });
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedEmployees?.length > 0 && !selectedTeam) {
|
|
||||||
handleRemove(selectedEmployees[0]?.id);
|
|
||||||
showToast(`Please select a role`, "warning");
|
|
||||||
}
|
|
||||||
if (Team?.length > 0 && selectedEmployees?.length > 0) {
|
|
||||||
setNonDuplicateEmployee((prev) => {
|
|
||||||
let updated = [...prev];
|
|
||||||
|
|
||||||
selectedEmployees.forEach((selectedEmployee) => {
|
|
||||||
const alreadyAllocated = Team.some(
|
|
||||||
(e) =>
|
|
||||||
e?.employee?.id === selectedEmployee?.id &&
|
|
||||||
e?.teamRole?.id === selectedTeam
|
|
||||||
);
|
|
||||||
if (alreadyAllocated) {
|
|
||||||
handleRemove(selectedEmployee?.id);
|
|
||||||
showToast(
|
|
||||||
`${selectedEmployee.firstName} ${selectedEmployee.lastName} already allocated with same role`,
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [Team, selectedEmployees, selectedTeam]);
|
|
||||||
|
|
||||||
const TeamAllocationBody = (
|
|
||||||
<>
|
|
||||||
<ConfirmModal
|
|
||||||
type="delete"
|
|
||||||
header="Remove Employee"
|
|
||||||
message="Are you sure you want remove?"
|
|
||||||
onSubmit={handleDeAllocation}
|
|
||||||
onClose={() => setSeletingEmp({ employee: null, isOpen: false })}
|
|
||||||
loading={isPending}
|
|
||||||
paramData={deletingEmp.employee}
|
|
||||||
isOpen={deletingEmp.isOpen}
|
|
||||||
/>
|
|
||||||
<div className=" text-start">
|
|
||||||
<div className="row">
|
|
||||||
<div className="d-flex justify-content-end">
|
|
||||||
{!isAddEmployee && (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
onClick={() => setIsAddEmployee(!isAddEmployee)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle me-2"></i>Add Employee
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isAddEmployee && (
|
|
||||||
<>
|
|
||||||
<div className="col-12 col-md-6 mb-3">
|
|
||||||
<SelectField
|
|
||||||
label="Select Role"
|
|
||||||
required
|
|
||||||
options={data}
|
|
||||||
value={selectedTeam}
|
|
||||||
labelKey="name"
|
|
||||||
valueKey="id"
|
|
||||||
onChange={(e) => {
|
|
||||||
setSelectedEmployees([]);
|
|
||||||
setSelectTeam(e);
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-3">
|
|
||||||
<SelectEmployeeServerSide
|
|
||||||
label="Select Employee"
|
|
||||||
isMultiple={true}
|
|
||||||
isFullObject={true}
|
|
||||||
value={selectedEmployees}
|
|
||||||
onChange={setSelectedEmployees}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
|
|
||||||
{selectedEmployees.map((e) => (
|
|
||||||
<EmployeeChip key={`${e.id}-emp`} handleRemove={handleRemove} employee={e} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isAddEmployee && (
|
|
||||||
<div className="d-flex justify-content-end my-2">
|
|
||||||
{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-label-secondary btn-sm me-2"
|
|
||||||
onClick={() => setIsAddEmployee(false)}
|
|
||||||
aria-label="Close"
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
disabled={isPending}
|
|
||||||
onClick={AssignEmployee}
|
|
||||||
>
|
|
||||||
{isPending && !deletingEmp.employee ? "Please wait..." : "Save"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="col-12">
|
|
||||||
<table className="table text-center">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{Team?.length > 0 ? (
|
|
||||||
Team?.map((emp) => (
|
|
||||||
<tr key={emp?.id}>
|
|
||||||
<td className="w-min">
|
|
||||||
<div className="d-flex align-items-center gap-2">
|
|
||||||
<Avatar
|
|
||||||
size="xs"
|
|
||||||
firstName={emp?.employee?.firstName}
|
|
||||||
lastName={emp?.employee?.lastName}
|
|
||||||
/>
|
|
||||||
<span className="fw-medium">
|
|
||||||
{emp?.employee?.firstName} {emp?.employee?.lastName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>{emp?.teamRole?.name}</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{deletingEmp?.employee?.id === emp.id && isPending ? (
|
|
||||||
<div className="spinner-border spinner-border-sm"></div>
|
|
||||||
) : (
|
|
||||||
<i
|
|
||||||
className="bx bx-trash bx-sm text-danger cursor-pointer"
|
|
||||||
onClick={() =>
|
|
||||||
setSeletingEmp({ employee: emp, isOpen: true })
|
|
||||||
}
|
|
||||||
></i>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={3}
|
|
||||||
className="text-center text-muted py-4 border-0"
|
|
||||||
>
|
|
||||||
{isTeamLoading ? (
|
|
||||||
<SpinnerLoader />
|
|
||||||
) : (
|
|
||||||
<p className="m-0 py-4">No Records Found</p>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size="lg"
|
|
||||||
title={"Team Allocation"}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
body={TeamAllocationBody}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceProjectTeamAllocation;
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Avatar from "../../common/Avatar";
|
|
||||||
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
|
|
||||||
import { useServiceProjectTeam } from "../../../hooks/useServiceProject";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { SpinnerLoader } from "../../common/Loader";
|
|
||||||
|
|
||||||
const ServiceProjectTeamList = () => {
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const { data, isLoading, isError, error } = useServiceProjectTeam(
|
|
||||||
projectId,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const servceProjectColmen = [
|
|
||||||
{
|
|
||||||
key: "employeName",
|
|
||||||
label: "Name",
|
|
||||||
getValue: (e) => (
|
|
||||||
<div className="d-flex align-items-center cursor-pointer"
|
|
||||||
onClick={() => navigate(`/employee/${e.employee.id}`)}>
|
|
||||||
{" "}
|
|
||||||
<Avatar size="xs"
|
|
||||||
firstName={e.employee.firstName}
|
|
||||||
lastName={e.employee.lastName}
|
|
||||||
/>
|
|
||||||
<small>{`${e.employee.firstName} ${e.employee.lastName}`}</small>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
align: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "teamRole",
|
|
||||||
label: "Role",
|
|
||||||
getValue: (e) => {
|
|
||||||
return (
|
|
||||||
<div className="d-flex align-itmes-center">
|
|
||||||
<span>{`${e.teamRole.name}`}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
align: "text-start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "assignedAt",
|
|
||||||
label: "assigned Date",
|
|
||||||
getValue: (e) =>
|
|
||||||
formatUTCToLocalTime(e.assignedAT)
|
|
||||||
,
|
|
||||||
align: "text-center",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table align-middle mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{servceProjectColmen.map((col) => (
|
|
||||||
<th key={col.key} className={col.align}>{col.label}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{data?.length > 0 ? (
|
|
||||||
data.map((row) => (
|
|
||||||
<tr key={row.id}>
|
|
||||||
{servceProjectColmen.map((col) => (
|
|
||||||
<td key={col.key} className={col.align}>{col.getValue(row)}</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={servceProjectColmen.length}
|
|
||||||
className="text-center py-4 border-0"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<SpinnerLoader />
|
|
||||||
) : (
|
|
||||||
<div className="py-8">No Records Available</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceProjectTeamList;
|
|
||||||
@ -104,7 +104,8 @@ const SubScriptionHistory = ({ tenantId }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item py-1"
|
className="dropdown-item py-1"
|
||||||
onClick={() =>{}
|
onClick={() =>
|
||||||
|
console.log("Download clicked for", item.id)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-cloud-download bx-sm"></i> Download
|
<i className="bx bx-cloud-download bx-sm"></i> Download
|
||||||
|
|||||||
@ -93,9 +93,11 @@ const TenantForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmitTenant = (data) => {
|
const onSubmitTenant = (data) => {
|
||||||
|
console.log("Tenant Data:", data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmitSubScription = (data) => {
|
const onSubmitSubScription = (data) => {
|
||||||
|
console.log("Subscription Data:", data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const newTenantConfig = [
|
const newTenantConfig = [
|
||||||
|
|||||||
@ -72,7 +72,7 @@ const AddPayment = ({ onClose }) => {
|
|||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
<div className="col-12 col-md-6 mb-2">
|
||||||
<Label required>Transaction Date </Label>
|
<Label required>Transaction Date </Label>
|
||||||
<DatePicker className="w-100"
|
<DatePicker
|
||||||
name="paymentReceivedDate"
|
name="paymentReceivedDate"
|
||||||
control={control}
|
control={control}
|
||||||
minDate={
|
minDate={
|
||||||
|
|||||||
@ -29,17 +29,18 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
const searchDebounce = useDebounce(searchString, 500);
|
const searchDebounce = useDebounce(searchString, 500);
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useCollections(
|
const { data, isLoading, isError, error } = useCollections(
|
||||||
selectedProject,
|
|
||||||
searchDebounce,
|
|
||||||
localToUtc(fromDate),
|
|
||||||
localToUtc(toDate),
|
|
||||||
ITEMS_PER_PAGE,
|
ITEMS_PER_PAGE,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
localToUtc(fromDate),
|
||||||
|
localToUtc(toDate),
|
||||||
|
isPending,
|
||||||
true,
|
true,
|
||||||
isPending
|
selectedProject,
|
||||||
|
searchDebounce
|
||||||
);
|
);
|
||||||
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
const { setProcessedPayment, setAddPayment, setViewCollection } =
|
||||||
useCollectionContext();
|
useCollectionContext();
|
||||||
|
|
||||||
const paginate = (page) => {
|
const paginate = (page) => {
|
||||||
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@ -112,16 +113,6 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
),
|
),
|
||||||
align: "text-center",
|
align: "text-center",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
label: "Status",
|
|
||||||
getValue: (col) => (
|
|
||||||
<span className={`badge bg-label-${col?.isActive ? "primary" : "danger"}`}>
|
|
||||||
{col?.isActive ? "Active" : "Inactive"}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
align: "text-center",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
label: "Total Amount",
|
label: "Total Amount",
|
||||||
@ -138,7 +129,6 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
),
|
),
|
||||||
align: "text-end",
|
align: "text-end",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: "balance",
|
key: "balance",
|
||||||
label: "Balance",
|
label: "Balance",
|
||||||
@ -196,84 +186,84 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
|
|||||||
canViewCollection ||
|
canViewCollection ||
|
||||||
canEditCollection ||
|
canEditCollection ||
|
||||||
canCreate) && (
|
canCreate) && (
|
||||||
<td
|
<td
|
||||||
className="sticky-action-column text-center"
|
className="sticky-action-column text-center"
|
||||||
style={{ padding: "12px 8px" }}
|
style={{ padding: "12px 8px" }}
|
||||||
>
|
>
|
||||||
<div className="dropdown z-2">
|
<div className="dropdown z-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
data-bs-offset="0,8"
|
data-bs-offset="0,8"
|
||||||
data-bs-placement="top"
|
data-bs-placement="top"
|
||||||
data-bs-custom-class="tooltip-dark"
|
data-bs-custom-class="tooltip-dark"
|
||||||
title="More Action"
|
title="More Action"
|
||||||
></i>
|
></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
<ul className="dropdown-menu dropdown-menu-end">
|
||||||
{/* View */}
|
{/* View */}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
onClick={() => setViewCollection(row.id)}
|
onClick={() => setViewCollection(row.id)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-show me-2 text-primary"></i>
|
<i className="bx bx-show me-2 text-primary"></i>
|
||||||
<span>View</span>
|
<span>View</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* Only if not completed */}
|
{/* Only if not completed */}
|
||||||
{!row?.markAsCompleted && (
|
{!row?.markAsCompleted && (
|
||||||
<>
|
<>
|
||||||
{/* Add Payment */}
|
{/* Add Payment */}
|
||||||
{(isAdmin || canAddPayment) && (
|
{(isAdmin || canAddPayment) && (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAddPayment({
|
setAddPayment({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
invoiceId: row.id,
|
invoiceId: row.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-wallet me-2 text-warning"></i>
|
<i className="bx bx-wallet me-2 text-warning"></i>
|
||||||
<span>Add Payment</span>
|
<span>Add Payment</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mark Payment */}
|
{/* Mark Payment */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
className="dropdown-item cursor-pointer"
|
className="dropdown-item cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setProcessedPayment({
|
setProcessedPayment({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
invoiceId: row.id,
|
invoiceId: row.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-check-circle me-2 text-success"></i>
|
<i className="bx bx-check-circle me-2 text-success"></i>
|
||||||
<span>Mark Payment</span>
|
<span>Mark Payment</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const Comment = ({ invoice }) => {
|
|||||||
AddComment(payload);
|
AddComment(payload);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="row pt-1 px-2 pb-3">
|
<div className="row pt-1 px-2">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@ -16,16 +16,10 @@ import {
|
|||||||
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
import { formatFileSize, localToUtc } from "../../utils/appUtils";
|
||||||
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
import { useCollectionContext } from "../../pages/collections/CollectionPage";
|
||||||
import { formatDate } from "../../utils/dateUtils";
|
import { formatDate } from "../../utils/dateUtils";
|
||||||
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
|
|
||||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
|
||||||
import { useOrganizationsList } from "../../hooks/useOrganization";
|
|
||||||
|
|
||||||
const ManageCollection = ({ collectionId, onClose }) => {
|
const ManageCollection = ({ collectionId, onClose }) => {
|
||||||
const { data, isError, isLoading, error } = useCollection(collectionId);
|
const { data, isError, isLoading, error } = useCollection(collectionId);
|
||||||
const { projectNames, projectLoading } = useProjectName(true);
|
const { projectNames, projectLoading } = useProjectName(true);
|
||||||
const { data: organization, isLoading: isLoadingOrganization } =
|
|
||||||
useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
resolver: zodResolver(newCollection),
|
resolver: zodResolver(newCollection),
|
||||||
defaultValues: defaultCollection,
|
defaultValues: defaultCollection,
|
||||||
@ -144,7 +138,6 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
taxAmount: data?.taxAmount,
|
taxAmount: data?.taxAmount,
|
||||||
basicAmount: data?.basicAmount,
|
basicAmount: data?.basicAmount,
|
||||||
description: data?.description,
|
description: data?.description,
|
||||||
billedToId: data?.billedToId,
|
|
||||||
attachments: data.attachments
|
attachments: data.attachments
|
||||||
? data.attachments.map((doc) => ({
|
? data.attachments.map((doc) => ({
|
||||||
fileName: doc.fileName,
|
fileName: doc.fileName,
|
||||||
@ -170,61 +163,36 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-0 text-start">
|
||||||
<div className="row px-md-1 px-0">
|
<div className="row px-md-1 px-0">
|
||||||
<div className="col-12 col-md-6 mb-2">
|
<div className="col-12 col-md-6 mb-2">
|
||||||
<SelectProjectField
|
<Label className="form-label" required>
|
||||||
label="Project"
|
Select Project
|
||||||
required
|
|
||||||
placeholder="Select Project"
|
|
||||||
value={watch("projectId")}
|
|
||||||
onChange={(val) =>
|
|
||||||
setValue("projectId", val, {
|
|
||||||
shouldDirty: true,
|
|
||||||
shouldValidate: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.projectId && (
|
|
||||||
<small className="danger-text">{errors.projectId.message}</small>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
|
||||||
<Label htmlFor="name" required>
|
|
||||||
Bill To
|
|
||||||
</Label>
|
</Label>
|
||||||
<div className="d-flex align-items-center gap-2">
|
<select
|
||||||
<select
|
className="form-select form-select-sm"
|
||||||
className="select2 form-select form-select flex-grow-1"
|
{...register("projectId")}
|
||||||
aria-label="Default select example"
|
>
|
||||||
{...register("billedToId", {
|
<option value="">Select Project</option>
|
||||||
required: "Client is required",
|
{projectLoading ? (
|
||||||
valueAsNumber: false,
|
<option>Loading...</option>
|
||||||
})}
|
) : (
|
||||||
>
|
projectNames?.map((project) => (
|
||||||
{isLoading ? (
|
<option key={project.id} value={project.id}>
|
||||||
<option>Loading...</option>
|
{project.name}
|
||||||
) : (
|
</option>
|
||||||
<>
|
))
|
||||||
<option value="">Select Client</option>
|
)}
|
||||||
{organization?.data?.map((org) => (
|
</select>
|
||||||
<option key={org.id} value={org.id}>
|
{errors.projectId && (
|
||||||
{org.name}
|
<small className="danger-text">
|
||||||
</option>
|
{errors.projectId.message}
|
||||||
))}
|
</small>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{errors?.clientId && (
|
|
||||||
<span className="danger-text">{errors.billedToId.message}</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-6 mb-2">
|
<div className="col-12 col-md-6 mb-2">
|
||||||
<Label required>Title</Label>
|
<Label required>Title</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control"
|
className="form-control form-control-sm"
|
||||||
{...register("title")}
|
{...register("title")}
|
||||||
/>
|
/>
|
||||||
{errors.title && (
|
{errors.title && (
|
||||||
@ -235,7 +203,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<Label required>Invoice Number</Label>
|
<Label required>Invoice Number</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control"
|
className="form-control form-control-sm"
|
||||||
{...register("invoiceNumber")}
|
{...register("invoiceNumber")}
|
||||||
/>
|
/>
|
||||||
{errors.invoiceId && (
|
{errors.invoiceId && (
|
||||||
@ -248,7 +216,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<Label required>E-Invoice Number</Label>
|
<Label required>E-Invoice Number</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control"
|
className="form-control form-control-sm"
|
||||||
{...register("eInvoiceNumber")}
|
{...register("eInvoiceNumber")}
|
||||||
/>
|
/>
|
||||||
{errors.invoiceId && (
|
{errors.invoiceId && (
|
||||||
@ -264,7 +232,6 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
name="invoiceDate"
|
name="invoiceDate"
|
||||||
control={control}
|
control={control}
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
size="md"
|
|
||||||
/>
|
/>
|
||||||
{errors.invoiceDate && (
|
{errors.invoiceDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -279,7 +246,6 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
name="exceptedPaymentDate"
|
name="exceptedPaymentDate"
|
||||||
control={control}
|
control={control}
|
||||||
minDate={watch("invoiceDate")}
|
minDate={watch("invoiceDate")}
|
||||||
size="md"
|
|
||||||
/>
|
/>
|
||||||
{errors.exceptedPaymentDate && (
|
{errors.exceptedPaymentDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -294,7 +260,6 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
name="clientSubmitedDate"
|
name="clientSubmitedDate"
|
||||||
control={control}
|
control={control}
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
size="md"
|
|
||||||
/>
|
/>
|
||||||
{errors.exceptedPaymentDate && (
|
{errors.exceptedPaymentDate && (
|
||||||
<small className="danger-text">
|
<small className="danger-text">
|
||||||
@ -310,7 +275,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="basicAmount"
|
id="basicAmount"
|
||||||
className="form-control form-control"
|
className="form-control form-control-sm"
|
||||||
min="1"
|
min="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
@ -329,7 +294,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="taxAmount"
|
id="taxAmount"
|
||||||
className="form-control form-control"
|
className="form-control form-control-sm"
|
||||||
min="1"
|
min="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
@ -348,7 +313,7 @@ const ManageCollection = ({ collectionId, onClose }) => {
|
|||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
className="form-control form-control"
|
className="form-control form-control-sm"
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
|
|
||||||
if (isLoading) return <CollectionDetailsSkeleton />;
|
if (isLoading) return <CollectionDetailsSkeleton />;
|
||||||
if (isError) return <div>{error.message}</div>;
|
if (isError) return <div>{error.message}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container p-3">
|
<div className="container p-3">
|
||||||
<p className="fs-5 fw-semibold">Collection Details</p>
|
<p className="fs-5 fw-semibold">Collection Details</p>
|
||||||
@ -42,8 +43,9 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
<div>
|
<div>
|
||||||
{" "}
|
{" "}
|
||||||
<span
|
<span
|
||||||
className={`badge bg-label-${data?.isActive ? "primary" : "danger"
|
className={`badge bg-label-${
|
||||||
}`}
|
data?.isActive ? "primary" : "danger"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{data?.isActive ? "Active" : "Inactive"}
|
{data?.isActive ? "Active" : "Inactive"}
|
||||||
</span>
|
</span>
|
||||||
@ -212,8 +214,9 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
<ul className="nav nav-tabs" role="tablist">
|
<ul className="nav nav-tabs" role="tablist">
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
className={`nav-link ${activeTab === "payments" ? "active" : ""
|
className={`nav-link ${
|
||||||
}`}
|
activeTab === "payments" ? "active" : ""
|
||||||
|
}`}
|
||||||
onClick={() => setActiveTab("payments")}
|
onClick={() => setActiveTab("payments")}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -222,8 +225,9 @@ const ViewCollection = ({ onClose }) => {
|
|||||||
</li>
|
</li>
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
className={`nav-link ${activeTab === "details" ? "active" : ""
|
className={`nav-link ${
|
||||||
}`}
|
activeTab === "details" ? "active" : ""
|
||||||
|
}`}
|
||||||
onClick={() => setActiveTab("details")}
|
onClick={() => setActiveTab("details")}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -19,7 +19,6 @@ export const newCollection = z.object({
|
|||||||
invoiceDate: z.string().min(1, { message: "Date is required" }),
|
invoiceDate: z.string().min(1, { message: "Date is required" }),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
clientSubmitedDate: z.string().min(1, { message: "Date is required" }),
|
clientSubmitedDate: z.string().min(1, { message: "Date is required" }),
|
||||||
billedToId: z.string().min(1, { message: "Date is required" }),
|
|
||||||
exceptedPaymentDate: z.string().min(1, { message: "Date is required" }),
|
exceptedPaymentDate: z.string().min(1, { message: "Date is required" }),
|
||||||
invoiceNumber: z
|
invoiceNumber: z
|
||||||
.string()
|
.string()
|
||||||
@ -76,7 +75,6 @@ export const defaultCollection = {
|
|||||||
taxAmount: "",
|
taxAmount: "",
|
||||||
basicAmount: "",
|
basicAmount: "",
|
||||||
description: "",
|
description: "",
|
||||||
billedToId:"",
|
|
||||||
attachments: [],
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export const EmployeeChip = ({ handleRemove, employee }) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={employee?.id}
|
|
||||||
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
{employee?.photo ? (
|
|
||||||
<span className="tagify__tag__avatar-wrap me-1">
|
|
||||||
<img
|
|
||||||
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
|
|
||||||
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
|
|
||||||
style={{ width: 12, height: 12, objectFit: "cover" }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className="avatar avatar-xs me-2">
|
|
||||||
<span className="avatar-initial roemployeended-circle bg-label-secondary">
|
|
||||||
{employee?.firstName?.[0] || ""}
|
|
||||||
{employee?.lastName?.[0] || ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="d-flex flex-colemployeemn">
|
|
||||||
<span className="tagify__tag-text">
|
|
||||||
{employee?.firstName} {employee?.lastName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<bemployeetton
|
|
||||||
type="bemployeetton"
|
|
||||||
className="tagify__tag__removeBtn border-none"
|
|
||||||
onClick={() => handleRemove(employee?.id)}
|
|
||||||
aria-label={`Remove ${employee?.firstName}`}
|
|
||||||
title="Remove"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -17,13 +17,9 @@ const ConfirmModal = ({
|
|||||||
case "delete":
|
case "delete":
|
||||||
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
|
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
|
||||||
case "success":
|
case "success":
|
||||||
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
|
return <i className="bx bx-check-circle text-success" style={{ fontSize: "60px" }}></i>;
|
||||||
case "archive":
|
case "warning":
|
||||||
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
|
return <i className="bx bx-error-circle text-warning" style={{ fontSize: "60px" }}></i>;
|
||||||
case "Un-archive":
|
|
||||||
return <i className="bx bx-archive-out text-warning" style={{ fontSize: "60px" }}></i>;
|
|
||||||
case "undo":
|
|
||||||
return <i className="bx bx-undo text-info" style={{ fontSize: "50px" }}></i>;
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { useController } from "react-hook-form";
|
|||||||
const DatePicker = ({
|
const DatePicker = ({
|
||||||
name,
|
name,
|
||||||
control,
|
control,
|
||||||
size="sm",
|
|
||||||
placeholder = "DD-MM-YYYY",
|
placeholder = "DD-MM-YYYY",
|
||||||
className = "",
|
className = "",
|
||||||
allowText = false,
|
allowText = false,
|
||||||
@ -52,7 +51,7 @@ const DatePicker = ({
|
|||||||
<div className={`position-relative ${className} w-max `}>
|
<div className={`position-relative ${className} w-max `}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={`form-control form-control form-control-${size}`}
|
className="form-control form-control-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Avatar from "./Avatar";
|
|
||||||
import Tooltip from "./Tooltip";
|
|
||||||
import HoverPopup from "./HoverPopup";
|
|
||||||
import { map } from "zod";
|
|
||||||
|
|
||||||
const EmployeeAvatarGroup = ({ employees = [] }) => {
|
|
||||||
const visibleEmployees = employees.slice(0, 3);
|
|
||||||
const remainingEmployees = employees.slice(3);
|
|
||||||
const remainingCount = employees.length - visibleEmployees.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="d-flex align-items-center avatar-group my-1 text-capitalize ">
|
|
||||||
{visibleEmployees.map((emp, i) => (
|
|
||||||
<div key={i} className="avatar avatar-sm me-1">
|
|
||||||
<HoverPopup title={"Employee"} content={<EmployeeDetails e={emp} />}>
|
|
||||||
{emp.avatarUrl ? (
|
|
||||||
<img
|
|
||||||
src={emp.avatarUrl}
|
|
||||||
alt={`${emp.firstName} ${emp.lastName}`}
|
|
||||||
className="rounded-circle pull-up"
|
|
||||||
title={`${emp.firstName} ${emp.lastName}`}
|
|
||||||
width="40"
|
|
||||||
height="40"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Avatar
|
|
||||||
firstName={emp.firstName}
|
|
||||||
lastName={emp.lastName}
|
|
||||||
classAvatar="pull-up"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HoverPopup>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<div className="avatar p-1">
|
|
||||||
<div className={`avatar avatar-sm `}>
|
|
||||||
<span className="avatar-initial rounded-circle pull-up text-dark border">
|
|
||||||
<HoverPopup
|
|
||||||
title={`More ${remainingCount} emplopyee`}
|
|
||||||
content={<Remaning emp={remainingEmployees} />}
|
|
||||||
>
|
|
||||||
+{remainingCount}
|
|
||||||
</HoverPopup>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeAvatarGroup;
|
|
||||||
|
|
||||||
const EmployeeDetails = ({ e }) => {
|
|
||||||
return (
|
|
||||||
<a className="list-group-item list-group-item-action d-flex justify-content-between text-capitalize">
|
|
||||||
<div className="li-wrapper d-flex justify-content-start align-items-start">
|
|
||||||
<Avatar firstName={e?.firstName} lastName={e?.lastName} />
|
|
||||||
<div className="list-content">
|
|
||||||
<p className="mb-0">{`${e?.firstName}' ${e?.lastName} `}</p>
|
|
||||||
<small className="text-body-secondary">
|
|
||||||
{e?.jobRoleName ?? "employee"}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const Remaning = ({ emp }) => {
|
|
||||||
return (
|
|
||||||
<div className="w-100">
|
|
||||||
{emp?.map((e) => (
|
|
||||||
<a className="list-group-item list-group-item-action d-flex justify-content-between text-capitalize">
|
|
||||||
<div className="li-wrapper d-flex justify-content-start align-items-start">
|
|
||||||
<Avatar firstName={e?.firstName} lastName={e?.lastName} />
|
|
||||||
<div className="list-content">
|
|
||||||
<p className="mb-0">{`${e?.firstName} ${e?.lastName} `}</p>
|
|
||||||
<small className="text-body-secondary">
|
|
||||||
{e?.jobRoleName ?? "employee"}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -7,7 +7,6 @@ import Avatar from "./Avatar";
|
|||||||
const EmployeeSearchInput = ({
|
const EmployeeSearchInput = ({
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
size = "sm",
|
|
||||||
projectId,
|
projectId,
|
||||||
placeholder,
|
placeholder,
|
||||||
forAll,
|
forAll,
|
||||||
@ -47,7 +46,7 @@ const EmployeeSearchInput = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`form-control form-control-sm-${size}`}
|
className={`form-control form-control-sm`}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import Label from "../Label";
|
|
||||||
|
|
||||||
const InputSuggessionField = ({
|
|
||||||
suggesstionList = [],
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
error,
|
|
||||||
disabled = false,
|
|
||||||
label = "Label",
|
|
||||||
placeholder = "Please Enter",
|
|
||||||
required = false,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectedOption = suggesstionList.find((opt) => opt === value);
|
|
||||||
|
|
||||||
const displayText = selectedOption ? selectedOption : placeholder;
|
|
||||||
|
|
||||||
const handleSelect = (option) => {
|
|
||||||
onChange(option);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDropdown = () => setOpen((prev) => !prev);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 position-relative" ref={dropdownRef}>
|
|
||||||
{label && (
|
|
||||||
<Label className="form-label" required={required}>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
|
|
||||||
open ? "show" : ""
|
|
||||||
}`}
|
|
||||||
onClick={toggleDropdown}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`text-truncate ${!selectedOption ? "text-muted" : ""}`}
|
|
||||||
>
|
|
||||||
{isLoading ? "Loading..." : displayText}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open && !isLoading && (
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "100%",
|
|
||||||
left: 0,
|
|
||||||
zIndex: 1050,
|
|
||||||
marginTop: "4px",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{suggesstionList.map((option, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`dropdown-item ${option === value ? "active" : ""}`}
|
|
||||||
onClick={() => handleSelect(option)}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InputSuggessionField;
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import Label from "../Label";
|
|
||||||
|
|
||||||
const SelectField = ({
|
|
||||||
label = "Select",
|
|
||||||
options = [],
|
|
||||||
placeholder = "Select Option",
|
|
||||||
required = false,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
valueKey = "id",
|
|
||||||
labelKey = "name",
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectedOption = options.find((opt) => opt[valueKey] === value);
|
|
||||||
|
|
||||||
const displayText = selectedOption ? selectedOption[labelKey] : placeholder;
|
|
||||||
|
|
||||||
const handleSelect = (option) => {
|
|
||||||
onChange(option[valueKey]);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDropdown = () => setOpen((prev) => !prev);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 position-relative" ref={dropdownRef}>
|
|
||||||
{label && (
|
|
||||||
<Label className="form-label" required={required}>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
|
|
||||||
open ? "show" : ""
|
|
||||||
}`}
|
|
||||||
onClick={toggleDropdown}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`text-truncate ${!selectedOption ? "text-muted" : ""}`}
|
|
||||||
>
|
|
||||||
{isLoading ? "Loading..." : displayText}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open && !isLoading && (
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "100%",
|
|
||||||
left: 0,
|
|
||||||
zIndex: 1050,
|
|
||||||
marginTop: "4px",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{options.map((option, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`dropdown-item ${
|
|
||||||
option[valueKey] === value ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSelect(option)}
|
|
||||||
>
|
|
||||||
{option[labelKey]}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectField;
|
|
||||||
@ -1,574 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import Label from "../Label";
|
|
||||||
import { useDebounce } from "../../../utils/appUtils";
|
|
||||||
import { useEmployeesName } from "../../../hooks/useEmployees";
|
|
||||||
import { useProjectBothName } from "../../../hooks/useProjects";
|
|
||||||
import EmployeeRepository from "../../../repositories/EmployeeRepository";
|
|
||||||
|
|
||||||
const SelectEmployeeServerSide = ({
|
|
||||||
label = "Select",
|
|
||||||
placeholder = "Select Employee",
|
|
||||||
required = false,
|
|
||||||
value = null,
|
|
||||||
onChange,
|
|
||||||
valueKey = "id",
|
|
||||||
isFullObject = false,
|
|
||||||
isMultiple = false,
|
|
||||||
projectId = null,
|
|
||||||
isAllEmployee = false,
|
|
||||||
}) => {
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
const debounce = useDebounce(searchText, 300);
|
|
||||||
const [forcedSelected, setForcedSelected] = useState(null);
|
|
||||||
|
|
||||||
const { data, isLoading } = useEmployeesName(
|
|
||||||
projectId,
|
|
||||||
debounce,
|
|
||||||
isAllEmployee
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = data?.data ?? [];
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
|
|
||||||
const getDisplayName = (emp) => {
|
|
||||||
if (!emp) return "";
|
|
||||||
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
let selectedSingle = null;
|
|
||||||
if (!isMultiple) {
|
|
||||||
if (isFullObject && value) selectedSingle = value;
|
|
||||||
else if (!isFullObject && value)
|
|
||||||
selectedSingle =
|
|
||||||
options.find((o) => o[valueKey] === value) || forcedSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedList = [];
|
|
||||||
if (isMultiple && Array.isArray(value)) {
|
|
||||||
if (isFullObject) selectedList = value;
|
|
||||||
else {
|
|
||||||
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayText = !isMultiple
|
|
||||||
? getDisplayName(selectedSingle) || placeholder
|
|
||||||
: selectedList.length > 0
|
|
||||||
? selectedList.map((e) => getDisplayName(e)).join(", ")
|
|
||||||
: placeholder;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSelect = (option) => {
|
|
||||||
if (!isMultiple) {
|
|
||||||
if (isFullObject) onChange(option);
|
|
||||||
else onChange(option[valueKey]);
|
|
||||||
setOpen(false);
|
|
||||||
} else {
|
|
||||||
let updated = [];
|
|
||||||
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
|
|
||||||
updated = exists
|
|
||||||
? selectedList.filter((e) => e[valueKey] !== option[valueKey])
|
|
||||||
: [...selectedList, option];
|
|
||||||
if (isFullObject) onChange(updated);
|
|
||||||
else onChange(updated.map((x) => x[valueKey]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!value || isFullObject) return;
|
|
||||||
|
|
||||||
const exists = options.some((o) => o[valueKey] === value);
|
|
||||||
if (exists) return;
|
|
||||||
|
|
||||||
const loadSingleEmployee = async () => {
|
|
||||||
try {
|
|
||||||
const emp = await EmployeeRepository.getEmployeeName(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
value
|
|
||||||
);
|
|
||||||
setForcedSelected(emp.data[0]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to load selected employee", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSingleEmployee();
|
|
||||||
}, [value, options, isFullObject, valueKey]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 position-relative" ref={dropdownRef}>
|
|
||||||
{label && (
|
|
||||||
<Label className="form-label" required={required}>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* MAIN BUTTON */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
|
|
||||||
open ? "show" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
|
|
||||||
{displayText}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "100%",
|
|
||||||
left: 0,
|
|
||||||
zIndex: 1050,
|
|
||||||
marginTop: "4px",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
padding: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<li className="p-1 sticky-top bg-white" style={{ zIndex: 10 }}>
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
placeholder="Search..."
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<li className="dropdown-item text-muted text-center">Loading...</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && options.length === 0 && (
|
|
||||||
<li className="dropdown-item text-muted text-center">
|
|
||||||
No results found
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading &&
|
|
||||||
options.map((option) => {
|
|
||||||
const isActive = isMultiple
|
|
||||||
? selectedList.some((x) => x[valueKey] === option[valueKey])
|
|
||||||
: selectedSingle &&
|
|
||||||
selectedSingle[valueKey] === option[valueKey];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={option[valueKey]} className="px-1 rounded">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`dropdown-item rounded ${
|
|
||||||
isActive ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSelect(option)}
|
|
||||||
>
|
|
||||||
{getDisplayName(option)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectEmployeeServerSide;
|
|
||||||
|
|
||||||
export const SelectProjectField = ({
|
|
||||||
label = "Select",
|
|
||||||
placeholder = "Select Project",
|
|
||||||
required = false,
|
|
||||||
value = null,
|
|
||||||
onChange,
|
|
||||||
valueKey = "id",
|
|
||||||
isFullObject = false,
|
|
||||||
isMultiple = false,
|
|
||||||
isAllProject = false,
|
|
||||||
disabled
|
|
||||||
}) => {
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
const debounce = useDebounce(searchText, 300);
|
|
||||||
|
|
||||||
const { data, isLoading } = useProjectBothName(debounce);
|
|
||||||
|
|
||||||
const options = data ?? [];
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
|
|
||||||
const getDisplayName = (project) => {
|
|
||||||
if (!project) return "";
|
|
||||||
return `${project.name || ""}`.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
let selectedSingle = null;
|
|
||||||
|
|
||||||
if (!isMultiple) {
|
|
||||||
if (isFullObject && value) selectedSingle = value;
|
|
||||||
else if (!isFullObject && value)
|
|
||||||
selectedSingle = options.find((o) => o[valueKey] === value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedList = [];
|
|
||||||
|
|
||||||
if (isMultiple && Array.isArray(value)) {
|
|
||||||
if (isFullObject) selectedList = value;
|
|
||||||
else {
|
|
||||||
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Main button label */
|
|
||||||
const displayText = !isMultiple
|
|
||||||
? getDisplayName(selectedSingle) || placeholder
|
|
||||||
: selectedList.length > 0
|
|
||||||
? selectedList.map((e) => getDisplayName(e)).join(", ")
|
|
||||||
: placeholder;
|
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* HANDLE OUTSIDE CLICK
|
|
||||||
* ----------------------------- */
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* HANDLE SELECT
|
|
||||||
* ----------------------------- */
|
|
||||||
const handleSelect = (option) => {
|
|
||||||
if (!isMultiple) {
|
|
||||||
// SINGLE SELECT
|
|
||||||
if (isFullObject) onChange(option);
|
|
||||||
else onChange(option[valueKey]);
|
|
||||||
} else {
|
|
||||||
// MULTIPLE SELECT
|
|
||||||
let updated = [];
|
|
||||||
|
|
||||||
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
// remove
|
|
||||||
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
|
|
||||||
} else {
|
|
||||||
// add
|
|
||||||
updated = [...selectedList, option];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFullObject) onChange(updated);
|
|
||||||
else onChange(updated.map((x) => x[valueKey]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 position-relative" ref={dropdownRef}>
|
|
||||||
{label && (
|
|
||||||
<Label className="form-label" required={required}>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* MAIN BUTTON */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
|
|
||||||
open ? "show" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
|
|
||||||
{displayText}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* DROPDOWN */}
|
|
||||||
{open && (
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "100%",
|
|
||||||
left: 0,
|
|
||||||
zIndex: 1050,
|
|
||||||
marginTop: "2px",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="p-1">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
placeholder="Search..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<li className="dropdown-item text-muted text-center">Loading...</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && options.length === 0 && (
|
|
||||||
<li className="dropdown-item text-muted text-center">
|
|
||||||
No results found
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading &&
|
|
||||||
options.map((option) => {
|
|
||||||
const isActive = isMultiple
|
|
||||||
? selectedList.some((x) => x[valueKey] === option[valueKey])
|
|
||||||
: selectedSingle &&
|
|
||||||
selectedSingle[valueKey] === option[valueKey];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={option[valueKey]} className="px-1 rounded w-full">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`dropdown-item rounded d-block text-truncate w-100 ${
|
|
||||||
isActive ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSelect(option)}
|
|
||||||
>
|
|
||||||
{getDisplayName(option)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectFieldSearch = ({
|
|
||||||
label = "Select",
|
|
||||||
placeholder = "Select ",
|
|
||||||
required = false,
|
|
||||||
value = null,
|
|
||||||
onChange,
|
|
||||||
valueKey = "id",
|
|
||||||
labelKey = "name",
|
|
||||||
disabled = false,
|
|
||||||
isFullObject = false,
|
|
||||||
isMultiple = false,
|
|
||||||
hookParams,
|
|
||||||
useFetchHook,
|
|
||||||
}) => {
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
const debounce = useDebounce(searchText, 300);
|
|
||||||
|
|
||||||
const { data, isLoading } = useFetchHook(...hookParams, debounce);
|
|
||||||
const options = data?.data ?? [];
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
|
|
||||||
const getDisplayName = (entity) => {
|
|
||||||
if (!entity) return "";
|
|
||||||
return `${entity[labelKey] || ""}`.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* SELECTED OPTION (SINGLE)
|
|
||||||
* ----------------------------- */
|
|
||||||
let selectedSingle = null;
|
|
||||||
|
|
||||||
if (!isMultiple) {
|
|
||||||
if (isFullObject && value) selectedSingle = value;
|
|
||||||
else if (!isFullObject && value)
|
|
||||||
selectedSingle = options.find((o) => o[valueKey] === value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* SELECTED OPTION (MULTIPLE)
|
|
||||||
* ----------------------------- */
|
|
||||||
let selectedList = [];
|
|
||||||
if (isMultiple && Array.isArray(value)) {
|
|
||||||
if (isFullObject) selectedList = value;
|
|
||||||
else {
|
|
||||||
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Main button label */
|
|
||||||
const displayText = !isMultiple
|
|
||||||
? getDisplayName(selectedSingle) || placeholder
|
|
||||||
: selectedList.length > 0
|
|
||||||
? selectedList.map((e) => getDisplayName(e)).join(", ")
|
|
||||||
: placeholder;
|
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* HANDLE OUTSIDE CLICK
|
|
||||||
* ----------------------------- */
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// MERGED OPTIONS TO ENSURE SELECTED VALUE APPEARS EVEN IF NOT IN SEARCH RESULT
|
|
||||||
const [mergedOptions, setMergedOptions] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let finalList = [...options];
|
|
||||||
|
|
||||||
if (!isMultiple && value && !isFullObject) {
|
|
||||||
// already selected option inside options?
|
|
||||||
const exists = options.some((o) => o[valueKey] === value);
|
|
||||||
|
|
||||||
// if selected item not found, try to get from props (value) as fallback
|
|
||||||
if (!exists && typeof value === "object") {
|
|
||||||
finalList.unshift(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMultiple && Array.isArray(value)) {
|
|
||||||
value.forEach((val) => {
|
|
||||||
const id = isFullObject ? val[valueKey] : val;
|
|
||||||
const exists = options.some((o) => o[valueKey] === id);
|
|
||||||
|
|
||||||
if (!exists && typeof val === "object") {
|
|
||||||
finalList.unshift(val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setMergedOptions(finalList);
|
|
||||||
}, [options, value]);
|
|
||||||
|
|
||||||
/** -----------------------------
|
|
||||||
* HANDLE SELECT
|
|
||||||
* ----------------------------- */
|
|
||||||
const handleSelect = (option) => {
|
|
||||||
if (!isMultiple) {
|
|
||||||
// SINGLE SELECT
|
|
||||||
if (isFullObject) onChange(option);
|
|
||||||
else onChange(option[valueKey]);
|
|
||||||
} else {
|
|
||||||
// MULTIPLE SELECT
|
|
||||||
let updated = [];
|
|
||||||
|
|
||||||
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
// remove
|
|
||||||
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
|
|
||||||
} else {
|
|
||||||
// add
|
|
||||||
updated = [...selectedList, option];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFullObject) onChange(updated);
|
|
||||||
else onChange(updated.map((x) => x[valueKey]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-3 position-relative" ref={dropdownRef}>
|
|
||||||
{label && (
|
|
||||||
<Label className="form-label" required={required}>
|
|
||||||
{label}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* MAIN BUTTON */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
|
|
||||||
open ? "show" : ""
|
|
||||||
}`}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
|
||||||
>
|
|
||||||
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
|
|
||||||
{displayText}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* DROPDOWN */}
|
|
||||||
{open && (
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "100%",
|
|
||||||
left: 0,
|
|
||||||
zIndex: 1050,
|
|
||||||
marginTop: "2px",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="p-1">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
placeholder="Search..."
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<li className="dropdown-item text-muted text-center">Loading...</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && options.length === 0 && (
|
|
||||||
<li className="dropdown-item text-muted text-center">
|
|
||||||
No results found
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading &&
|
|
||||||
options.map((option) => {
|
|
||||||
const isActive = isMultiple
|
|
||||||
? selectedList.some((x) => x[valueKey] === option[valueKey])
|
|
||||||
: selectedSingle &&
|
|
||||||
selectedSingle[valueKey] === option[valueKey];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={option[valueKey]}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`dropdown-item ${isActive ? "active" : ""}`}
|
|
||||||
onClick={() => handleSelect(option)}
|
|
||||||
>
|
|
||||||
{getDisplayName(option)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -28,6 +28,7 @@ const CommentEditor = () => {
|
|||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
console.log("Comment:", value);
|
||||||
// Submit or handle content
|
// Submit or handle content
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,197 +1,68 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import {
|
|
||||||
closePopup,
|
|
||||||
openPopup,
|
|
||||||
togglePopup,
|
|
||||||
} from "../../slices/localVariablesSlice";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* align: "auto" | "left" | "right"
|
|
||||||
* boundaryRef: optional ref to the drawer/container element to use as boundary
|
|
||||||
*/
|
|
||||||
const HoverPopup = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
children,
|
|
||||||
className = "",
|
|
||||||
Mode = "hover",
|
|
||||||
align = "auto",
|
|
||||||
boundaryRef = null,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const visible = useSelector((s) => s.localVariables.popups[id] || false);
|
|
||||||
|
|
||||||
|
const HoverPopup = ({ title, content, children }) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const triggerRef = useRef(null);
|
const triggerRef = useRef(null);
|
||||||
const popupRef = useRef(null);
|
const popupRef = useRef(null);
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
// Toggle popup on hover or click
|
||||||
if (Mode === "hover") dispatch(openPopup(id));
|
const handleMouseEnter = () => setVisible(true);
|
||||||
};
|
const handleClick = () => setVisible((prev) => !prev);
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (Mode === "hover") dispatch(closePopup(id));
|
|
||||||
};
|
|
||||||
const handleClick = (e) => {
|
|
||||||
if (Mode === "click") {
|
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(togglePopup(id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close on outside click when using click mode
|
// Hide popup on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Mode !== "click" || !visible) return;
|
const handleDocumentClick = (e) => {
|
||||||
|
|
||||||
const handler = (e) => {
|
|
||||||
if (
|
if (
|
||||||
popupRef.current &&
|
!popupRef.current?.contains(e.target) &&
|
||||||
!popupRef.current.contains(e.target) &&
|
!triggerRef.current?.contains(e.target)
|
||||||
triggerRef.current &&
|
|
||||||
!triggerRef.current.contains(e.target)
|
|
||||||
) {
|
) {
|
||||||
dispatch(closePopup(id));
|
setVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("click", handler);
|
if (visible) document.addEventListener("click", handleDocumentClick);
|
||||||
return () => document.removeEventListener("click", handler);
|
return () => document.removeEventListener("click", handleDocumentClick);
|
||||||
}, [Mode, visible, dispatch, id]);
|
}, [visible]);
|
||||||
|
|
||||||
// Positioning effect: respects align prop and stays inside boundary (drawer)
|
return (
|
||||||
useEffect(() => {
|
|
||||||
if (!visible || !popupRef.current || !triggerRef.current) return;
|
|
||||||
|
|
||||||
// run in next frame so DOM/layout settles
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const popup = popupRef.current;
|
|
||||||
|
|
||||||
// choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement)
|
|
||||||
const boundaryEl =
|
|
||||||
(boundaryRef && boundaryRef.current) || popup.parentElement;
|
|
||||||
if (!boundaryEl) return;
|
|
||||||
|
|
||||||
const boundaryRect = boundaryEl.getBoundingClientRect();
|
|
||||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
// reset styles first
|
|
||||||
popup.style.left = "";
|
|
||||||
popup.style.right = "";
|
|
||||||
popup.style.transform = "";
|
|
||||||
popup.style.top = "";
|
|
||||||
|
|
||||||
const popupRect = popup.getBoundingClientRect();
|
|
||||||
const parentRect = boundaryRect; // alias
|
|
||||||
|
|
||||||
// Convert trigger center to parent coordinates
|
|
||||||
const triggerCenterX =
|
|
||||||
triggerRect.left + triggerRect.width / 2 - parentRect.left;
|
|
||||||
|
|
||||||
// preferred left so popup center aligns to trigger center:
|
|
||||||
const preferredLeft = triggerCenterX - popupRect.width / 2;
|
|
||||||
|
|
||||||
// Helpers to set styles in parent's coordinate system:
|
|
||||||
const setLeft = (leftPx) => {
|
|
||||||
popup.style.left = `${leftPx}px`;
|
|
||||||
popup.style.right = "auto";
|
|
||||||
popup.style.transform = "none";
|
|
||||||
};
|
|
||||||
const setRight = (rightPx) => {
|
|
||||||
popup.style.left = "auto";
|
|
||||||
popup.style.right = `${rightPx}px`;
|
|
||||||
popup.style.transform = "none";
|
|
||||||
};
|
|
||||||
|
|
||||||
// If user forced align:
|
|
||||||
if (align === "left") {
|
|
||||||
// align popup's left to parent's left (0)
|
|
||||||
setLeft(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (align === "right") {
|
|
||||||
// align popup's right to parent's right (0)
|
|
||||||
setRight(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (align === "center") {
|
|
||||||
popup.style.left = "50%";
|
|
||||||
popup.style.right = "auto";
|
|
||||||
popup.style.transform = "translateX(-50%)";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// align === "auto": try preferred centered position, but flip fully if overflow
|
|
||||||
// clamp preferredLeft to boundaries so it doesn't render partially outside
|
|
||||||
const leftIfCentered = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(preferredLeft, parentRect.width - popupRect.width)
|
|
||||||
);
|
|
||||||
|
|
||||||
// if centered fits, use it
|
|
||||||
if (leftIfCentered === preferredLeft) {
|
|
||||||
setLeft(leftIfCentered);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if centering would overflow right -> stick popup fully to left (left=0)
|
|
||||||
if (preferredLeft > parentRect.width - popupRect.width) {
|
|
||||||
// place popup so its right aligns to parent's right
|
|
||||||
// i.e., left = parent width - popup width
|
|
||||||
setLeft(parentRect.width - popupRect.width);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if centering would overflow left -> stick popup fully to left=0
|
|
||||||
if (preferredLeft < 0) {
|
|
||||||
setLeft(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback center
|
|
||||||
setLeft(leftIfCentered);
|
|
||||||
});
|
|
||||||
}, [visible, align, boundaryRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="d-inline-block position-relative" // <-- ADD THIS !!
|
|
||||||
style={{
|
|
||||||
maxWidth: "calc(700px - 100px)",
|
|
||||||
width: "100%",
|
|
||||||
wordWrap: "break-word",
|
|
||||||
overflow: "visible", // also make sure popup isn't clipped
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="d-inline-block"
|
className="d-inline-block position-relative"
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={() => setVisible(false)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className="bg-white border rounded shadow-sm p-3 text-start"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
zIndex: 1050,
|
||||||
|
minWidth: "240px",
|
||||||
|
maxWidth: "300px",
|
||||||
|
marginTop: "8px",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<h6 className="mb-2 fw-semibold text-dark" style={{ fontSize: "0.9rem" }}>
|
||||||
|
{title}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
<div className="text-muted" style={{ fontSize: "0.85rem", lineHeight: "1.4" }}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{visible && (
|
|
||||||
<div
|
|
||||||
ref={popupRef}
|
|
||||||
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
|
|
||||||
style={{
|
|
||||||
zIndex: 2000,
|
|
||||||
top: "100%",
|
|
||||||
width: "max-content",
|
|
||||||
minWidth: "120px",
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
|
|
||||||
<div>{content}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HoverPopup;
|
export default HoverPopup;
|
||||||
|
|||||||
@ -5,13 +5,14 @@ const InputSuggestions = ({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
disabled = false,
|
disabled=false
|
||||||
}) => {
|
}) => {
|
||||||
const [filteredList, setFilteredList] = useState([]);
|
const [filteredList, setFilteredList] = useState([]);
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
onChange(val);
|
onChange(val);
|
||||||
|
|
||||||
const matches = organizationList.filter((org) =>
|
const matches = organizationList.filter((org) =>
|
||||||
org.toLowerCase().includes(val.toLowerCase())
|
org.toLowerCase().includes(val.toLowerCase())
|
||||||
);
|
);
|
||||||
@ -25,7 +26,7 @@ const InputSuggestions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 position-relative">
|
<div className="position-relative">
|
||||||
<input
|
<input
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
value={value}
|
value={value}
|
||||||
@ -38,19 +39,19 @@ const InputSuggestions = ({
|
|||||||
/>
|
/>
|
||||||
{showSuggestions && filteredList.length > 0 && (
|
{showSuggestions && filteredList.length > 0 && (
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
|
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: "180px",
|
maxHeight: "180px",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
marginTop: "2px",
|
marginTop: "2px",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
borderRadius: "0px",
|
borderRadius:"0px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filteredList.map((org) => (
|
{filteredList.map((org) => (
|
||||||
<li
|
<li
|
||||||
key={org}
|
key={org}
|
||||||
className="ropdown-item"
|
className="list-group-item list-group-item-action border-none "
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
padding: "5px 12px",
|
padding: "5px 12px",
|
||||||
@ -58,15 +59,17 @@ const InputSuggestions = ({
|
|||||||
transition: "background-color 0.2s",
|
transition: "background-color 0.2s",
|
||||||
}}
|
}}
|
||||||
onMouseDown={() => handleSelectSuggestion(org)}
|
onMouseDown={() => handleSelectSuggestion(org)}
|
||||||
className={`dropdown-item ${
|
onMouseEnter={(e) =>
|
||||||
org === value ? "active" : ""
|
(e.currentTarget.style.backgroundColor = "#f8f9fa")
|
||||||
}`}
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.backgroundColor = "transparent")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{org}
|
{org}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <small className="danger-text">{error}</small>}
|
{error && <small className="danger-text">{error}</small>}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const Modal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="modal-body pt-0 text-black">{body}</div>
|
<div className="modal-body pt-0">{body}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px;
|
padding: 5px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
.multi-select-dropdown-option.selected {
|
.multi-select-dropdown-option.selected {
|
||||||
background-color: #dbe7ff;
|
background-color: #dbe7ff;
|
||||||
color: #696cff;
|
color: #0d6efd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-select-dropdown-option input[type="checkbox"] {
|
.multi-select-dropdown-option input[type="checkbox"] {
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
const OffcanvasComponent = ({
|
|
||||||
id = "globalOffcanvas",
|
|
||||||
title = "Offcanvas Title",
|
|
||||||
placement = "start", // start | end | top | bottom
|
|
||||||
children,
|
|
||||||
show = false,
|
|
||||||
onClose = () => { },
|
|
||||||
}) => {
|
|
||||||
const offcanvasRef = useRef(null);
|
|
||||||
const bsInstance = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!offcanvasRef.current) return;
|
|
||||||
|
|
||||||
// initialize once
|
|
||||||
bsInstance.current = bootstrap.Offcanvas.getOrCreateInstance(offcanvasRef.current);
|
|
||||||
|
|
||||||
const el = offcanvasRef.current;
|
|
||||||
const handleHide = () => onClose();
|
|
||||||
|
|
||||||
el.addEventListener("hidden.bs.offcanvas", handleHide);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
el.removeEventListener("hidden.bs.offcanvas", handleHide);
|
|
||||||
};
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
// react to `show` changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!bsInstance.current) return;
|
|
||||||
|
|
||||||
if (show) bsInstance.current.show();
|
|
||||||
else bsInstance.current.hide();
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div >
|
|
||||||
<div
|
|
||||||
ref={offcanvasRef}
|
|
||||||
id={id}
|
|
||||||
className={`offcanvas offcanvas offcanvas-${placement} offcanvas-wide`}
|
|
||||||
tabIndex="-1"
|
|
||||||
aria-labelledby={`${id}-label`}
|
|
||||||
>
|
|
||||||
<div className="offcanvas-header">
|
|
||||||
<div className="d-flex align-items-center gap-3">
|
|
||||||
<i
|
|
||||||
className="bx bx-lg bx-left-arrow-alt cursor-pointer"
|
|
||||||
data-bs-dismiss="offcanvas"
|
|
||||||
aria-label="Close"
|
|
||||||
></i>
|
|
||||||
<i className="bx bx-briefcase text-primary"></i>
|
|
||||||
<h5 className="offcanvas-title mb-0" id={`${id}-label`}>
|
|
||||||
{title}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close"
|
|
||||||
data-bs-dismiss="offcanvas"
|
|
||||||
aria-label="Close"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="offcanvas-body ">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OffcanvasComponent;
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef, useMemo } from "react";
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { useController } from "react-hook-form";
|
import { useController } from "react-hook-form";
|
||||||
import { useDebounce } from "../../utils/appUtils";
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
import { useEmployeesName, useUserCache } from "../../hooks/useEmployees";
|
import { useEmployeesName } from "../../hooks/useEmployees";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
|
|
||||||
const PmsEmployeeInputTag = ({
|
const PmsEmployeeInputTag = ({
|
||||||
@ -11,17 +11,15 @@ const PmsEmployeeInputTag = ({
|
|||||||
projectId,
|
projectId,
|
||||||
forAll,
|
forAll,
|
||||||
isApplicationUser = false,
|
isApplicationUser = false,
|
||||||
disabled,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
field: { value = [], onChange },
|
field: { value = [], onChange },
|
||||||
} = useController({ name, control });
|
} = useController({ name, control });
|
||||||
|
|
||||||
const { getUser, addToCache, userCache } = useUserCache();
|
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
const [filteredUsers, setFilteredUsers] = useState([]);
|
const [filteredUsers, setFilteredUsers] = useState([]);
|
||||||
|
const [userCache, setUserCache] = useState({});
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const activeIndexRef = useRef(-1);
|
const activeIndexRef = useRef(-1);
|
||||||
@ -33,28 +31,26 @@ const PmsEmployeeInputTag = ({
|
|||||||
forAll
|
forAll
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Keep both filtered list and cache updated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (employees?.data?.length) {
|
if (employees?.data?.length) {
|
||||||
setFilteredUsers(employees.data);
|
setFilteredUsers(employees.data);
|
||||||
activeIndexRef.current = -1;
|
activeIndexRef.current = -1;
|
||||||
|
|
||||||
employees.data.forEach((u) => addToCache(u.id, u));
|
// cache all fetched users by id
|
||||||
|
setUserCache((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
employees.data.forEach((u) => {
|
||||||
|
updated[u.id] = u;
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setFilteredUsers([]);
|
setFilteredUsers([]);
|
||||||
}
|
}
|
||||||
}, [employees]);
|
}, [employees]);
|
||||||
|
|
||||||
// load selected employees not in filtered list
|
// close dropdown when clicking outside
|
||||||
useEffect(() => {
|
|
||||||
if (!Array.isArray(value) || value.length === 0) return;
|
|
||||||
|
|
||||||
value.forEach(async (id) => {
|
|
||||||
if (!userCache[id]) {
|
|
||||||
await getUser(id); // fetch and cache
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onDocClick = (e) => {
|
const onDocClick = (e) => {
|
||||||
if (
|
if (
|
||||||
@ -69,6 +65,7 @@ const PmsEmployeeInputTag = ({
|
|||||||
return () => document.removeEventListener("mousedown", onDocClick);
|
return () => document.removeEventListener("mousedown", onDocClick);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// select a user
|
||||||
const handleSelect = (user) => {
|
const handleSelect = (user) => {
|
||||||
if (value.includes(user.id)) return;
|
if (value.includes(user.id)) return;
|
||||||
const updated = [...value, user.id];
|
const updated = [...value, user.id];
|
||||||
@ -78,11 +75,13 @@ const PmsEmployeeInputTag = ({
|
|||||||
setTimeout(() => inputRef.current?.focus(), 0);
|
setTimeout(() => inputRef.current?.focus(), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// remove selected user
|
||||||
const handleRemove = (id) => {
|
const handleRemove = (id) => {
|
||||||
const updated = value.filter((uid) => uid !== id);
|
const updated = value.filter((uid) => uid !== id);
|
||||||
onChange(updated);
|
onChange(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// keyboard navigation
|
||||||
const onInputKeyDown = (e) => {
|
const onInputKeyDown = (e) => {
|
||||||
if (!showDropdown) return;
|
if (!showDropdown) return;
|
||||||
const max = Math.max(0, filteredUsers.length - 1);
|
const max = Math.max(0, filteredUsers.length - 1);
|
||||||
@ -103,6 +102,7 @@ const PmsEmployeeInputTag = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// scroll active dropdown item into view
|
||||||
const scrollToActive = () => {
|
const scrollToActive = () => {
|
||||||
const wrapper = dropdownRef.current?.querySelector(
|
const wrapper = dropdownRef.current?.querySelector(
|
||||||
".tagify__dropdown__wrapper"
|
".tagify__dropdown__wrapper"
|
||||||
@ -119,21 +119,24 @@ const PmsEmployeeInputTag = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ⬆️ FIX: allow null (not found yet)
|
// resolve user details by ID (for rendering tags)
|
||||||
const resolveUserById = (id) => {
|
const resolveUserById = (id) => {
|
||||||
return userCache[id] || filteredUsers.find((u) => u.id === id) || null;
|
return userCache[id] || filteredUsers.find((u) => u.id === id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// main visible users list (memoized)
|
||||||
const visibleUsers = useMemo(() => {
|
const visibleUsers = useMemo(() => {
|
||||||
const baseList = isApplicationUser
|
const baseList = isApplicationUser
|
||||||
? (filteredUsers || []).filter((u) => u?.email)
|
? (filteredUsers || []).filter((u) => u?.email)
|
||||||
: filteredUsers || [];
|
: filteredUsers || [];
|
||||||
|
|
||||||
|
// also include selected users even if missing from current API
|
||||||
const selectedUsers =
|
const selectedUsers =
|
||||||
Array.isArray(value) && value.length
|
Array.isArray(value) && value.length
|
||||||
? value.map((uid) => userCache[uid]).filter(Boolean)
|
? value.map((uid) => userCache[uid]).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// merge unique
|
||||||
const merged = [
|
const merged = [
|
||||||
...selectedUsers,
|
...selectedUsers,
|
||||||
...baseList.filter((u) => !selectedUsers.some((s) => s.id === u.id)),
|
...baseList.filter((u) => !selectedUsers.some((s) => s.id === u.id)),
|
||||||
@ -144,9 +147,10 @@ const PmsEmployeeInputTag = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="tagify form-control d-flex align-items-center flex-wrap position-relative"
|
className="tagify form-control d-flex align-items-center flex-wrap position-relative "
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
>
|
>
|
||||||
|
{/* Selected tags (chips) */}
|
||||||
{value.map((id) => {
|
{value.map((id) => {
|
||||||
const u = resolveUserById(id);
|
const u = resolveUserById(id);
|
||||||
if (!u) return null;
|
if (!u) return null;
|
||||||
@ -183,8 +187,10 @@ const PmsEmployeeInputTag = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="tagify__tag__removeBtn border-none"
|
className="tagify__tag__removeBtn"
|
||||||
onClick={() => handleRemove(id)}
|
onClick={() => handleRemove(id)}
|
||||||
|
aria-label={`Remove ${u.firstName}`}
|
||||||
|
title="Remove"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -197,7 +203,7 @@ const PmsEmployeeInputTag = ({
|
|||||||
id="TagifyUserList"
|
id="TagifyUserList"
|
||||||
name="TagifyUserList"
|
name="TagifyUserList"
|
||||||
className="tagify__input flex-grow-1 border-0 bg-transparent"
|
className="tagify__input flex-grow-1 border-0 bg-transparent"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder || "Type to search users..."}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
setShowDropdown(true);
|
setShowDropdown(true);
|
||||||
@ -209,7 +215,6 @@ const PmsEmployeeInputTag = ({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-expanded={showDropdown}
|
aria-expanded={showDropdown}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
disabled={disabled}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
|
|||||||
@ -11,26 +11,21 @@ const SelectMultiple = ({
|
|||||||
labelKey = "name",
|
labelKey = "name",
|
||||||
valueKey = "id",
|
valueKey = "id",
|
||||||
placeholder = "Please select...",
|
placeholder = "Please select...",
|
||||||
IsLoading = false,
|
IsLoading = false,required = false
|
||||||
required = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { setValue, watch, register } = useFormContext();
|
const { setValue, watch,register } = useFormContext();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
register(name, { value: [] });
|
register(name, { value: [] });
|
||||||
}, [register, name]);
|
}, [register, name]);
|
||||||
|
|
||||||
const selectedValues = watch(name) || [];
|
const selectedValues = watch(name) || [];
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
const [dropdownStyles, setDropdownStyles] = useState({
|
const [dropdownStyles, setDropdownStyles] = useState({ top: 0, left: 0, width: 0 });
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e) => {
|
const handleClickOutside = (e) => {
|
||||||
@ -70,12 +65,13 @@ const SelectMultiple = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredOptions = (options || []).filter((item) => {
|
const filteredOptions = (options || []).filter((item) => {
|
||||||
const label = getLabel(item);
|
const label = getLabel(item);
|
||||||
return (
|
return (
|
||||||
typeof label === "string" &&
|
typeof label === "string" &&
|
||||||
label.toLowerCase().includes(searchText.toLowerCase())
|
label.toLowerCase().includes(searchText.toLowerCase())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const dropdownElement = (
|
const dropdownElement = (
|
||||||
<div
|
<div
|
||||||
@ -101,7 +97,7 @@ const SelectMultiple = ({
|
|||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
className="multi-select-dropdown-search-input"
|
className="multi-select-dropdown-search-input"
|
||||||
style={{ width: "100%", }}
|
style={{ width: "100%", padding: 4 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -113,14 +109,8 @@ const SelectMultiple = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={valueVal}
|
key={valueVal}
|
||||||
className={`multi-select-dropdown-option ${
|
className={`multi-select-dropdown-option ${isChecked ? "selected" : ""}`}
|
||||||
isChecked ? "selected" : ""
|
style={{ display: "flex", alignItems: "center", padding: "4px 8px" }}
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: "4px 8px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -149,11 +139,7 @@ const SelectMultiple = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
|
||||||
ref={containerRef}
|
|
||||||
className="multi-select-dropdown-container"
|
|
||||||
style={{ position: "relative" }}
|
|
||||||
>
|
|
||||||
<label className="form-label mb-1">{label}</label>
|
<label className="form-label mb-1">{label}</label>
|
||||||
<Label className={name} required={required}></Label>
|
<Label className={name} required={required}></Label>
|
||||||
|
|
||||||
@ -164,9 +150,7 @@ const SelectMultiple = ({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
selectedValues.length > 0
|
selectedValues.length > 0 ? "placeholder-style-selected" : "placeholder-style"
|
||||||
? "placeholder-style-selected"
|
|
||||||
: "placeholder-style"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="selected-badges-container">
|
<div className="selected-badges-container">
|
||||||
@ -175,10 +159,7 @@ const SelectMultiple = ({
|
|||||||
const found = options.find((opt) => opt[valueKey] === val);
|
const found = options.find((opt) => opt[valueKey] === val);
|
||||||
const label = found ? getLabel(found) : "";
|
const label = found ? getLabel(found) : "";
|
||||||
return (
|
return (
|
||||||
<span
|
<span key={val} className="badge badge-selected-item mx-1 mb-1">
|
||||||
key={val}
|
|
||||||
className="badge bg-label-primary mx-1 py-2"
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Label from "./Label";
|
import Label from "./Label";
|
||||||
|
|
||||||
const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = false, options = [] }) => {
|
const TagInput = ({ label, name, placeholder, color = "#e9ecef", options = [] }) => {
|
||||||
const { setValue, watch } = useFormContext();
|
const { setValue, watch } = useFormContext();
|
||||||
const tags = watch(name) || [];
|
const tags = watch(name) || [];
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
@ -33,29 +33,29 @@ const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = fals
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setInput(val);
|
setInput(val);
|
||||||
|
|
||||||
if (val) {
|
if (val) {
|
||||||
setSuggestions(
|
setSuggestions(
|
||||||
options
|
options
|
||||||
.filter((opt) => {
|
.filter((opt) => {
|
||||||
const label = typeof opt === "string" ? opt : opt.name;
|
const label = typeof opt === "string" ? opt : opt.name;
|
||||||
return (
|
return (
|
||||||
label.toLowerCase().includes(val.toLowerCase()) &&
|
label.toLowerCase().includes(val.toLowerCase()) &&
|
||||||
!tags.some((t) => t.name === label)
|
!tags.some((t) => t.name === label)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((opt) => ({
|
.map((opt) => ({
|
||||||
name: typeof opt === "string" ? opt : opt.name,
|
name: typeof opt === "string" ? opt : opt.name,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuggestionClick = (sugg) => {
|
const handleSuggestionClick = (sugg) => {
|
||||||
handleAdd(sugg);
|
handleAdd(sugg);
|
||||||
@ -65,13 +65,13 @@ const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = fals
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Label htmlFor={name} className="form-label" required={required}>
|
<label htmlFor={name} className="form-label">
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="form-control form-control-sm p-1"
|
className="form-control form-control-sm p-1"
|
||||||
style={{ minHeight: "33px", position: "relative" }}
|
style={{ minHeight: "38px", position: "relative" }}
|
||||||
>
|
>
|
||||||
<div className="d-flex flex-wrap align-items-center gap-1">
|
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => (
|
||||||
@ -105,9 +105,6 @@ const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = fals
|
|||||||
outline: "none",
|
outline: "none",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: "120px",
|
minWidth: "120px",
|
||||||
backgroundColor: "white",
|
|
||||||
color: "black"
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const GalleryFilterPanel = ({ onApply }) => {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Label>Select Date:</Label>
|
<Label>Select Date:</Label>
|
||||||
<DateRangePicker1 className="w-100"
|
<DateRangePicker1
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
startField="startDate"
|
startField="startDate"
|
||||||
endField="endDate"
|
endField="endDate"
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
import { useForm, Controller,FormProvider } from "react-hook-form";
|
|
||||||
|
|
||||||
export const useAppForm = (config) => useForm(config);
|
|
||||||
export const AppFormProvider = FormProvider;
|
|
||||||
export const AppFormController = Controller;
|
|
||||||
|
|
||||||
@ -296,26 +296,6 @@ export const useOrganizationType = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useJobStatus=(statusId,projectId)=>{
|
|
||||||
return useQuery({
|
|
||||||
queryKey:["Job_Staus",statusId,projectId],
|
|
||||||
queryFn:async()=>{
|
|
||||||
const resp = await MasterRespository.getJobStatus(statusId,projectId);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
enabled:!!statusId && !!projectId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTeamRole=()=>{
|
|
||||||
return useQuery({
|
|
||||||
queryKey:["Team_Role"],
|
|
||||||
queryFn:async()=>{
|
|
||||||
const resp = await MasterRespository.getTeamRole();
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
//#region ==Get Masters==
|
//#region ==Get Masters==
|
||||||
const fetchMasterData = async (masterType) => {
|
const fetchMasterData = async (masterType) => {
|
||||||
switch (masterType) {
|
switch (masterType) {
|
||||||
@ -405,8 +385,6 @@ const useMaster = () => {
|
|||||||
export default useMaster;
|
export default useMaster;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ================================Mutation====================================
|
// ================================Mutation====================================
|
||||||
|
|
||||||
//#region Job Role
|
//#region Job Role
|
||||||
@ -457,9 +435,6 @@ export const useCreateJobRole = (onSuccessCallback) => {
|
|||||||
//#endregion Job Role
|
//#endregion Job Role
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Application Role
|
//#region Application Role
|
||||||
export const useCreateApplicationRole = (onSuccessCallback) => {
|
export const useCreateApplicationRole = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -505,10 +480,6 @@ export const useUpdateApplicationRole = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Create work Category
|
//#region Create work Category
|
||||||
export const useCreateWorkCategory = (onSuccessCallback) => {
|
export const useCreateWorkCategory = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -554,11 +525,6 @@ export const useUpdateWorkCategory = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Contact Category
|
//#region Contact Category
|
||||||
|
|
||||||
export const useCreateContactCategory = (onSuccessCallback) => {
|
export const useCreateContactCategory = (onSuccessCallback) => {
|
||||||
@ -609,10 +575,6 @@ export const useUpdateContactCategory = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Contact Tag
|
//#region Contact Tag
|
||||||
|
|
||||||
export const useCreateContactTag = (onSuccessCallback) => {
|
export const useCreateContactTag = (onSuccessCallback) => {
|
||||||
@ -659,10 +621,6 @@ export const useUpdateContactTag = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Expense Category
|
//#region Expense Category
|
||||||
export const useCreateExpenseCategory = (onSuccessCallback) => {
|
export const useCreateExpenseCategory = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -708,11 +666,6 @@ export const useUpdateExpenseCategory = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Payment Mode
|
//#region Payment Mode
|
||||||
|
|
||||||
export const useCreatePaymentMode = (onSuccessCallback) => {
|
export const useCreatePaymentMode = (onSuccessCallback) => {
|
||||||
@ -759,10 +712,6 @@ export const useUpdatePaymentMode = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Services-------------------------------
|
// Services-------------------------------
|
||||||
|
|
||||||
// export const useCreateService = (onSuccessCallback) => {
|
// export const useCreateService = (onSuccessCallback) => {
|
||||||
@ -844,10 +793,6 @@ export const useUpdateService = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Activity Grouph
|
//#region Activity Grouph
|
||||||
|
|
||||||
export const useCreateActivityGroup = (onSuccessCallback) => {
|
export const useCreateActivityGroup = (onSuccessCallback) => {
|
||||||
@ -912,10 +857,6 @@ export const useUpdateActivityGroup = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Activities
|
//#region Activities
|
||||||
export const useCreateActivity = (onSuccessCallback) => {
|
export const useCreateActivity = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -970,11 +911,6 @@ export const useUpdateActivity = (onSuccessCallback) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Expense Status
|
//#region Expense Status
|
||||||
export const useCreateExpenseStatus = (onSuccessCallback) => {
|
export const useCreateExpenseStatus = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -1018,10 +954,6 @@ export const useUpdateExpenseStatus = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Document-Category
|
//#region Document-Category
|
||||||
export const useCreateDocumentCatgory = (onSuccessCallback) => {
|
export const useCreateDocumentCatgory = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -1068,11 +1000,6 @@ export const useUpdateDocumentCategory = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Document-Type
|
//#region Document-Type
|
||||||
export const useCreateDocumentType = (onSuccessCallback) => {
|
export const useCreateDocumentType = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -1117,10 +1044,6 @@ export const useUpdateDocumentType = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region Payment Adjustment Head
|
//#region Payment Adjustment Head
|
||||||
export const useCreatePaymentAjustmentHead = (onSuccessCallback) => {
|
export const useCreatePaymentAjustmentHead = (onSuccessCallback) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -1164,10 +1087,6 @@ export const useUpdatePaymentAjustmentHead = (onSuccessCallback) => {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//#region ==Delete Master==
|
//#region ==Delete Master==
|
||||||
export const useDeleteMasterItem = () => {
|
export const useDeleteMasterItem = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -1203,8 +1122,6 @@ export const useDeleteMasterItem = () => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useDeleteServiceGroup = () => {
|
export const useDeleteServiceGroup = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@ -25,24 +25,16 @@ import showToast from "../services/toastService";
|
|||||||
|
|
||||||
export const useModal = (modalType) => {
|
export const useModal = (modalType) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isOpen = useSelector(
|
||||||
const modalState = useSelector(
|
(state) => state.localVariables.modals[modalType]?.isOpen
|
||||||
(state) => state.localVariables.modals[modalType] || {}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isOpen = false, data = null } = modalState;
|
const onOpen = (data = {}) => dispatch(openModal({ modalType, data }));
|
||||||
|
|
||||||
const onOpen = (payload = {}) =>
|
|
||||||
dispatch(openModal({ modalType, data: payload }));
|
|
||||||
|
|
||||||
const onClose = () => dispatch(closeModal({ modalType }));
|
const onClose = () => dispatch(closeModal({ modalType }));
|
||||||
|
|
||||||
const onToggle = () => dispatch(toggleModal({ modalType }));
|
const onToggle = () => dispatch(toggleModal({ modalType }));
|
||||||
|
|
||||||
return { isOpen, data, onOpen, onClose, onToggle };
|
return { isOpen, onOpen, onClose, onToggle };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useSubscription = (frequency) => {
|
export const useSubscription = (frequency) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["subscriptionPlans", frequency],
|
queryKey: ["subscriptionPlans", frequency],
|
||||||
|
|||||||
@ -2,80 +2,39 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { CollectionRepository } from "../repositories/ColllectionRepository";
|
import { CollectionRepository } from "../repositories/ColllectionRepository";
|
||||||
import showToast from "../services/toastService";
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
// export const useCollections = (
|
|
||||||
// pageSize,
|
|
||||||
// pageNumber,
|
|
||||||
// fromDate,
|
|
||||||
// toDate,
|
|
||||||
// isPending,
|
|
||||||
// isActive,
|
|
||||||
// projectId,
|
|
||||||
// searchString
|
|
||||||
// ) => {
|
|
||||||
// return useQuery({
|
|
||||||
// queryKey: [
|
|
||||||
// "collections",
|
|
||||||
// pageSize,
|
|
||||||
// pageNumber,
|
|
||||||
// fromDate,
|
|
||||||
// toDate,
|
|
||||||
// isPending,
|
|
||||||
// isActive,
|
|
||||||
// projectId,
|
|
||||||
// searchString,
|
|
||||||
// ],
|
|
||||||
|
|
||||||
// queryFn: async () => {
|
|
||||||
// const response = await CollectionRepository.getCollections(
|
|
||||||
// pageSize,
|
|
||||||
// pageNumber,
|
|
||||||
// fromDate,
|
|
||||||
// toDate,
|
|
||||||
// isPending,
|
|
||||||
// isActive,
|
|
||||||
// projectId,
|
|
||||||
// searchString
|
|
||||||
// );
|
|
||||||
// return response.data;
|
|
||||||
// },
|
|
||||||
|
|
||||||
// keepPreviousData: true,
|
|
||||||
// staleTime: 1000 * 60 * 1,
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
export const useCollections = (
|
export const useCollections = (
|
||||||
projectId,
|
|
||||||
searchString,
|
|
||||||
fromDate,
|
|
||||||
toDate,
|
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
isPending,
|
||||||
isActive,
|
isActive,
|
||||||
isPending
|
projectId,
|
||||||
|
searchString
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
"collections",
|
"collections",
|
||||||
projectId,
|
|
||||||
searchString,
|
|
||||||
fromDate,
|
|
||||||
toDate,
|
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
isActive,
|
fromDate,
|
||||||
|
toDate,
|
||||||
isPending,
|
isPending,
|
||||||
|
isActive,
|
||||||
|
projectId,
|
||||||
|
searchString,
|
||||||
],
|
],
|
||||||
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await CollectionRepository.getCollections(
|
const response = await CollectionRepository.getCollections(
|
||||||
projectId,
|
|
||||||
searchString,
|
|
||||||
fromDate,
|
|
||||||
toDate,
|
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
isPending,
|
||||||
isActive,
|
isActive,
|
||||||
isPending
|
projectId,
|
||||||
|
searchString
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@ -85,14 +44,15 @@ export const useCollections = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCollection = (collectionId) => {
|
|
||||||
|
export const useCollection =(collectionId)=>{
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["collection", collectionId],
|
queryKey:["collection",collectionId],
|
||||||
queryFn: async () => {
|
queryFn:async()=> {
|
||||||
const resp = await CollectionRepository.getCollection(collectionId);
|
const resp = await CollectionRepository.getCollection(collectionId);
|
||||||
return resp.data
|
return resp.data
|
||||||
},
|
},
|
||||||
enabled: !!collectionId
|
enabled:!!collectionId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,11 +133,11 @@ export const useAddComment = (onSuccessCallBack) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateCollection = (onSuccessCallBack) => {
|
export const useUpdateCollection =(onSuccessCallBack)=>{
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ collectionId, payload }) => await CollectionRepository.updateCollection(collectionId, payload),
|
mutationFn:async({collectionId,payload})=> await CollectionRepository.updateCollection(collectionId,payload),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
client.invalidateQueries({ queryKey: ["collections"] });
|
client.invalidateQueries({ queryKey: ["collections"] });
|
||||||
client.invalidateQueries({ queryKey: ["collection"] });
|
client.invalidateQueries({ queryKey: ["collection"] });
|
||||||
showToast("Collection Updated Successfully", "success");
|
showToast("Collection Updated Successfully", "success");
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
|||||||
import GlobalRepository from "../repositories/GlobalRepository";
|
import GlobalRepository from "../repositories/GlobalRepository";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { 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([]);
|
||||||
const [isLineChartLoading, setLoading] = useState(false);
|
const [isLineChartLoading, setLoading] = useState(false);
|
||||||
@ -17,13 +18,11 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
days,
|
days,
|
||||||
FromDate: FromDate || "",
|
FromDate: FromDate || '',
|
||||||
projectId: projectId || null,
|
projectId: projectId || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await GlobalRepository.getDashboardProgressionData(
|
const response = await GlobalRepository.getDashboardProgressionData(payload);
|
||||||
payload
|
|
||||||
);
|
|
||||||
setDashboard_Data(response.data);
|
setDashboard_Data(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Failed to fetch dashboard data.");
|
setError("Failed to fetch dashboard data.");
|
||||||
@ -39,6 +38,123 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
|||||||
return { dashboard_data, loading: isLineChartLoading, error };
|
return { dashboard_data, loading: isLineChartLoading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// export const useDashboard_AttendanceData = (date, projectId) => {
|
||||||
|
// const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
|
||||||
|
// const [isLineChartLoading, setLoading] = useState(false);
|
||||||
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const fetchData = async () => {
|
||||||
|
// setLoading(true);
|
||||||
|
// setError("");
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
|
||||||
|
// setDashboard_AttendanceData(response.data);
|
||||||
|
// } catch (err) {
|
||||||
|
// setError("Failed to fetch dashboard data.");
|
||||||
|
// console.error(err);
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (date && projectId !== null) {
|
||||||
|
// fetchData();
|
||||||
|
// }
|
||||||
|
// }, [date, projectId]);
|
||||||
|
|
||||||
|
// return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
// 🔹 Dashboard Projects Card Data Hook
|
||||||
|
// export const useDashboardProjectsCardData = () => {
|
||||||
|
// const [projectsCardData, setProjectsData] = useState([]);
|
||||||
|
// const [loading, setLoading] = useState(false);
|
||||||
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const fetchProjectsData = async () => {
|
||||||
|
// setLoading(true);
|
||||||
|
// setError("");
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const response = await GlobalRepository.getDashboardProjectsCardData();
|
||||||
|
// setProjectsData(response.data);
|
||||||
|
// } catch (err) {
|
||||||
|
// setError("Failed to fetch projects card data.");
|
||||||
|
// console.error(err);
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// fetchProjectsData();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// return { projectsCardData, loading, error };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 🔹 Dashboard Teams Card Data Hook
|
||||||
|
// export const useDashboardTeamsCardData = (projectId) => {
|
||||||
|
// const [teamsCardData, setTeamsData] = useState({});
|
||||||
|
// const [loading, setLoading] = useState(false);
|
||||||
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const fetchTeamsData = async () => {
|
||||||
|
// setLoading(true);
|
||||||
|
// setError("");
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
|
||||||
|
// setTeamsData(response.data || {});
|
||||||
|
// } catch (err) {
|
||||||
|
// setError("Failed to fetch teams card data.");
|
||||||
|
// console.error("Error fetching teams card data:", err);
|
||||||
|
// setTeamsData({});
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// fetchTeamsData();
|
||||||
|
// }, [projectId]);
|
||||||
|
|
||||||
|
// return { teamsCardData, loading, error };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export const useDashboardTasksCardData = (projectId) => {
|
||||||
|
// const [tasksCardData, setTasksData] = useState({});
|
||||||
|
// const [loading, setLoading] = useState(false);
|
||||||
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const fetchTasksData = async () => {
|
||||||
|
// setLoading(true);
|
||||||
|
// setError("");
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const response = await GlobalRepository.getDashboardTasksCardData(projectId);
|
||||||
|
// setTasksData(response.data);
|
||||||
|
// } catch (err) {
|
||||||
|
// setError("Failed to fetch tasks card data.");
|
||||||
|
// console.error(err);
|
||||||
|
// setTasksData({});
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// fetchTasksData();
|
||||||
|
// }, [projectId]);
|
||||||
|
|
||||||
|
// return { tasksCardData, loading, error };
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
export const useAttendanceOverviewData = (projectId, days) => {
|
export const useAttendanceOverviewData = (projectId, days) => {
|
||||||
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
|
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -51,10 +167,7 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await GlobalRepository.getAttendanceOverview(
|
const response = await GlobalRepository.getAttendanceOverview(projectId, days);
|
||||||
projectId,
|
|
||||||
days
|
|
||||||
);
|
|
||||||
setAttendanceOverviewData(response.data);
|
setAttendanceOverviewData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Failed to fetch attendance overview data.");
|
setError("Failed to fetch attendance overview data.");
|
||||||
@ -69,6 +182,7 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
return { attendanceOverviewData, loading, error };
|
return { attendanceOverviewData, loading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// -------------------Query----------------------------
|
// -------------------Query----------------------------
|
||||||
|
|
||||||
// export const useDashboard_Data = (days, FromDate, projectId)=>{
|
// export const useDashboard_Data = (days, FromDate, projectId)=>{
|
||||||
@ -85,47 +199,39 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
export const useProjectCompletionStatus = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["projectCompletionStatus"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await await GlobalRepository.getProjectCompletionStatus();
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useDashboard_AttendanceData = (date, projectId) => {
|
export const useDashboard_AttendanceData = (date, projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardAttendances", date, projectId],
|
queryKey: ["dashboardAttendances", date, projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await await GlobalRepository.getDashboardAttendanceData(
|
|
||||||
date,
|
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
|
||||||
projectId
|
|
||||||
);
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useDashboardTeamsCardData = (projectId) => {
|
export const useDashboardTeamsCardData = (projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardTeams", projectId],
|
queryKey: ["dashboardTeams", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId);
|
|
||||||
|
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useDashboardTasksCardData = (projectId) => {
|
export const useDashboardTasksCardData = (projectId) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardTasks", projectId],
|
queryKey: ["dashboardTasks", projectId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getDashboardTasksCardData(projectId);
|
|
||||||
|
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
// export const useAttendanceOverviewData = (projectId, days) => {
|
// export const useAttendanceOverviewData = (projectId, days) => {
|
||||||
// return useQuery({
|
// return useQuery({
|
||||||
// queryKey:["dashboardAttendanceOverView",projectId],
|
// queryKey:["dashboardAttendanceOverView",projectId],
|
||||||
@ -141,30 +247,29 @@ export const useDashboardProjectsCardData = () => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["dashboardProjects"],
|
queryKey: ["dashboardProjects"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|
||||||
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
|
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
|
||||||
const hasBothDates = !!startDate && !!endDate;
|
const hasBothDates = !!startDate && !!endDate;
|
||||||
const noDatesSelected = !startDate && !endDate;
|
const noDatesSelected = !startDate && !endDate;
|
||||||
|
|
||||||
const shouldFetch = noDatesSelected || hasBothDates;
|
const shouldFetch =
|
||||||
|
noDatesSelected ||
|
||||||
|
hasBothDates;
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
|
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getExpenseData(
|
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
|
||||||
projectId,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
);
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
enabled: shouldFetch,
|
enabled: shouldFetch,
|
||||||
refetchOnWindowFocus: true, // refetch when you come back
|
refetchOnWindowFocus: true, // refetch when you come back
|
||||||
refetchOnMount: "always", // always refetch on remount
|
refetchOnMount: "always", // always refetch on remount
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -175,20 +280,17 @@ export const useExpenseStatus = (projectId) => {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getExpenseStatus(projectId);
|
const resp = await GlobalRepository.getExpenseStatus(projectId);
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
export const useExpenseDataByProject = (projectId, categoryId, months) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["expenseByProject", projectId, categoryId, months],
|
queryKey: ["expenseByProject", projectId, categoryId, months],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const resp = await GlobalRepository.getExpenseDataByProject(
|
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
|
||||||
projectId,
|
|
||||||
categoryId,
|
|
||||||
months
|
|
||||||
);
|
|
||||||
return resp.data;
|
return resp.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState,useCallback } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { cacheData, getCachedData } from "../slices/apiDataManager";
|
import { cacheData, getCachedData } from "../slices/apiDataManager";
|
||||||
import { RolesRepository } from "../repositories/MastersRepository";
|
import { RolesRepository } from "../repositories/MastersRepository";
|
||||||
import EmployeeRepository from "../repositories/EmployeeRepository";
|
import EmployeeRepository from "../repositories/EmployeeRepository";
|
||||||
@ -9,36 +9,6 @@ import { useSelector } from "react-redux";
|
|||||||
import { store } from "../store/store";
|
import { store } from "../store/store";
|
||||||
import { queryClient } from "../layouts/AuthLayout";
|
import { queryClient } from "../layouts/AuthLayout";
|
||||||
|
|
||||||
|
|
||||||
export const useUserCache = () => {
|
|
||||||
const [userCache, setUserCache] = useState({});
|
|
||||||
|
|
||||||
const addToCache = (id, user) => {
|
|
||||||
setUserCache((prev) => ({ ...prev, [id]: user }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUser = async (id) => {
|
|
||||||
if (userCache[id]) return userCache[id];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await EmployeeRepository.getEmployeeProfile(id);
|
|
||||||
if (res?.data) {
|
|
||||||
addToCache(id, res.data);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("User not found", id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
userCache,
|
|
||||||
getUser,
|
|
||||||
addToCache,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Query ---------------------------------------------------------------------------
|
// Query ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const useEmployee = (employeeId) => {
|
export const useEmployee = (employeeId) => {
|
||||||
@ -231,7 +201,7 @@ export const useEmployeesName = (projectId, search, allEmployee) => {
|
|||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await EmployeeRepository.getEmployeeName(projectId, search, allEmployee),
|
await EmployeeRepository.getEmployeeName(projectId, search, allEmployee),
|
||||||
|
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000, // Optional: cache for 5 minutes
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -371,41 +341,3 @@ export const useUpdateEmployeeRoles = ({
|
|||||||
error: mutation.error,
|
error: mutation.error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useOrganizationHierarchy = (employeeId) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["organizationHierarchy", employeeId],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await EmployeeRepository.getOrganizaionHierarchy(employeeId);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
enabled: !!employeeId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useManageEmployeeHierarchy = (employeeId, onSuccessCallBack) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (payload) => {
|
|
||||||
return await EmployeeRepository.manageOrganizationHierarchy(
|
|
||||||
employeeId,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["organizationHierarchy", employeeId],
|
|
||||||
});
|
|
||||||
showToast("Reporting hierarchy updated successfully", "success");
|
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Something went wrong, please try again!",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -438,15 +438,6 @@ export const useExpenseTransactions = (employeeId)=>{
|
|||||||
keepPreviousData:true,
|
keepPreviousData:true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const useExpenseAllTransactionsList = (searchString) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["transaction", searchString],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await ExpenseRepository.getAllTranctionList(searchString);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// ---------------------------Put Post Recurring Expense---------------------------------------
|
// ---------------------------Put Post Recurring Expense---------------------------------------
|
||||||
|
|||||||
@ -20,17 +20,13 @@ export const useCurrentService = () => {
|
|||||||
|
|
||||||
// ------------------------------Query-------------------
|
// ------------------------------Query-------------------
|
||||||
|
|
||||||
export const useProjects = (pageSize, pageNumber,searchString) => {
|
export const useProjects = () => {
|
||||||
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["ProjectsList", pageSize, pageNumber,searchString],
|
queryKey: ["ProjectsList"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await ProjectRepository.getProjectList(
|
const response = await ProjectRepository.getProjectList();
|
||||||
pageSize,
|
return response.data;
|
||||||
pageNumber,
|
|
||||||
searchString,
|
|
||||||
);
|
|
||||||
return response?.data;
|
|
||||||
},
|
},
|
||||||
enabled: !!loggedUser,
|
enabled: !!loggedUser,
|
||||||
});
|
});
|
||||||
@ -157,7 +153,7 @@ export const useProjectsAllocationByEmployee = (employeeId) => {
|
|||||||
return { projectList, loading: isLoading, error, refetch };
|
return { projectList, loading: isLoading, error, refetch };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectName = (provideAll = false) => {
|
export const useProjectName = (provideAll=false) => {
|
||||||
const {
|
const {
|
||||||
data = [],
|
data = [],
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -165,7 +161,7 @@ export const useProjectName = (provideAll = false) => {
|
|||||||
refetch,
|
refetch,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["basicProjectNameList", provideAll],
|
queryKey: ["basicProjectNameList",provideAll],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await ProjectRepository.projectNameList(provideAll);
|
const res = await ProjectRepository.projectNameList(provideAll);
|
||||||
return res.data || res;
|
return res.data || res;
|
||||||
@ -183,19 +179,6 @@ export const useProjectName = (provideAll = false) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectBothName = (searchString) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["basic_bothProject", searchString],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await ProjectRepository.projectNameListAll(searchString);
|
|
||||||
return res.data || res;
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(error.message || "Error while Fetching project Name", "error");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useProjectInfra = (projectId, serviceId) => {
|
export const useProjectInfra = (projectId, serviceId) => {
|
||||||
const {
|
const {
|
||||||
data: projectInfra,
|
data: projectInfra,
|
||||||
@ -354,7 +337,7 @@ export const useEmployeeForTaskAssign = (
|
|||||||
|
|
||||||
// -- -------------Mutation-------------------------------
|
// -- -------------Mutation-------------------------------
|
||||||
|
|
||||||
export const useCreateProject = (onSuccessCallback) => {
|
export const useCreateProject = ( onSuccessCallback ) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@ -385,7 +368,7 @@ export const useCreateProject = (onSuccessCallback) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateProject = (onSuccessCallback) => {
|
export const useUpdateProject = ( onSuccessCallback ) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isPending, isSuccess, isError } = useMutation({
|
const { mutate, isPending, isSuccess, isError } = useMutation({
|
||||||
mutationFn: async ({ projectId, payload }) => {
|
mutationFn: async ({ projectId, payload }) => {
|
||||||
@ -412,6 +395,7 @@ export const useUpdateProject = (onSuccessCallback) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
console.log(error);
|
||||||
showToast(error?.message || "Error while updating project", "error");
|
showToast(error?.message || "Error while updating project", "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,423 +0,0 @@
|
|||||||
import {
|
|
||||||
useInfiniteQuery,
|
|
||||||
useMutation,
|
|
||||||
useQuery,
|
|
||||||
useQueryClient,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import { ServiceProjectRepository } from "../repositories/ServiceProjectRepository";
|
|
||||||
import showToast from "../services/toastService";
|
|
||||||
|
|
||||||
//#region Service Project
|
|
||||||
export const useServiceProjects = (pageSize, pageNumber, searchString) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await ServiceProjectRepository.GetServiceProjects(
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
searchString
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useServiceProject = (projectId) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["serviceProject", projectId],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await ServiceProjectRepository.GetServiceProject(
|
|
||||||
projectId
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
enabled: !!projectId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useServiceProjectTeam = (projectId, isActive) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["serviceProjectTeam", projectId, isActive],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await ServiceProjectRepository.GetAllocatedEmployees(
|
|
||||||
projectId,
|
|
||||||
isActive
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
enabled: !!projectId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useCreateServiceProject = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (playload) =>
|
|
||||||
await ServiceProjectRepository.CreateServiceProject(playload),
|
|
||||||
onSuccess: (data, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
showToast("Project created successfully", "success");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to delete task",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useUpdateServiceProject = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({ id, payload }) => {
|
|
||||||
return await ServiceProjectRepository.UpdateServiceProject(id, payload);
|
|
||||||
},
|
|
||||||
onSuccess: (data, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["serviceProject", variables.id],
|
|
||||||
});
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
showToast("Project updated successfully", "success");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to update project",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useActiveInActiveServiceProject = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (id, status) => {
|
|
||||||
return await ServiceProjectRepository.DeleteServiceProject(id, status);
|
|
||||||
},
|
|
||||||
onSuccess: (data, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
showToast(
|
|
||||||
`Project ${status ? "restored" : "deleted"} successfully`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to update project",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAllocationServiceProjectTeam = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({ payload, isActive }) =>
|
|
||||||
await ServiceProjectRepository.AllocateEmployee(payload),
|
|
||||||
onSuccess: (data, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjectTeam"] });
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
if (variables.isActive) {
|
|
||||||
showToast(
|
|
||||||
`${variables?.payload.length} Employee allocated successfully`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showToast(`Employee removed successfully`, "success");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to update project",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Service Jobs
|
|
||||||
|
|
||||||
export const useServiceProjectJobs = (
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
isActive = true,
|
|
||||||
project,
|
|
||||||
isArchive
|
|
||||||
) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: [
|
|
||||||
"serviceProjectJobs",
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
isActive,
|
|
||||||
project,
|
|
||||||
isArchive,
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await ServiceProjectRepository.GetJobList(
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
isActive,
|
|
||||||
project,
|
|
||||||
isArchive
|
|
||||||
);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useJobComments = (jobId, pageSize, pageNumber) => {
|
|
||||||
return useInfiniteQuery({
|
|
||||||
queryKey: ["jobComments", jobId, pageSize],
|
|
||||||
|
|
||||||
queryFn: async ({ pageParam = pageNumber }) => {
|
|
||||||
const resp = await ServiceProjectRepository.GetJobComment(
|
|
||||||
jobId,
|
|
||||||
pageSize,
|
|
||||||
pageParam
|
|
||||||
);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
enabled: !!jobId,
|
|
||||||
|
|
||||||
initialPageParam: pageNumber,
|
|
||||||
|
|
||||||
getNextPageParam: (lastPage, allPages) => {
|
|
||||||
if (!lastPage?.data?.length) return null;
|
|
||||||
|
|
||||||
return allPages.length + pageNumber;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useJobTags = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["Job_Tags"],
|
|
||||||
queryFn: async () => await ServiceProjectRepository.GetJobTags(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useServiceProjectJobDetails = (job) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["service-job", job],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await ServiceProjectRepository.GetJobDetails(job);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
enabled: !!job,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const useAddCommentJob = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (data) => await ServiceProjectRepository.AddComment(data),
|
|
||||||
onSuccess: (data, variables) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["jobComments", variables?.jobTicketId],
|
|
||||||
});
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
showToast("Comment added successfully", "success");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to update project",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCreateServiceProjectJob = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (payload) => {
|
|
||||||
return await ServiceProjectRepository.CreateJob(payload);
|
|
||||||
},
|
|
||||||
onSuccess: (data, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
|
|
||||||
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
showToast("Job Created successfully", "success");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to update project",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateServiceProjectJob = (onSuccessCallback) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({ id, payload, isArchiveAction = false }) => {
|
|
||||||
const resp = await ServiceProjectRepository.UpdateJob(id, payload);
|
|
||||||
|
|
||||||
return { resp, isArchiveAction };
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess: ({ isArchiveAction }) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["service-job"] });
|
|
||||||
|
|
||||||
if (onSuccessCallback) onSuccessCallback();
|
|
||||||
|
|
||||||
if (isArchiveAction) {
|
|
||||||
showToast("Job archived successfully", "success");
|
|
||||||
} else {
|
|
||||||
showToast("Job restored successfully", "success");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to update project",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Branch
|
|
||||||
export const useBranches = (
|
|
||||||
projectId,
|
|
||||||
isActive,
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
searchString
|
|
||||||
) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: [
|
|
||||||
"branches",
|
|
||||||
projectId,
|
|
||||||
isActive,
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
searchString,
|
|
||||||
],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await ServiceProjectRepository.GetBranchList(
|
|
||||||
projectId,
|
|
||||||
isActive,
|
|
||||||
pageSize,
|
|
||||||
pageNumber,
|
|
||||||
searchString
|
|
||||||
);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
enabled: !!projectId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBranchTypes = () => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["branch_Type"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await ServiceProjectRepository.GetBranchTypeList();
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBranchDetails = (id) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["branch", id],
|
|
||||||
queryFn: async () => {
|
|
||||||
const resp = await ServiceProjectRepository.GetBranchDetail(id);
|
|
||||||
return resp.data;
|
|
||||||
},
|
|
||||||
enabled: !!id,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCreateBranch = (onSuccessCallBack) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (payload) => {
|
|
||||||
await ServiceProjectRepository.CreateBranch(payload);
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["branches"] });
|
|
||||||
showToast("Branches Created Successfully", "success");
|
|
||||||
if (onSuccessCallBack) onSuccessCallBack();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error.message || "Something went wrong please try again !",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdateBranch = (onSuccessCallBack) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({ id, payload }) =>
|
|
||||||
await ServiceProjectRepository.UpdateBranch(id, payload),
|
|
||||||
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
// remove old single-branch cache
|
|
||||||
queryClient.removeQueries({ queryKey: ["branch", variables.id] });
|
|
||||||
|
|
||||||
// refresh list
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["branches"] });
|
|
||||||
|
|
||||||
showToast("Branch updated successfully", "success");
|
|
||||||
onSuccessCallBack?.();
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: () => {
|
|
||||||
showToast("Something went wrong. Please try again later.", "error");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeleteBranch = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async ({ id, isActive }) =>
|
|
||||||
await ServiceProjectRepository.DeleteBranch(id, isActive),
|
|
||||||
|
|
||||||
onSuccess: (_, variable) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["branches"] });
|
|
||||||
showToast(
|
|
||||||
`Branch ${variable.isActive ? "restored" : "deleted"} successfully`,
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(
|
|
||||||
error?.response?.data?.message ||
|
|
||||||
error.message ||
|
|
||||||
"Failed to delete branch",
|
|
||||||
"error"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user