Compare commits

..

82 Commits

Author SHA1 Message Date
96bcdffdca added optional chain for n=menu 2025-12-05 18:45:19 +05:30
f8963ef476 Merge pull request 'At Assigned Employee when selecting a Job role it change according to master now show correct data.' (#520) from Assigned_Employee_Job_Role into main
Reviewed-on: #520
merged
2025-11-26 12:20:28 +00:00
9ad3b8726c At Assigned Employee when selecting a Job role it change according to master now show correct data. 2025-11-25 18:09:32 +05:30
d4582c101a Merge pull request 'Project_Branch_Management : New Feature Service Project - Branch Management' (#519) from Project_Branch_Management into main
Reviewed-on: #519
merged
2025-11-25 09:42:47 +00:00
c6af020c85 changed fs-6 and body-font-size 2025-11-25 15:09:25 +05:30
822ff1a7e4 UI Alignment in Service Card view. 2025-11-25 11:20:19 +05:30
92d17167b1 added zoom in-out 2025-11-25 10:11:43 +05:30
8ec62827d5 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-24 17:58:26 +05:30
965fce1587 Merge pull request 'Service_Project_ListView :- Creating List view for service Project.' (#518) from Service_Project_ListView into Project_Branch_Management
Reviewed-on: #518
Merged
2025-11-24 11:16:57 +00:00
bb37fd7044 set align for Service Project List card 2025-11-24 11:16:57 +00:00
03d60bf66d Adding search functionality in Projectpage. 2025-11-24 11:16:57 +00:00
071e5956a7 Changes. 2025-11-24 11:16:57 +00:00
c669eb90c3 Correction in List view alignment. 2025-11-24 11:16:57 +00:00
4d2e37f52e Adding Message for Card and list view in Services and Infra 2025-11-24 11:16:57 +00:00
6dde240926 Creating a list view for Services. 2025-11-24 11:16:57 +00:00
482b5a1680 Adding lIst view. 2025-11-24 11:16:57 +00:00
098090cf69 Merge pull request 'Status Dropdown Should Auto-Close After Job Status Update' (#516) from Kartik_Bug#1773 into Project_Branch_Management
Reviewed-on: #516
mergd
2025-11-24 11:05:51 +00:00
5747d4ec71 Status Dropdown Should Auto-Close After Job Status Update 2025-11-24 10:35:03 +05:30
48f314eac4 added signalR for project Branch 2025-11-21 15:28:31 +05:30
18e739f3ab fixed collection header ui 2025-11-21 14:51:31 +05:30
a5329f1a2a added new selectproject field inside ManageRecurring 2025-11-21 14:25:32 +05:30
8b4a9d2d1c added new project filed inside ManagePaymentRequest 2025-11-21 14:08:52 +05:30
4506f740eb Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 13:13:50 +05:30
f001dff5b0 Creating new icon for Un-archive 2025-11-21 13:09:14 +05:30
33d94f6f06 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 12:58:52 +05:30
0ac1141579 changed filed for project and employee now not need project for select employee 2025-11-21 12:58:47 +05:30
4d8af5da91 added pre-fill updating 2025-11-21 12:57:54 +05:30
fead3a37a6 Adding Pending in Loading in Job list. 2025-11-21 12:31:10 +05:30
98e9a8b625 When we in Archive tab then canvas will be open now it work correctly. 2025-11-21 12:21:22 +05:30
2781da8906 Change the logic according to archieve show by status. 2025-11-21 12:04:36 +05:30
d13c48dd39 Change the Ui for Archieve jobs. 2025-11-21 11:47:55 +05:30
ee4ef18594 Implementing Restore Functionality in Jobs. 2025-11-21 11:39:36 +05:30
742337a3d0 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 11:12:03 +05:30
5abab06b0c removed console error 2025-11-21 11:11:44 +05:30
17a9f4a9b1 Implementing Project change 2025-11-21 11:10:55 +05:30
de088fcfc2 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-21 09:46:32 +05:30
8e12eb6797 Adding Switch button on Jobs. 2025-11-21 09:45:28 +05:30
14c1da7888 added restore 2025-11-20 22:01:25 +05:30
753d8c4a2c Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-20 17:09:09 +05:30
766d19744c added suggestion for branch Type inside service Projects 2025-11-20 17:09:04 +05:30
caaadc4c08 Implementing Validation when we status will be done or closed then Archieve button will be shown. 2025-11-20 16:38:25 +05:30
1531fbe6f2 Implementing Archieve functionality in Jobs. 2025-11-20 16:16:20 +05:30
9f5a167613 added copy button for localtion url 2025-11-20 14:49:54 +05:30
fa923d4c3a added popup for branch view 2025-11-20 14:39:54 +05:30
bd43475d12 Merge pull request 'Advance_Payment_List :- Implementing advance Payment new API.' (#514) from Advance_Payment_List into Project_Branch_Management
Reviewed-on: #514
merged
2025-11-20 09:08:07 +00:00
521d46bdee Merge pull request 'Adding Billed To field in Manage Collection.' (#513) from Collection_Service_Project into Project_Branch_Management
Reviewed-on: #513
merged
2025-11-20 07:23:25 +00:00
604bb68dc2 Adding Billed To field in Manage Collection. 2025-11-20 07:23:25 +00:00
047e563505 added mutiple contact person inside branch 2025-11-20 12:49:56 +05:30
e4f053ee65 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-20 12:41:39 +05:30
7105a3640f Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-20 12:36:40 +05:30
d167c57ab0 Implementing the Update api for branches. 2025-11-20 12:34:45 +05:30
d3c006279c Increasing the size of Service details page. 2025-11-20 10:29:42 +05:30
195a0c83bb Correction in api for details. 2025-11-20 10:00:06 +05:30
8f86b05d35 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-20 09:51:29 +05:30
dd716187da Implementing edit api for branches. 2025-11-20 09:51:23 +05:30
5b86a2f64f removed console error 2025-11-20 09:50:49 +05:30
e99d49b83e added view comments 2025-11-20 09:26:02 +05:30
0210e17170 added branch details 2025-11-19 22:58:41 +05:30
e7fddd41c2 added branch for projects 2025-11-19 21:28:01 +05:30
7e4dffff34 Implementing Get api. 2025-11-19 18:47:30 +05:30
e7a68aeab7 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 18:32:03 +05:30
8a5d6158c2 Implementing Creating API. 2025-11-19 18:31:17 +05:30
50269aead2 added selectFiled for branch 2025-11-19 18:28:54 +05:30
0ede07b0d5 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 16:36:37 +05:30
df24b18a27 At the time of hit in cancel form will be closed. 2025-11-19 16:36:32 +05:30
f5d89f2bab Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 16:36:01 +05:30
15978b2ac7 fixed useClient implmention 2025-11-19 16:35:53 +05:30
2dbf5dc109 Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Project_Branch_Management 2025-11-19 16:31:12 +05:30
7cc8cc99c2 Creating a list view and modal for Branches. 2025-11-19 16:30:51 +05:30
c04ee8dab5 configure branch related api inside service repo. and made hooks for branch 2025-11-19 16:28:37 +05:30
c52f85ee4a added Advance payment flag inside paymentRequest list and view details 2025-11-19 15:44:54 +05:30
c718269dfd added spinner in service profile page 2025-11-19 15:35:39 +05:30
268bd2875f Changing the position of Status in Collection list view. 2025-11-19 14:26:12 +05:30
7773b7a43b Adding Status and UI implementation in Collection. 2025-11-19 12:59:57 +05:30
25003d912e add green color class for onfieldwork.com 2025-11-19 12:35:13 +05:30
a3be63b74f Implementing api for Advance payment all data. 2025-11-19 12:22:38 +05:30
54420c70d9 change landing page logo to marco from ON 2025-11-19 12:15:08 +05:30
4b5f8756b3 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-19 11:27:51 +05:30
10e54637d5 Adding list view in Advance payment list. 2025-11-19 10:47:11 +05:30
5b91c13b85 adding list view in advance payment list. 2025-11-19 09:46:41 +05:30
54d89e1429 Merge branch 'Service_Project_Managment' of https://git.marcoaiot.com/admin/marco.pms.web into Advance_Payment_List 2025-11-18 17:38:30 +05:30
9822ae91ec Adding AdvancePayment a list view. 2025-11-18 15:56:16 +05:30
70 changed files with 3208 additions and 1067 deletions

View File

@ -89,7 +89,7 @@
);
--bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 0.875rem;
--bs-body-font-size: 0.85rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.375;
--bs-body-color: #646e78;
@ -9060,7 +9060,7 @@ img[data-app-light-img][data-app-dark-img] {
}
.table th {
color: var(--bs-heading-color);
font-size: 0.8125rem;
font-size: 0.8025rem;
letter-spacing: 0.2px;
text-transform: uppercase;
}
@ -20345,7 +20345,7 @@ li:not(:first-child) .dropdown-item,
}
.fs-6 {
font-size: 0.9375rem !important;
font-size: 0.8375rem !important;
}
.fs-tiny {
@ -32560,9 +32560,7 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
.bg-blue {
background-color:var(--bs-blue)
}
.text-blue{
color:var(--bs-blue)
}
.bg-indigo {
background-color:var(--bs-indigo)
}
@ -32575,3 +32573,9 @@ body:not(.modal-open) .layout-content-navbar .layout-navbar {
.text-red{
color:var(--bs-red)
}
.text-blue{
color:var(--bs-blue)
}
.text-green{
color:var(--bs-green)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -1,6 +1,6 @@
import React, { useEffect, useMemo } from "react";
import { useExpenseTransactions } from "../../hooks/useExpense";
import { useExpenseAllTransactionsList, useExpenseTransactions } from "../../hooks/useExpense";
import Error from "../common/Error";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Loader, { SpinnerLoader } from "../common/Loader";
@ -11,11 +11,10 @@ import { employee } from "../../data/masters";
import { useAdvancePaymentContext } from "../../pages/AdvancePayment/AdvancePaymentPage";
import { formatFigure } from "../../utils/appUtils";
const AdvancePaymentList = ({ employeeId }) => {
const AdvancePaymentList = ({ employeeId, searchString }) => {
const { setBalance } = useAdvancePaymentContext();
const { data, isError, isLoading, error, isFetching } =
useExpenseTransactions(employeeId, { enabled: !!employeeId });
const records = Array.isArray(data) ? data : [];
let currentBalance = 0;

View File

@ -0,0 +1,100 @@
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;

View File

@ -2,15 +2,19 @@ import React, { useState } from "react";
import HorizontalBarChart from "../Charts/HorizontalBarChart";
import { useProjects } from "../../hooks/useProjects";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import { useProjectCompletionStatus } from "../../hooks/useDashboard_Data";
const ProjectCompletionChart = () => {
const [currentPage, setCurrentPage] = useState(1);
const { data: projects, isLoading: loading, isError, error } = useProjects(50,currentPage);
// Bar chart logic
const projectNames = projects?.data?.map((p) => p.name) || [];
const {
data: projects,
isLoading: loading,
isError,
error,
} = useProjectCompletionStatus();
const projectNames = projects?.map((p) => p.name) || [];
const projectProgress =
projects?.data?.map((p) => {
projects?.map((p) => {
const completed = p.completedWork || 0;
const planned = p.plannedWork || 1;
const percent = planned ? (completed / planned) * 100 : 0;

View File

@ -32,6 +32,11 @@ import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist";
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 {
@ -40,6 +45,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
error: ExpenseErrorLoad,
} = useExpense(expenseToEdit);
const [expenseCategory, setExpenseCategory] = useState();
const [selectedEmployees, setSelectedEmployees] = useState([]);
const dispatch = useDispatch();
const {
expenseCategories,
@ -83,11 +89,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
loading: StatusLoadding,
error: stausError,
} = useExpenseStatus();
const {
data: employees,
isLoading: EmpLoading,
isError: isEmployeeError,
} = useEmployeesNameByProject(selectedproject);
// const {
// data: employees,
// isLoading: EmpLoading,
// isError: isEmployeeError,
// } = useEmployeesNameByProject(selectedproject);
const files = watch("billAttachments");
const onFileChange = async (e) => {
@ -150,6 +156,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
}
};
const { mutate: AllocationTeam, isPending1 } =
useAllocationServiceProjectTeam(() => {
setSelectedEmployees([]);
setSeletingEmp({
employee: null,
isOpen: false,
});
});
useEffect(() => {
if (expenseToEdit && data) {
reset({
@ -180,7 +194,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
: [],
});
}
}, [data, reset, employees]);
}, [data, reset]);
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
handleClose()
);
@ -223,7 +237,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</h5>
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2 text-start">
<div className="col-md-6">
{/* <div className="col-md-6">
<Label className="form-label" required>
Select Project
</Label>
@ -245,6 +259,23 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{errors.projectId && (
<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 className="col-md-6">
@ -307,14 +338,28 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>
{/* <Label className="form-label" required>
Paid By{" "}
</Label>
<EmployeeSearchInput
</Label> */}
{/* <EmployeeSearchInput
control={control}
name="paidById"
projectId={null}
forAll={expenseToEdit ? true : false}
forAll={true}
/> */}
<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>
@ -423,7 +468,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.gstNumber.message}</small>
)}
</div>
</div>
<div className="row">
<div className="col-md-6 text-start ">
@ -454,7 +498,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div>
{expenseCategory?.noOfPersonsRequired && (
<div className="col-md-6 text-start">
<Label className="form-label" required>No. of Persons</Label>
<Label className="form-label" required>
No. of Persons
</Label>
<input
type="number"
id="noOfPersons"

View File

@ -1,24 +1,48 @@
import { useState } from "react";
const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true);
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 (
<>
<div className="d-flex justify-content-start">
<div className="d-flex justify-content-start gap-3 mb-2">
<i
className="bx bx-rotate-right cursor-pointer"
className="bx bx-rotate-right cursor-pointer fs-4"
title="Rotate"
onClick={() => setRotation((prev) => prev + 90)}
></i>
<i
className="bx bx-zoom-in cursor-pointer fs-4"
title="Zoom In"
onClick={zoomIn}
></i>
<i
className="bx bx-zoom-out cursor-pointer fs-4"
title="Zoom Out"
onClick={zoomOut}
></i>
</div>
<div
className="position-relative d-flex flex-column justify-content-center align-items-center"
className="position-relative d-flex flex-column justify-content-center align-items-center overflow-hidden"
style={{ minHeight: "80vh" }}
>
{loading && (
<div className="text-secondary text-center mb-2">Loading...</div>
<div className="text-secondary text-center mb-2">
Loading...
</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center">
@ -30,18 +54,18 @@ const PreviewDocument = ({ imageUrl }) => {
maxHeight: "80vh",
objectFit: "contain",
display: loading ? "none" : "block",
transform: `rotate(${rotation}deg)`,
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 justify-content-center gap-2">
<div className="position-absolute bottom-0 start-0 m-2">
<button
className="btn btn-outline-secondary"
onClick={() => setRotation(0)}
title="Reset Rotation"
onClick={resetAll}
>
<i className="bx bx-reset"></i> Reset
</button>
@ -51,4 +75,6 @@ const PreviewDocument = ({ imageUrl }) => {
);
};
export default PreviewDocument;

View File

@ -50,8 +50,11 @@ const Header = () => {
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 || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage);
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage || isAdvancePayment1);
};
const allowedProjectStatusIds = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba",

View File

@ -25,22 +25,19 @@ const Sidebar = () => {
/>
</span> */}
<a
href="/"
className="app-brand-link fw-bold navbar-brand text-green fs-6"
>
<small className="app-brand-link fw-bold navbar-brand text-green fs-6">
<span className="app-brand-logo demo">
<img src="/img/brand/marco.png" width="50" />
</span>
<span className="text-blue">OnField</span>
<span>Work</span>
<span className="text-dark">.com</span>
</a>
</small>
</Link>
<a className="layout-menu-toggle menu-link text-large ms-auto">
<small 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>
</a>
</small>
</div>
<div className="menu-inner-shadow"></div>
@ -61,7 +58,7 @@ const Sidebar = () => {
</>
)}
{data &&
data?.data.map((section) => (
data?.data?.map((section) => (
<React.Fragment
key={section.id || section.header || section.items[0]?.id}
>

View File

@ -40,7 +40,6 @@ const ActionPaymentRequest = ({ requestId }) => {
error: PaymentModeError,
} = usePaymentMode();
console.log("Kartik", data)
const IsReview = useHasUserPermission(REVIEW_EXPENSE);
const [imageLoaded, setImageLoaded] = useState({});

View File

@ -29,6 +29,7 @@ import Filelist from "../Expenses/Filelist";
import InputSuggestions from "../common/InputSuggestion";
import { useProfile } from "../../hooks/useProfile";
import { blockUI } from "../../utils/blockUI";
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
const {
@ -234,10 +235,10 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
{/* Project and Category */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label className="form-label" required>
{/* <Label className="form-label" required>
Select Project
</Label>
<select
</Label> */}
{/* <select
className="form-select form-select-sm"
{...register("projectId")}
disabled={
@ -254,7 +255,23 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
</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 && (
<small className="danger-text">{errors.projectId.message}</small>
)}

View File

@ -85,7 +85,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
key: "paymentRequestUID",
label: "Request ID",
align: "text-start mx-2",
getValue: (e) => e.paymentRequestUID || "N/A",
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>,
},
{
key: "title",

View File

@ -148,7 +148,7 @@ const ViewPaymentRequest = ({ requestId }) => {
<div className="col-12 col-sm-6 ">
<div className="row ">
<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></div>
<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>
<span
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
}`}

View File

@ -82,7 +82,6 @@ const EditActivityModal = ({
useEffect(() => {
if (!workItem) return;
console.log(workItem)
reset({
activityID: String(
workItem?.workItem?.activityId || workItem?.activityMaster?.id

View File

@ -8,7 +8,12 @@ const ProjectCardView = ({ data, currentPage, totalPages, paginate }) => {
return (
<div className="row page-min-h">
{data?.length === 0 && (
<p className="text-center text-muted">No projects found.</p>
<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) => (

View File

@ -126,7 +126,7 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
return (
<div className="card page-min-h py-4 px-6 shadow-sm">
<div className="table-responsive text-nowrap page-min-h">
<div className="table-responsive text-nowrap">
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
@ -143,7 +143,8 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</tr>
</thead>
<tbody>
{data?.map((project) => (
{data?.length > 0 ? (
data?.map((project) => (
<tr key={project.id}>
{projectColumns.map((col) => (
<td
@ -158,8 +159,7 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</td>
))}
<td
className={`mx-2 ${
canManageProject ? "d-sm-table-cell" : "d-none"
className={`mx-2 ${canManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
<div className="dropdown z-2">
@ -213,7 +213,24 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</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>
</tr>
)}
</tbody>
</table>
</div>

View File

@ -10,11 +10,13 @@ import {
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
import showToast from "../../../services/toastService";
import { useOrganizationEmployees } from "../../../hooks/useOrganization";
import { useDispatch } from "react-redux";
import { changeMaster } from "../../../slices/localVariablesSlice";
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
const selectedProject = useSelectedProject();
const debounceSearchTerm = useDebounce(searchTerm, 500);
const dispatch = useDispatch();
const {
data: employeesData = [],
isLoading,
@ -45,6 +47,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
});
useEffect(() => {
dispatch(changeMaster("Job Role"));
if (employeesData?.data?.length > 0) {
const available = employeesData.data.filter((emp) => {
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
@ -119,7 +122,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
status: true,
}));
handleAssignEmployee({ payload,actionType:"assign"} );
handleAssignEmployee({ payload, actionType: "assign" });
setEmployees((prev) =>
prev.map((emp) => ({
@ -132,11 +135,11 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
);
};
if (isLoading) {
return ( <div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>) ;
}
if (isLoading) {
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 (
<div className="page-min-h d-flex justify-content-center align-items-center ">
{error?.status === 400 ? (
@ -144,14 +147,14 @@ if (isError) {
) : (
<p className="m-0 dange-text">Something went wrong. Please try again later.</p>
)}
</div>
</div>
);
}
}
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>) ;
}
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 (
@ -183,8 +186,7 @@ if (employees.length === 0) {
onChange={(e) =>
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>
@ -205,8 +207,7 @@ if (employees.length === 0) {
onChange={(e) =>
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>

View File

@ -27,6 +27,7 @@ import InputSuggestions from "../common/InputSuggestion";
import { useEmployeesName } from "../../hooks/useEmployees";
import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag";
import HoverPopup from "../common/HoverPopup";
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
const {
@ -131,7 +132,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
}
}, [currencyData, requestToEdit, setValue]);
const StrikeDate = watch("strikeDate")
const StrikeDate = watch("strikeDate");
const onSubmit = (fromdata) => {
let payload = {
@ -163,10 +164,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
{/* Project and Category */}
<div className="row my-2 text-start">
<div className="col-md-6">
<Label className="form-label" required>
Select Project
</Label>
<select
{/* <select
className="form-select form-select-sm"
{...register("projectId")}
>
@ -180,7 +178,19 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
</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 && (
<small className="danger-text">{errors.projectId.message}</small>
)}
@ -235,7 +245,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
</div>
<div className="col-md-6 mt-2">
<div className="d-flex justify-content-start align-items-center gap-2">
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
<Label htmlFor="isVariable" className="form-label mb-0" required>
Payment Type
</Label>
@ -243,13 +253,16 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
title="Payment Type"
id="payment_type"
content={
<div className=" w-50">
<p>
Choose whether the payment amount varies or remains fixed each cycle.
Choose whether the payment amount varies or remains fixed
each cycle.
<br />
<strong>Is Variable:</strong> Amount changes per cycle.
<br />
<strong>Fixed:</strong> Amount stays constant.
</p>
</div>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
@ -270,7 +283,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
checked={field.value === true}
onChange={() => field.onChange(true)}
/>
<Label htmlFor="isVariableTrue" className="form-check-label">
<Label
htmlFor="isVariableTrue"
className="form-check-label"
>
Is Variable
</Label>
</div>
@ -283,7 +299,10 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
checked={field.value === false}
onChange={() => field.onChange(false)}
/>
<Label htmlFor="isVariableFalse" className="form-check-label">
<Label
htmlFor="isVariableFalse"
className="form-check-label"
>
Fixed
</Label>
</div>
@ -295,7 +314,6 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
<small className="danger-text">{errors.isVariable.message}</small>
)}
</div>
</div>
{/* Date and Amount */}
@ -391,11 +409,12 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
id="frequency"
content={
<p>
Defines how often payments or billing occur, such as monthly, quarterly, or annually.
Defines how often payments or billing occur, such as
monthly, quarterly, or annually.
</p>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
</HoverPopup>
</div>
@ -444,10 +463,13 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
{/* Payment Buffer Days and End Date */}
<div className="row my-2 text-start">
<div className="col-md-6">
<div className="d-flex justify-content-start align-items-center gap-2">
<Label htmlFor="paymentBufferDays" className="form-label mb-0" required>
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
<Label
htmlFor="paymentBufferDays"
className="form-label mb-0 "
required
>
Payment Buffer Days
</Label>
<HoverPopup
@ -455,11 +477,12 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
id="payment_buffer_days"
content={
<p>
Number of extra days allowed after the due date before payment is considered late.
Number of extra days allowed after the due date before
payment is considered late.
</p>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
</HoverPopup>
</div>
@ -480,9 +503,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
)}
</div>
<div className="col-md-6">
<div className="d-flex justify-content-start align-items-center gap-2">
<div className="d-flex justify-content-start align-items-center text-nowrap gap-2">
<Label htmlFor="endDate" className="form-label mb-0" required>
End Date
</Label>
@ -495,7 +517,7 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
</p>
}
>
<i className="bx bx-info-circle bx-sm text-muted cursor-pointer"></i>
<i className="bx bx-info-circle bx-xs text-muted cursor-pointer"></i>
</HoverPopup>
</div>
@ -510,10 +532,8 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => {
<small className="danger-text">{errors.endDate.message}</small>
)}
</div>
</div>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="notifyTo" className="form-label" required>

View File

@ -166,7 +166,7 @@ const RecurringExpenseList = ({ search, filterStatuses }) => {
}
);
};
console.log("Tanish",filteredData)
return (
<>
{IsDeleteModalOpen && (

View File

@ -4,24 +4,49 @@ import {
getJobStatusBadge,
getNextBadgeColor,
} from "../../utils/appUtils";
import { useServiceProjectJobs } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants";
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 = () => {
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
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 = [
{
@ -35,12 +60,17 @@ const JobList = () => {
label: "Title",
getValue: (e) => (
<span
className="fw-semibold text-truncate d-inline-block cursor-pointer"
className={`fw-semibold text-truncate d-inline-block ${!isArchive ? "cursor-pointer" : ""}`}
style={{
maxWidth: "100%",
width: "210px",
}}
onClick={() => setSelectedJob({ showCanvas: true, job: e?.id })}
onClick={() => {
if (!isArchive) {
setSelectedJob({ showCanvas: true, job: e?.id });
}
}}
title={e?.title}
>
{e?.title}
</span>
@ -49,6 +79,7 @@ const JobList = () => {
className: "text-start",
},
{
key: "dueDate",
label: "Due On",
@ -86,7 +117,33 @@ const JobList = () => {
},
];
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"
@ -97,8 +154,7 @@ const JobList = () => {
{jobGrid.map((col) => (
<th
key={col.key}
className={`${col.align || "text-center"} ${
col.className || ""
className={`${col.align || "text-center"} ${col.className || ""
}`}
scope="col"
>
@ -119,9 +175,15 @@ const JobList = () => {
<td
key={col.key}
className={col.className}
onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id })
// onClick={() =>
// setSelectedJob({ showCanvas: true, job: row?.id })
// }
onClick={() => {
if (!isArchive) {
setSelectedJob({ showCanvas: true, job: e?.id });
}
}}
>
{col.getValue(row)}
</td>
@ -135,6 +197,9 @@ const JobList = () => {
<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={() =>
@ -144,7 +209,6 @@ const JobList = () => {
<i className="bx bx-detail bx-sm"></i> View
</button>
<>
<button
className="dropdown-item py-1"
onClick={() =>
@ -154,7 +218,36 @@ const JobList = () => {
<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>
@ -172,6 +265,7 @@ const JobList = () => {
</tbody>
</table>
</div>
</>
);
};

View File

@ -5,8 +5,8 @@ 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 "./ManageJob";
import ManageJobTicket from "./ManageJobTicket";
import ManageJob from "./ServiceProjectJob/ManageJob";
import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket";
import GlobalModel from "../common/GlobalModel";
import PreviewDocument from "../Expenses/PreviewDocument";
@ -21,6 +21,8 @@ export const useServiceProjectJobContext = () => {
};
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({
@ -58,9 +60,21 @@ const Jobs = () => {
<ManageJob Job={manageJob.jobId} />
</OffcanvasComponent>
<div className="card page-min-h my-2 px-7 pb-4">
<div className="row">
<div className="col-12 py-2 d-flex justify-content-end ">
<div className="px-2">
<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 })}
@ -70,9 +84,10 @@ const Jobs = () => {
</div>
</div>
<JobList filterByProject={selectedProject} />
</div>
{/* Job List */}
<JobList filterByProject={selectedProject} isArchive={showArchive} />
</div>
</JonContext.Provider>
</>
);

View File

@ -192,8 +192,8 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
{...register("statusId")}
>
<option>Select Service</option>
{PROJECT_STATUS.map((status) => (
<option value={status.id}>{status.label}</option>
{PROJECT_STATUS?.map((status) => (
<option key={status.id} value={status.id}>{status.label}</option>
))}
</select>
{errors?.statusId && (

View File

@ -0,0 +1,95 @@
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

View File

@ -0,0 +1,86 @@
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;

View File

@ -0,0 +1,358 @@
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;

View File

@ -0,0 +1,283 @@
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;

View File

@ -1,17 +1,16 @@
import SelectField from "../common/Forms/SelectField";
import { useJobStatus } from "../../hooks/masterHook/useMaster";
import { SpinnerLoader } from "../common/Loader";
import Error from "../common/Error";
import SelectField from "../../common/Forms/SelectField";
import Error from "../../common/Error";
import { z } from "zod";
import {
AppFormController,
AppFormProvider,
useAppForm,
} from "../../hooks/appHooks/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 { 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" }),
@ -32,7 +31,7 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
} = methods;
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
// handleClose();
handleClose();
});
const onSubmit = (formData) => {
const payload =
@ -53,6 +52,11 @@ const ChangeStatus = ({ statusId, projectId, jobId, 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

View File

@ -1,16 +1,16 @@
import React, { useEffect, useState } from "react";
import Avatar from "../common/Avatar";
import { useAppForm } from "../../hooks/appHooks/useAppForm";
import Avatar from "../../common/Avatar";
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { JobCommentSchema } from "./ServiceProjectSchema";
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";
} 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 {
@ -150,7 +150,7 @@ const JobComments = ({ data }) => {
type="submit"
disabled={!watch("comment")?.trim() || isPending}
>
Submit
Send
</button>
</div>
</form>
@ -161,30 +161,33 @@ const JobComments = ({ data }) => {
const user = item?.createdBy;
return (
<div
key={item.id}
className="list-group-item border-0 border-bottom p-0"
>
<div className="d-flex align-items-start mt-2 mx-0 px-0">
<Avatar
size="xs"
firstName={user?.firstName}
lastName={user?.lastName}
/>
<div className="">
<div className="d-flex flex-row gap-3">
<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 ">
<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
@ -204,7 +207,6 @@ const JobComments = ({ data }) => {
</div>
</div>
</div>
</div>
);
})}
</div>

View File

@ -1,6 +1,7 @@
import React from "react";
import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import Avatar from "../../common/Avatar";
import { formatUTCToLocalTime } from "../../../utils/dateUtils";
const JobStatusLog = ({ data }) => {
return (

View File

@ -1,30 +1,32 @@
import React, { useEffect, useState } from "react";
import Breadcrumb from "../common/Breadcrumb";
import Label from "../common/Label";
import Breadcrumb from "../../common/Breadcrumb";
import Label from "../../common/Label";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultJobValue, jobSchema } from "./ServiceProjectSchema";
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";
} 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";
} 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 { useJobStatus } from "../../../hooks/masterHook/useMaster";
import { useServiceProjectJobContext } from "../Jobs";
import { SelectFieldSearch } from "../../common/Forms/SelectFieldServerSide";
const ManageJob = ({ Job }) => {
const { setManageJob, setSelectedJob } = useServiceProjectJobContext();
@ -40,6 +42,7 @@ const ManageJob = ({ Job }) => {
watch,
handleSubmit,
reset,
setValue,
formState: { errors },
} = methods;
@ -162,6 +165,7 @@ const ManageJob = ({ Job }) => {
dueDate: JobData.dueDate ?? null,
tags: JobData.tags ?? [],
statusId: JobData.status.id,
projectBranchId : JobData?.projectBranch?.id
});
}, [JobData, Job, projectId]);
return (
@ -199,7 +203,7 @@ const ManageJob = ({ Job }) => {
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>Select Employee</Label>
<Label>Select Employee</Label>
<PmsEmployeeInputTag
control={control}
name="assignees"
@ -238,7 +242,20 @@ const ManageJob = ({ Job }) => {
name="tags"
label="Tag"
placeholder="Enter Tag"
required
/>
</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">

View File

@ -1,25 +1,27 @@
import React, { useEffect } 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 JobStatusLog from "./JobStatusLog";
import JobComments from "./JobComments";
import { daysLeft, getJobStatusBadge } from "../../utils/appUtils";
import HoverPopup from "../common/HoverPopup";
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 { 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",
@ -37,7 +39,7 @@ const ManageJobTicket = ({ Job }) => {
},
];
if (isLoading) return <SpinnerLoader />;
if (isLoading) return <JobDetailsSkeleton />;
if (isError)
return (
<div>
@ -45,7 +47,11 @@ const ManageJobTicket = ({ Job }) => {
</div>
);
return (
<div className="row text-start">
<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">
@ -54,16 +60,16 @@ const ManageJobTicket = ({ Job }) => {
{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">
<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"
title="Change Status"
Mode="click"
className=""
align="right"
content={
<ChangeStatus
statusId={data?.status?.id}
@ -90,23 +96,33 @@ const ManageJobTicket = ({ Job }) => {
<p>{data?.description || "N/A"}</p>
</div>
<div className="d-flex justify-content-between mb-4">
<div className="d-flex flex-row gap-1 fw-medium">
<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="fw-medium">
<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="fw-medium">
<span className="text-secondry">
<i className="bx bx-calendar"></i> Due on :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>
@ -116,7 +132,7 @@ const ManageJobTicket = ({ Job }) => {
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
return (
<span>
<span className="fw-medium me-1">Days Left:</span>
<span className="text-secondry me-1">Days Left:</span>
<span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"}
</span>
@ -124,61 +140,82 @@ const ManageJobTicket = ({ Job }) => {
);
})()}
</div>
<div className="d-block mt-4 mb-3">
<div className="row align-items-start align-items-md-start gap-2 mb-1">
<div className="col-12 col-md-auto">
<small className="fs-6 fw-medium">Created By</small>
{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="col d-flex flex-row align-items-center ">
)}
<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-row align-items-center">
<p className="m-0">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</p>
<small className="text-secondary ms-1">
({data?.createdBy?.jobRoleName})
/>
<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>
{data?.assignees?.length > 0 && (
<div className="row align-items-start align-items-md-start gap-2">
<div className="col-12 col-md-auto">
<small className="fs-6 fw-medium">Assigned To</small>
</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="col">
<div className="row gap-4">
<div className="flex-grow-1">
<div className="d-flex flex-wrap gap-3">
{data?.assignees?.map((emp) => (
<div
key={emp.id}
className="col-6 col-sm-6 col-md-4 col-lg-4"
>
<div className="d-flex align-items-center gap-2">
<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">
<span className=" text-truncate">
<div className="d-flex flex-column ms-2 text-truncate">
<span className="text-truncate">
{emp.firstName} {emp.lastName}
</span>
<small className="text-secondary text-truncate">
<small className="text-secondary text-xs text-truncate">
{emp.jobRoleName}
</small>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>

View File

@ -0,0 +1,80 @@
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;

View File

@ -4,18 +4,27 @@ 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="">Loadng.</div>;
}
if (isLoading)
return (
<div className="py-8">
<SpinnerLoader />
</div>
);
return (
<>
{IsOpenModal && (
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
<GlobalModel
isOpen={IsOpenModal}
closeModal={() => setIsOpenModal(false)}
>
<ManageServiceProject
serviceProjectId={projectId}
onClose={() => setIsOpenModal(false)}
@ -24,98 +33,13 @@ const ServiceProjectProfile = () => {
)}
<div className="row py-2">
<div className="col-md-6 col-lg-4 order-2 mb-6">
<div className="card mb-4">
<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 className="col-md-6 col-lg-5 order-2 mb-6">
<ServiceProfile data={data} setIsOpenModal={setIsOpenModal}/>
</div>
{/* Content section that wraps nicely */}
<div className="flex-grow-1 text-start text-wrap">
{data.name}
<div className="col-md-6 col-lg-7 order-2 mb-6">
<ServiceBranch />
</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"> {/* Added mt-4 for some top margin */}
<a className="d-flex justify-content-center mt-4"> {/* Added mt-4 for some top margin */}
<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>
</div>
</div>
</>
);

View File

@ -50,6 +50,10 @@ export const defaultProjectValues = {
//#endregion
//#region JobSchema
export const TagSchema = z.object({
@ -70,6 +74,8 @@ export const jobSchema = z.object({
tags: z.array(TagSchema).optional().default([]),
statusId: z.string().optional().nullable(),
projectBranchId: z.string().optional().nullable(),
});
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
@ -109,6 +115,52 @@ export const defaultJobValue = {
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

View File

@ -0,0 +1,138 @@
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>
);
};

View File

@ -26,13 +26,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const { setMangeProject, setManageServiceProject } = useProjectContext();
const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%";
};
const getProgressInNumber = (planned, completed) => {
return (completed * 100) / planned;
};
const handleClose = () => setShowModal(false);
const handleViewProject = () => {
@ -43,10 +36,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
navigate(`/service-projects/${project.id}`);
}
};
const handleViewActivities = () => {
dispatch(setProjectId(project.id));
navigate(`/activities/records?project=${project.id}`);
};
const handleManage = () => {
if (isCore) {
setMangeProject({
@ -68,6 +57,8 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
DeleteProject(projectId, false);
};
return (
<>
<ConfirmModal
@ -98,7 +89,7 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
>
{project?.shortName ? project?.shortName : project?.name}
</h5>
<div className="client-info text-body">
<div className="client-info text-body text-start">
<span>{project?.shortName ? project?.name : ""}</span>
</div>
</div>
@ -138,14 +129,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
<span className="align-left">Modify</span>
</a>
</li>
{isCore && (
<li onClick={handleViewActivities}>
<a className="dropdown-item">
<i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span>
</a>
</li>
)}
{!isCore && (
<li
onClick={() =>

View File

@ -0,0 +1,209 @@
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;

View File

@ -162,7 +162,7 @@ const ServiceProjectTeamAllocation = () => {
</div>
<div className="col-12 d-flex flex-row gap-2 flex-wrap">
{selectedEmployees.map((e) => (
<EmployeeChip handleRemove={handleRemove} employee={e} />
<EmployeeChip key={`${e.id}-emp`} handleRemove={handleRemove} employee={e} />
))}
</div>
</>

View File

@ -104,8 +104,7 @@ const SubScriptionHistory = ({ tenantId }) => {
</button>
<button
className="dropdown-item py-1"
onClick={() =>
console.log("Download clicked for", item.id)
onClick={() =>{}
}
>
<i className="bx bx-cloud-download bx-sm"></i> Download

View File

@ -93,11 +93,9 @@ const TenantForm = () => {
};
const onSubmitTenant = (data) => {
console.log("Tenant Data:", data);
};
const onSubmitSubScription = (data) => {
console.log("Subscription Data:", data);
};
const newTenantConfig = [

View File

@ -40,7 +40,6 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
);
const { setProcessedPayment, setAddPayment, setViewCollection } =
useCollectionContext();
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
@ -113,6 +112,16 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
),
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",
label: "Total Amount",
@ -129,6 +138,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
),
align: "text-end",
},
{
key: "balance",
label: "Balance",

View File

@ -25,7 +25,6 @@ const ViewCollection = ({ onClose }) => {
if (isLoading) return <CollectionDetailsSkeleton />;
if (isError) return <div>{error.message}</div>;
return (
<div className="container p-3">
<p className="fs-5 fw-semibold">Collection Details</p>
@ -43,8 +42,7 @@ const ViewCollection = ({ onClose }) => {
<div>
{" "}
<span
className={`badge bg-label-${
data?.isActive ? "primary" : "danger"
className={`badge bg-label-${data?.isActive ? "primary" : "danger"
}`}
>
{data?.isActive ? "Active" : "Inactive"}
@ -214,8 +212,7 @@ const ViewCollection = ({ onClose }) => {
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item">
<button
className={`nav-link ${
activeTab === "payments" ? "active" : ""
className={`nav-link ${activeTab === "payments" ? "active" : ""
}`}
onClick={() => setActiveTab("payments")}
type="button"
@ -225,8 +222,7 @@ const ViewCollection = ({ onClose }) => {
</li>
<li className="nav-item">
<button
className={`nav-link ${
activeTab === "details" ? "active" : ""
className={`nav-link ${activeTab === "details" ? "active" : ""
}`}
onClick={() => setActiveTab("details")}
type="button"

View File

@ -1,7 +1,7 @@
import React from 'react'
import React from "react";
export const EmployeeChip = ({handleRemove,employee}) => {
return(
export const EmployeeChip = ({ handleRemove, employee }) => {
return (
<span
key={employee?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
@ -40,6 +40,5 @@ export const EmployeeChip = ({handleRemove,employee}) => {
title="Remove"
/>
</span>
)
}
);
};

View File

@ -17,9 +17,13 @@ const ConfirmModal = ({
case "delete":
return <i className="bx bx-x-circle text-danger" style={{ fontSize: "60px" }}></i>;
case "success":
return <i className="bx bx-check-circle text-success" style={{ fontSize: "60px" }}></i>;
case "warning":
return <i className="bx bx-error-circle text-warning" style={{ fontSize: "60px" }}></i>;
return <i className="bx bx-archive-in text-warning" style={{ fontSize: "60px" }}></i>;
case "archive":
return <i className="bx bx-archive-in 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:
return null;
}

View File

@ -7,6 +7,7 @@ import Avatar from "./Avatar";
const EmployeeSearchInput = ({
control,
name,
size = "sm",
projectId,
placeholder,
forAll,
@ -46,7 +47,7 @@ const EmployeeSearchInput = ({
<input
type="text"
ref={ref}
className={`form-control form-control-sm`}
className={`form-control form-control-sm-${size}`}
placeholder={placeholder}
value={search}
onChange={(e) => {

View File

@ -2,15 +2,18 @@ import React, { useEffect, useRef, useState } from "react";
import Label from "../Label";
const InputSuggessionField = ({
organizationList = [],
suggesstionList = [],
value,
onChange,
error,
disabled=false
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)) {
@ -21,12 +24,12 @@ const InputSuggessionField = ({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const selectedOption = options.find((opt) => opt[valueKey] === value);
const selectedOption = suggesstionList.find((opt) => opt === value);
const displayText = selectedOption ? selectedOption[labelKey] : placeholder;
const displayText = selectedOption ? selectedOption : placeholder;
const handleSelect = (option) => {
onChange(option[valueKey]);
onChange(option);
setOpen(false);
};
@ -57,7 +60,7 @@ const InputSuggessionField = ({
{open && !isLoading && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
style={{
position: "absolute",
top: "100%",
@ -68,16 +71,14 @@ const InputSuggessionField = ({
overflow: "hidden",
}}
>
{options.map((option, i) => (
{suggesstionList.map((option, i) => (
<li key={i}>
<button
type="button"
className={`dropdown-item ${
option[valueKey] === value ? "active" : ""
}`}
className={`dropdown-item ${option === value ? "active" : ""}`}
onClick={() => handleSelect(option)}
>
{option[labelKey]}
{option}
</button>
</li>
))}

View File

@ -3,6 +3,7 @@ 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",
@ -18,6 +19,7 @@ const SelectEmployeeServerSide = ({
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const [forcedSelected, setForcedSelected] = useState(null);
const { data, isLoading } = useEmployeesName(
projectId,
@ -34,22 +36,15 @@ const SelectEmployeeServerSide = ({
return `${emp.firstName || ""} ${emp.lastName || ""}`.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);
selectedSingle =
options.find((o) => o[valueKey] === value) || forcedSelected;
}
/** -----------------------------
* SELECTED OPTION (MULTIPLE)
* ----------------------------- */
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
@ -57,54 +52,61 @@ const SelectEmployeeServerSide = ({
}
}
/** 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]);
setOpen(false);
} 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];
}
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 && (
@ -126,7 +128,6 @@ const SelectEmployeeServerSide = ({
</span>
</button>
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
@ -137,10 +138,10 @@ const SelectEmployeeServerSide = ({
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
overflow: "hidden",
padding: 0,
}}
>
<div className="p-1">
<li className="p-1 sticky-top bg-white" style={{ zIndex: 10 }}>
<input
type="search"
value={searchText}
@ -148,7 +149,7 @@ const SelectEmployeeServerSide = ({
className="form-control form-control-sm"
placeholder="Search..."
/>
</div>
</li>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
@ -168,10 +169,12 @@ const SelectEmployeeServerSide = ({
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]}>
<li key={option[valueKey]} className="px-1 rounded">
<button
type="button"
className={`dropdown-item ${isActive ? "active" : ""}`}
className={`dropdown-item rounded ${
isActive ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
@ -184,6 +187,7 @@ const SelectEmployeeServerSide = ({
</div>
);
};
export default SelectEmployeeServerSide;
export const SelectProjectField = ({
@ -196,6 +200,7 @@ export const SelectProjectField = ({
isFullObject = false,
isMultiple = false,
isAllProject = false,
disabled
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
@ -211,9 +216,6 @@ export const SelectProjectField = ({
return `${project.name || ""}`.trim();
};
/** -----------------------------
* SELECTED OPTION (SINGLE)
* ----------------------------- */
let selectedSingle = null;
if (!isMultiple) {
@ -222,9 +224,6 @@ export const SelectProjectField = ({
selectedSingle = options.find((o) => o[valueKey] === value);
}
/** -----------------------------
* SELECTED OPTION (MULTIPLE)
* ----------------------------- */
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
@ -297,6 +296,7 @@ export const SelectProjectField = ({
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
disabled={disabled}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
@ -337,6 +337,217 @@ export const SelectProjectField = ({
</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

View File

@ -28,7 +28,6 @@ const CommentEditor = () => {
const [value, setValue] = useState("");
const handleSubmit = () => {
console.log("Comment:", value);
// Submit or handle content
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
closePopup,
@ -6,6 +6,10 @@ import {
togglePopup,
} from "../../slices/localVariablesSlice";
/**
* align: "auto" | "left" | "right"
* boundaryRef: optional ref to the drawer/container element to use as boundary
*/
const HoverPopup = ({
id,
title,
@ -13,6 +17,8 @@ const HoverPopup = ({
children,
className = "",
Mode = "hover",
align = "auto",
boundaryRef = null,
}) => {
const dispatch = useDispatch();
const visible = useSelector((s) => s.localVariables.popups[id] || false);
@ -23,11 +29,9 @@ const HoverPopup = ({
const handleMouseEnter = () => {
if (Mode === "hover") dispatch(openPopup(id));
};
const handleMouseLeave = () => {
if (Mode === "hover") dispatch(closePopup(id));
};
const handleClick = (e) => {
if (Mode === "click") {
e.stopPropagation();
@ -35,47 +39,131 @@ const HoverPopup = ({
}
};
// Close on outside click when using click mode
useEffect(() => {
if (Mode !== "click" || !visible) return;
const handleOutside = (e) => {
const handler = (e) => {
if (
!popupRef.current?.contains(e.target) &&
!triggerRef.current?.contains(e.target)
popupRef.current &&
!popupRef.current.contains(e.target) &&
triggerRef.current &&
!triggerRef.current.contains(e.target)
) {
dispatch(closePopup(id));
}
};
document.addEventListener("click", handleOutside);
return () => document.removeEventListener("click", handleOutside);
}, [visible, Mode, id]);
document.addEventListener("click", handler);
return () => document.removeEventListener("click", handler);
}, [Mode, visible, dispatch, id]);
// Positioning effect: respects align prop and stays inside boundary (drawer)
useEffect(() => {
if (!visible || !popupRef.current) return;
if (!visible || !popupRef.current || !triggerRef.current) return;
// run in next frame so DOM/layout settles
requestAnimationFrame(() => {
const popup = popupRef.current;
const rect = popup.getBoundingClientRect();
// 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%)";
if (rect.right > window.innerWidth) {
popup.style.left = "auto";
popup.style.right = "0";
popup.style.transform = "none";
return;
}
if (rect.left < 0) {
popup.style.left = "0";
popup.style.right = "auto";
popup.style.transform = "none";
// 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;
}
}, [visible]);
// 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">
<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
className="d-inline-block"
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
@ -88,21 +176,22 @@ const HoverPopup = ({
{visible && (
<div
ref={popupRef}
className={`bg-white border rounded shadow-sm p-3 w-max position-absolute top-100 mt-2 ${className}`}
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
style={{
zIndex: 2000,
left: "50%",
transform: "translateX(-50%)",
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;

View File

@ -5,14 +5,13 @@ const InputSuggestions = ({
value,
onChange,
error,
disabled=false
disabled = false,
}) => {
const [filteredList, setFilteredList] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const handleInputChange = (e) => {
const val = e.target.value;
onChange(val);
const matches = organizationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
@ -26,7 +25,7 @@ const InputSuggestions = ({
};
return (
<div className="position-relative">
<div className="mb-3 position-relative">
<input
className="form-control form-control-sm"
value={value}
@ -39,19 +38,19 @@ const InputSuggestions = ({
/>
{showSuggestions && filteredList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius:"0px"
borderRadius: "0px",
}}
>
{filteredList.map((org) => (
<li
key={org}
className="list-group-item list-group-item-action border-none "
className="ropdown-item"
style={{
cursor: "pointer",
padding: "5px 12px",
@ -59,17 +58,15 @@ const InputSuggestions = ({
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectSuggestion(org)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
className={`dropdown-item ${
org === value ? "active" : ""
}`}
>
{org}
</li>
))}
</ul>
)}
{error && <small className="danger-text">{error}</small>}

View File

@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import GlobalRepository from "../repositories/GlobalRepository";
import { useQuery } from "@tanstack/react-query";
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
const [dashboard_data, setDashboard_Data] = useState([]);
const [isLineChartLoading, setLoading] = useState(false);
@ -18,11 +17,13 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
try {
const payload = {
days,
FromDate: FromDate || '',
FromDate: FromDate || "",
projectId: projectId || null,
};
const response = await GlobalRepository.getDashboardProgressionData(payload);
const response = await GlobalRepository.getDashboardProgressionData(
payload
);
setDashboard_Data(response.data);
} catch (err) {
setError("Failed to fetch dashboard data.");
@ -38,123 +39,6 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
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) => {
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
const [loading, setLoading] = useState(false);
@ -167,7 +51,10 @@ export const useAttendanceOverviewData = (projectId, days) => {
setError("");
try {
const response = await GlobalRepository.getAttendanceOverview(projectId, days);
const response = await GlobalRepository.getAttendanceOverview(
projectId,
days
);
setAttendanceOverviewData(response.data);
} catch (err) {
setError("Failed to fetch attendance overview data.");
@ -182,7 +69,6 @@ export const useAttendanceOverviewData = (projectId, days) => {
return { attendanceOverviewData, loading, error };
};
// -------------------Query----------------------------
// export const useDashboard_Data = (days, FromDate, projectId)=>{
@ -199,39 +85,47 @@ 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) => {
return useQuery({
queryKey: ["dashboardAttendances", date, projectId],
queryFn: async () => {
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
const resp = await await GlobalRepository.getDashboardAttendanceData(
date,
projectId
);
return resp.data;
}
})
}
},
});
};
export const useDashboardTeamsCardData = (projectId) => {
return useQuery({
queryKey: ["dashboardTeams", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId);
return resp.data;
}
})
}
},
});
};
export const useDashboardTasksCardData = (projectId) => {
return useQuery({
queryKey: ["dashboardTasks", projectId],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
const resp = await GlobalRepository.getDashboardTasksCardData(projectId);
return resp.data;
}
})
}
},
});
};
// export const useAttendanceOverviewData = (projectId, days) => {
// return useQuery({
// queryKey:["dashboardAttendanceOverView",projectId],
@ -247,24 +141,25 @@ export const useDashboardProjectsCardData = () => {
return useQuery({
queryKey: ["dashboardProjects"],
queryFn: async () => {
const resp = await GlobalRepository.getDashboardProjectsCardData();
return resp.data;
}
})
}
},
});
};
export const useExpenseAnalysis = (projectId, startDate, endDate) => {
const hasBothDates = !!startDate && !!endDate;
const noDatesSelected = !startDate && !endDate;
const shouldFetch =
noDatesSelected ||
hasBothDates;
const shouldFetch = noDatesSelected || hasBothDates;
return useQuery({
queryKey: ["expenseAnalysis", projectId, startDate, endDate],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate);
const resp = await GlobalRepository.getExpenseData(
projectId,
startDate,
endDate
);
return resp.data;
},
enabled: shouldFetch,
@ -280,17 +175,20 @@ export const useExpenseStatus = (projectId) => {
queryFn: async () => {
const resp = await GlobalRepository.getExpenseStatus(projectId);
return resp.data;
}
})
}
},
});
};
export const useExpenseDataByProject = (projectId, categoryId, months) => {
return useQuery({
queryKey: ["expenseByProject", projectId, categoryId, months],
queryFn: async () => {
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months);
const resp = await GlobalRepository.getExpenseDataByProject(
projectId,
categoryId,
months
);
return resp.data;
},
});
};

View File

@ -231,7 +231,7 @@ export const useEmployeesName = (projectId, search, allEmployee) => {
queryFn: async () =>
await EmployeeRepository.getEmployeeName(projectId, search, allEmployee),
staleTime: 5 * 60 * 1000, // Optional: cache for 5 minutes
staleTime: 5 * 60 * 1000,
});
};

View File

@ -438,6 +438,15 @@ export const useExpenseTransactions = (employeeId)=>{
keepPreviousData:true,
})
}
export const useExpenseAllTransactionsList = (searchString) => {
return useQuery({
queryKey: ["transaction", searchString],
queryFn: async () => {
const resp = await ExpenseRepository.getAllTranctionList(searchString);
return resp.data;
},
});
};
//#endregion
// ---------------------------Put Post Recurring Expense---------------------------------------

View File

@ -20,14 +20,15 @@ export const useCurrentService = () => {
// ------------------------------Query-------------------
export const useProjects = (pageSize, pageNumber) => {
export const useProjects = (pageSize, pageNumber,searchString) => {
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
return useQuery({
queryKey: ["ProjectsList", pageSize, pageNumber],
queryKey: ["ProjectsList", pageSize, pageNumber,searchString],
queryFn: async () => {
const response = await ProjectRepository.getProjectList(
pageSize,
pageNumber
pageNumber,
searchString,
);
return response?.data;
},
@ -411,7 +412,6 @@ export const useUpdateProject = (onSuccessCallback) => {
},
onError: (error) => {
console.log(error);
showToast(error?.message || "Error while updating project", "error");
},
});

View File

@ -8,13 +8,14 @@ import { ServiceProjectRepository } from "../repositories/ServiceProjectReposito
import showToast from "../services/toastService";
//#region Service Project
export const useServiceProjects = (pageSize, pageNumber) => {
export const useServiceProjects = (pageSize, pageNumber, searchString) => {
return useQuery({
queryKey: ["serviceProjects", pageSize, pageNumber],
queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects(
pageSize,
pageNumber
pageNumber,
searchString
);
return response.data;
},
@ -154,16 +155,25 @@ export const useServiceProjectJobs = (
pageSize,
pageNumber,
isActive = true,
project
project,
isArchive
) => {
return useQuery({
queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project],
queryKey: [
"serviceProjectJobs",
pageSize,
pageNumber,
isActive,
project,
isArchive,
],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobList(
pageSize,
pageNumber,
isActive,
project
project,
isArchive
);
return resp.data;
},
@ -181,7 +191,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => {
);
return resp.data;
},
enabled:!!jobId,
enabled: !!jobId,
initialPageParam: pageNumber,
@ -258,18 +268,25 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => {
export const useUpdateServiceProjectJob = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) => {
// Call the repository patch
mutationFn: async ({ id, payload, isArchiveAction = false }) => {
const resp = await ServiceProjectRepository.UpdateJob(id, payload);
return resp;
return { resp, isArchiveAction };
},
onSuccess: () => {
onSuccess: ({ isArchiveAction }) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
queryClient.invalidateQueries({ queryKey: ["service-job"] });
if (onSuccessCallback) onSuccessCallback();
showToast("Job Updated successfully", "success");
if (isArchiveAction) {
showToast("Job archived successfully", "success");
} else {
showToast("Job restored successfully", "success");
}
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
@ -282,3 +299,125 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
};
//#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"
);
},
});
};

View File

@ -13,6 +13,8 @@ import Label from "../../components/common/Label";
import AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList";
import { employee } from "../../data/masters";
import { formatFigure } from "../../utils/appUtils";
import { useParams } from "react-router-dom";
import { useExpenseTransactions } from "../../hooks/useExpense";
export const AdvancePaymentContext = createContext();
export const useAdvancePaymentContext = () => {
@ -25,14 +27,32 @@ export const useAdvancePaymentContext = () => {
return context;
};
const AdvancePaymentPage = () => {
const { employeeId } = useParams();
const { data: transactionData } = useExpenseTransactions(employeeId, {
enabled: !!employeeId
});
const employeeName = useMemo(() => {
if (Array.isArray(transactionData) && transactionData.length > 0) {
const emp = transactionData[0].employee;
if (emp) return `${emp.firstName} ${emp.lastName}`;
}
return "";
}, [transactionData]);
const [balance, setBalance] = useState(null);
const { control, reset, watch } = useForm({
defaultValues: {
employeeId: "",
employeeId: employeeId || "",
searchString: "",
},
});
const selectedEmployeeId = watch("employeeId");
const selectedEmployeeId = employeeId || watch("employeeId");
const searchString = watch("searchString");
useEffect(() => {
const selectedEmpoyee = sessionStorage.getItem("transaction-empId");
reset({
@ -47,29 +67,19 @@ const AdvancePaymentPage = () => {
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Finance", link: "/advance-payment" },
{ label: "Advance Payment" },
]}
{ label: "Advance Payment", link: "/advance-payment" },
employeeName && { label: employeeName, link: "" },
].filter(Boolean)}
/>
<div className="card px-4 py-2 page-min-h ">
<div className="row py-1">
<div className="col-12 col-md-4">
<div className="d-block text-start">
<EmployeeSearchInput
control={control}
name="employeeId"
projectId={null}
forAll={true}
placeholder={"Enter Employee Name"}
/>
</div>
</div>
<div className="row py-1 justify-content-end">
<div className="col-md-8 d-flex align-items-center justify-content-end">
{balance ? (
<>
<label className="fs-5 fw-semibold">Current Balance : </label>
<span
className={`${
balance > 0 ? "text-success" : "text-danger"
className={`${balance > 0 ? "text-success" : "text-danger"
} fs-5 fw-bold ms-1`}
>
{balance > 0 ? (
@ -88,7 +98,7 @@ const AdvancePaymentPage = () => {
)}
</div>
</div>
<AdvancePaymentList employeeId={selectedEmployeeId} />
<AdvancePaymentList employeeId={selectedEmployeeId} searchString={searchString} />
</div>
</div>
</AdvancePaymentContext.Provider>

View File

@ -0,0 +1,34 @@
import React from 'react'
import Breadcrumb from '../../components/common/Breadcrumb'
import AdvancePaymentList1 from '../../components/AdvancePayment/AdvancePaymentList1'
import { useForm } from 'react-hook-form';
import EmployeeSearchInput from '../../components/common/EmployeeSearchInput';
const AdvancePaymentPage1 = () => {
const { control, reset, watch } = useForm({
defaultValues: {
searchString: "",
},
});
const searchString = watch("searchString");
return (
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Finance", link: "/advance-payment" },
{ label: "Advance Payment" },
]}
/>
<div className="card px-4 py-2 page-min-h">
<div className="row py-1">
<AdvancePaymentList1 searchString={searchString} />
</div>
</div>
</div>
)
}
export default AdvancePaymentPage1

View File

@ -20,7 +20,7 @@ const LandingPage = () => {
<div className="row w-100">
<div className="col-md-auto d-flex justify-content-between align-items-center">
<img
src="/img/brand/ofw-500x500.png"
src="/img/brand/marco-250x250.png"
style={{ width: "40px" }}
className="me-2"
></img>
@ -401,15 +401,15 @@ const LandingPage = () => {
</p>
<h5> Our Mission</h5>{" "}
<p>
At <OfwLabel></OfwLabel>
, our mission is to empower organizations to manage their field
operations effortlessly helping teams stay organized,
accountable, and productive, no matter where they are. We aim to
eliminate manual processes, data silos, and communication gaps
that often slow down projects and increase operational costs.{" "}
<br /> What We Do We provide a comprehensive suite of tools
designed to handle every critical aspect of field management
from workforce tracking to expense control and reporting. With
At <OfwLabel></OfwLabel>, our mission is to empower organizations
to manage their field operations effortlessly helping teams stay
organized, accountable, and productive, no matter where they are.
We aim to eliminate manual processes, data silos, and
communication gaps that often slow down projects and increase
operational costs. <br /> What We Do We provide a comprehensive
suite of tools designed to handle every critical aspect of field
management from workforce tracking to expense control and
reporting. With
<OfwLabel></OfwLabel>, you can:
</p>
<ul>

View File

@ -11,14 +11,18 @@ import GlobalModel from "../../components/common/GlobalModel";
import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject";
import { SpinnerLoader } from "../../components/common/Loader";
import ServiceProjectCard from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectCard";
import ServiceProjectList from "../../components/ServiceProject/ServiceProjectTeam/ServiceProjectList";
import { useDebounce } from "../../utils/appUtils";
const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
const ServiceProjectDisplay = ({ listView, selectedStatuses, searchTerm }) => {
const [currentPage, setCurrentPage] = useState(1);
const { manageServiceProject, setManageServiceProject } = useProjectContext();
const debouncedSearch = useDebounce(searchTerm, 500);
const { data, isLoading, isError, error } = useServiceProjects(
ITEMS_PER_PAGE,
currentPage
currentPage,
debouncedSearch
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
@ -47,15 +51,20 @@ const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
</div>
);
return (
<div className="">
<div className="row">
{listView ? (
<p>List</p>
) : (
<ServiceProjectList data={filteredProjects}
currentPage={currentPage}
totalPages={data?.totalPages}
paginate={paginate}
isCore={false} />
) : filteredProjects?.length > 0 ? (
filteredProjects?.map((project) => (
<ServiceProjectCard key={project.id} project={project} isCore={false} />
))
)}
):(<div className="d-flex justify-content-center align-items-center page-min-h "><p>No Service projects available</p></div>)}
<div className="col-12 d-flex justify-content-start mt-3">
<Pagination
@ -82,6 +91,7 @@ const ServiceProjectDisplay = ({ listView ,selectedStatuses }) => {
</GlobalModel>
)}
</div>
</div>
);
};

View File

@ -116,59 +116,63 @@ const CollectionPage = () => {
/>
<div className="card my-3 py-2 px-sm-4 px-2">
<div className="row align-items-center mx-0">
{/* Left side: Date Picker + Show Pending (stacked on mobile) */}
<div className="col-12 col-md-6 d-flex flex-column flex-md-row flex-wrap align-items-start align-md-items-center gap-2 gap-md-3 mb-3 mb-md-0">
<FormProvider {...methods}>
<DateRangePicker1 howManyDay={180} startField="fromDate"
endField="toDate" />
</FormProvider>
<div className="form-check form-switch d-flex align-items-center mt-1">
<input
type="checkbox"
className="form-check-input"
role="switch"
id="inactiveEmployeesCheckbox"
checked={showPending}
onChange={(e) => setShowPending(e.target.checked)}
/>
<label
className="form-check-label ms-2"
htmlFor="inactiveEmployeesCheckbox"
<div className="row align-items-center gap-sm-2 gap-md-0 mx-0">
<div className="col-12 col-md-4 d-flex flex-column flex-md-row flex-wrap align-items-start">
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-none">
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
!showPending ? "btn-primary text-white" : ""
}`}
onClick={() => setShowPending(false)}
>
Show Completed Collections
</label>
Show All
</button>
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${
showPending ? "btn-primary text-white" : ""
}`}
onClick={() => setShowPending(true)}
>
Pending
</button>
</div>
</div>
{/* Right side: Search + Add Button */}
<div className="col-12 col-sm-6 d-flex justify-content-end align-items-center gap-2">
<div className="col-12 col-sm-8 d-block d-sm-flex justify-content-end ga-2 align-items-center gap-2">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Search Collection"
className="form-control form-control-sm w-auto"
className="form-control form-control-sm mt-2 mt-sm-0"
/>
<div className="d-flex justify-content-between justify-content-sm-between mt-2 mt-sm-0">
<FormProvider {...methods} className="me-3">
<DateRangePicker1
howManyDay={180}
startField="fromDate"
endField="toDate"
/>
</FormProvider>
{(canCreate || isAdmin) && (
<button
className="btn btn-sm btn-primary"
className="btn btn-sm btn-primary ms-sm-2"
type="button"
onClick={() =>
setCollection({ isOpen: true, invoiceId: null })
}
>
<i className="bx bx-plus-circle me-2"></i>
<span className="d-none d-md-inline-block">
Add New Collection
</span>
<span className="d-none d-md-inline-block">Collection</span>
</button>
)}
</div>
</div>
</div>
</div>
<CollectionList
fromDate={fromDate}

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useState } from "react";
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import {
ITEMS_PER_PAGE,
@ -36,15 +36,17 @@ const ProjectPage = () => {
isOpen: false,
project: null,
});
const dropdownRef = useRef(null);
const [projectList, setProjectList] = useState([]);
const [listView, setListView] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [coreProjects, setCoreProjects] = useState(() => {
const storedValue = sessionStorage.getItem('whichProjectDisplay');
return storedValue === 'true';
const storedValue = sessionStorage.getItem("whichProjectDisplay");
return storedValue === "true";
});
const HasManageProject = useHasUserPermission(MANAGE_PROJECT);
const [currentPage, setCurrentPage] = useState(1);
const [open, setOpen] = useState(false);
const [selectedStatuses, setSelectedStatuses] = useState(
PROJECT_STATUS.map((s) => s.id)
@ -64,12 +66,20 @@ const ProjectPage = () => {
manageServiceProject,
};
const handleToggleProject = (value) => {
setCoreProjects(value);
sessionStorage.setItem("whichProjectDisplay", String(value));
};
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<ProjectContext.Provider value={contextDispatcher}>
@ -105,14 +115,11 @@ const ProjectPage = () => {
>
Infra Project
</button>
</div>
</div>
{/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */}
<div className="d-flex flex-wrap align-items-center justify-content-end">
{/* Search */}
<div className="me-2" style={{ minWidth: "200px" }}>
<input
@ -131,7 +138,8 @@ const ProjectPage = () => {
<div className="d-flex gap-2">
<button
type="button"
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"}`}
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
title="Card View"
>
@ -140,7 +148,8 @@ const ProjectPage = () => {
<button
type="button"
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"}`}
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
title="List View"
>
@ -149,17 +158,36 @@ const ProjectPage = () => {
</div>
{/* Dropdown Filter */}
<div className="dropdown me-2">
<a
className="dropdown-toggle hide-arrow cursor-pointer p-1"
data-bs-toggle="dropdown"
aria-expanded="false"
title="Filter"
<div className="dropdown me-2 position-relative">
<div
className="cursor-pointer p-1"
onClick={() => setOpen(!open)}
>
<i className="bx bx-slider-alt fs-5"></i>
</a>
<i
className={`bx bx-slider-alt fs-5 ${selectedStatuses.length !== PROJECT_STATUS.length ? "text-primary" : ""
}`}
></i>
<ul className="dropdown-menu p-2 text-capitalize">
{selectedStatuses.length !== PROJECT_STATUS.length && (
<span className="badge bg-warning text-white rounded-pill position-absolute"
style={{
top: "-4px",
right: "-4px",
fontSize: "0.6rem",
padding: "2px 5px",
}}
>
{PROJECT_STATUS.length - selectedStatuses.length}
</span>
)}
</div>
{open && (
<ul
ref={dropdownRef}
className="dropdown-menu show p-2 text-capitalize"
onMouseDown={(e) => e.stopPropagation()} // IMPORTANT
>
{PROJECT_STATUS.map(({ id, label }) => (
<li key={id}>
<div className="form-check">
@ -167,23 +195,37 @@ const ProjectPage = () => {
className="form-check-input"
type="checkbox"
checked={selectedStatuses.includes(id)}
onClick={(e) => e.stopPropagation()} // IMPORTANT
onChange={() => handleStatusChange(id)}
/>
<label className="form-check-label">{label}</label>
<label
className="form-check-label"
onClick={(e) => e.stopPropagation()} // OPTIONAL
>
{label}
</label>
</div>
</li>
))}
</ul>
)}
</div>
{HasManageProject && (
<button
type="button"
className="btn btn-primary btn-sm d-flex align-items-center my-2"
onClick={() =>
onClick={
() =>
coreProjects
? setMangeProject({ isOpen: true, Project: null }) // Organization Project Infra
: setManageServiceProject({ isOpen: true, Project: null }) // Service Project
: setManageServiceProject({
isOpen: true,
Project: null,
}) // Service Project
}
>
<i className="bx bx-plus-circle me-2"></i>
@ -195,11 +237,22 @@ const ProjectPage = () => {
</div>
</div>
{coreProjects ? <ProjectsDisplay listView={listView}
{coreProjects ? (
<ProjectsDisplay
listView={listView}
searchTerm={searchTerm}
selectedStatuses={selectedStatuses}
handleStatusChange={handleStatusChange} /> : <ServiceProjectDisplay listView={listView}
selectedStatuses={selectedStatuses} />}
handleStatusChange={handleStatusChange}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
) : (
<ServiceProjectDisplay
listView={listView}
searchTerm={searchTerm}
selectedStatuses={selectedStatuses}
/>
)}
</div>
</ProjectContext.Provider>
);

View File

@ -10,6 +10,7 @@ import { useServiceProjects } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
import usePagination from "../../hooks/usePagination";
import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
import { useDebounce } from "../../utils/appUtils";
const ProjectsDisplay = ({
listView,
@ -26,8 +27,8 @@ const ProjectsDisplay = ({
} = useProjectContext();
const [projectList, setProjectList] = useState([]);
const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1);
const debouncedSearch = useDebounce(searchTerm, 500);
const { data, isLoading, isError, error } = useProjects(ITEMS_PER_PAGE, 1, debouncedSearch);
const filteredProjects =
data?.data?.filter((project) => {
@ -98,7 +99,7 @@ const ProjectsDisplay = ({
);
return (
<div className="row">
<div className="">
{listView ? (
<ProjectListView
data={projectList}

View File

@ -11,12 +11,13 @@ const EmployeeRepository = {
// deleteEmployee: ( id ) => api.delete( `/users/${ id }` ),
getEmployeeProfile: (id) => api.get(`/api/employee/profile/get/${id}`),
deleteEmployee: (id, active) => api.delete(`/api/employee/${id}?active=${active}`),
getEmployeeName: (projectId, search, allEmployee) => {
getEmployeeName: (projectId, search, allEmployee,employeeId) => {
const params = new URLSearchParams();
if (projectId) params.append("projectId", projectId);
if (search) params.append("searchString", search);
if (allEmployee) params.append("allEmployee", allEmployee)
if (allEmployee) params.append("allEmployee", allEmployee);
if (employeeId) params.append("employeeId", employeeId);
const query = params.toString();
return api.get(`/api/Employee/basic${query ? `?${query}` : ""}`);

View File

@ -44,13 +44,13 @@ const ExpenseRepository = {
DeletePaymentRequest: () => api.get("delete here come"),
CreatePaymentRequestExpense: (data) =>
api.post("/api/Expense/payment-request/expense/create", data),
GetPayee:()=>api.get('/api/Expense/payment-request/payee'),
GetPayee: () => api.get('/api/Expense/payment-request/payee'),
//#endregion
//#region Recurring Expense
GetRecurringExpenseList:(pageSize, pageNumber, filter,isActive, searchString) => {
GetRecurringExpenseList: (pageSize, pageNumber, filter, isActive, searchString) => {
const payloadJsonString = JSON.stringify(filter);
return api.get(
`/api/expense/get/recurring-payment/list?pageSize=${pageSize}&pageNumber=${pageNumber}&filter=${payloadJsonString}&isActive=${isActive}&searchString=${searchString}`
@ -70,6 +70,8 @@ const ExpenseRepository = {
//#region Advance Payment
GetTranctionList: (employeeId) =>
api.get(`/api/Expense/get/transactions/${employeeId}`),
getAllTranctionList: (searchString) =>
api.get(`/api/Expense/get/advance-payment/employee/list?searchString=${searchString}`),
//#endregion

View File

@ -18,6 +18,8 @@ const GlobalRepository = {
return api.get(`/api/Dashboard/Progression?${params.toString()}`);
},
getProjectCompletionStatus:()=>api.get(`/api/Dashboard/project-completion-status`),
getDashboardAttendanceData: (date, projectId) => {

View File

@ -1,8 +1,9 @@
import { api } from "../utils/axiosClient";
const ProjectRepository = {
getProjectList: (pageSize, pageNumber) =>
api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}`),
getProjectList: (pageSize, pageNumber,searchString) =>
api.get(`/api/project/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`),
getProjectByprojectId: (projetid) =>
api.get(`/api/project/details/${projetid}`),

View File

@ -1,10 +1,12 @@
import { isAction } from "@reduxjs/toolkit";
import { api } from "../utils/axiosClient";
export const ServiceProjectRepository = {
//#region Service Project
CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data),
GetServiceProjects: (pageSize, pageNumber) =>
GetServiceProjects: (pageSize, pageNumber,searchString) =>
api.get(
`/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}`
`/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`
),
GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`),
UpdateServiceProject: (id, data) =>
@ -17,12 +19,14 @@ export const ServiceProjectRepository = {
api.get(
`/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} `
),
//#endregion
//#region Job
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),
GetJobList: (pageSize, pageNumber, isActive, projectId) =>
GetJobList: (pageSize, pageNumber, isActive, projectId,isArchive) =>
api.get(
`/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}`
`/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}&isArchive=${isArchive}`
),
GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`),
AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data),
@ -35,4 +39,22 @@ export const ServiceProjectRepository = {
api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, {
"Content-Type": "application/json-patch+json",
}),
//#endregion
//#region Project Branch
CreateBranch: (data) => api.post(`/api/ServiceProject/branch/create`, data),
UpdateBranch: (id, data) =>
api.put(`/api/ServiceProject/branch/edit/${id}`, data),
GetBranchList: (projectId, isActive, pageSize, pageNumber, searchString) => {
return api.get(
`/api/ServiceProject/branch/list/${projectId}?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`
);
},
GetBranchDetail: (id) => api.get(`/api/ServiceProject/branch/details/${id}`),
DeleteBranch: (id, isActive = false) =>
api.delete(`/api/ServiceProject/branch/delete/${id}?isActive=${isActive}`),
GetBranchTypeList: () => api.get(`/api/serviceproject/branch-type/list`),
};

View File

@ -60,7 +60,8 @@ import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage";
import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail";
import ManageJob from "../components/ServiceProject/ManageJob";
import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob";
import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1";
const router = createBrowserRouter(
[
{
@ -116,7 +117,8 @@ const router = createBrowserRouter(
{ path: "/expenses", element: <ExpensePage /> },
{ path: "/payment-request", element: <PaymentRequestPage /> },
{ path: "/recurring-payment", element: <RecurringExpensePage /> },
{ path: "/advance-payment", element: <AdvancePaymentPage /> },
{ path: "/advance-payment", element: <AdvancePaymentPage1 /> },
{ path: "/advance-payment/:employeeId", element: <AdvancePaymentPage /> },
{ path: "/collection", element: <CollectionPage /> },
{ path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> },

View File

@ -150,6 +150,9 @@ export function startSignalR(loggedUser) {
queryClient.invalidateQueries(["serviceProjects"]);
queryClient.invalidateQueries(["serviceProject"]);
}
if (keyword === "Project_Branch") {
queryClient.invalidateQueries(["branches"]);
}
if (keyword === "Service_Project_Allocation") {
queryClient.invalidateQueries(["serviceProjectTeam"]);

View File

@ -211,3 +211,34 @@ export const PAYEE_RECURRING_EXPENSE = [
export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69"
//#endregion
export const JOBS_STATUS_IDS = [
{
id: "32d76a02-8f44-4aa0-9b66-c3716c45a918",
label: "New",
},
{
id: "cfa1886d-055f-4ded-84c6-42a2a8a14a66",
label: "Assigned",
},
{
id: "5a6873a5-fed7-4745-a52f-8f61bf3bd72d",
label: "In Progress",
},
{
id: "aab71020-2fb8-44d9-9430-c9a7e9bf33b0",
label: "Work Done",
},
{
id: "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7",
label: "Review Done",
},
{
id: "3ddeefb5-ae3c-4e10-a922-35e0a452bb69",
label: "Closed",
},
{
id: "75a0c8b8-9c6a-41af-80bf-b35bab722eb2",
label: "On Hold",
},
];