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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

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

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

View File

@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { defaultExpense, ExpenseSchema } from "./ExpenseSchema"; import { defaultExpense, ExpenseSchema } from "./ExpenseSchema";
import { formatFileSize, localToUtc } from "../../utils/appUtils"; import { formatFileSize, localToUtc } from "../../utils/appUtils";
import { useProjectName } from "../../hooks/useProjects"; import { useProjectName } from "../../hooks/useProjects";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice"; import { changeMaster } from "../../slices/localVariablesSlice";
import useMaster, { import useMaster, {
@ -32,6 +32,11 @@ import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput"; import EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist"; import Filelist from "./Filelist";
import { DEFAULT_CURRENCY } from "../../utils/constants"; import { DEFAULT_CURRENCY } from "../../utils/constants";
import SelectEmployeeServerSide, {
SelectProjectField,
} from "../common/Forms/SelectFieldServerSide";
import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject";
import { AppFormController } from "../../hooks/appHooks/useAppForm";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const { const {
@ -40,6 +45,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
error: ExpenseErrorLoad, error: ExpenseErrorLoad,
} = useExpense(expenseToEdit); } = useExpense(expenseToEdit);
const [expenseCategory, setExpenseCategory] = useState(); const [expenseCategory, setExpenseCategory] = useState();
const [selectedEmployees, setSelectedEmployees] = useState([]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
expenseCategories, expenseCategories,
@ -83,11 +89,11 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
loading: StatusLoadding, loading: StatusLoadding,
error: stausError, error: stausError,
} = useExpenseStatus(); } = useExpenseStatus();
const { // const {
data: employees, // data: employees,
isLoading: EmpLoading, // isLoading: EmpLoading,
isError: isEmployeeError, // isError: isEmployeeError,
} = useEmployeesNameByProject(selectedproject); // } = useEmployeesNameByProject(selectedproject);
const files = watch("billAttachments"); const files = watch("billAttachments");
const onFileChange = async (e) => { const onFileChange = async (e) => {
@ -150,6 +156,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
} }
}; };
const { mutate: AllocationTeam, isPending1 } =
useAllocationServiceProjectTeam(() => {
setSelectedEmployees([]);
setSeletingEmp({
employee: null,
isOpen: false,
});
});
useEffect(() => { useEffect(() => {
if (expenseToEdit && data) { if (expenseToEdit && data) {
reset({ reset({
@ -180,7 +194,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
: [], : [],
}); });
} }
}, [data, reset, employees]); }, [data, reset]);
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() => const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
handleClose() handleClose()
); );
@ -223,7 +237,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</h5> </h5>
<form id="expenseForm" onSubmit={handleSubmit(onSubmit)}> <form id="expenseForm" onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2 text-start"> <div className="row my-2 text-start">
<div className="col-md-6"> {/* <div className="col-md-6">
<Label className="form-label" required> <Label className="form-label" required>
Select Project Select Project
</Label> </Label>
@ -245,6 +259,23 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{errors.projectId && ( {errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small> <small className="danger-text">{errors.projectId.message}</small>
)} )}
</div> */}
<div className="col-12 col-md-6 mb-2">
<SelectProjectField
label="Project"
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
shouldValidate: true,
})
}
/>
{errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small>
)}
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
@ -307,14 +338,28 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
)} )}
</div> </div>
<div className="col-12 col-md-6 text-start"> <div className="col-12 col-md-6 text-start">
<Label className="form-label" required> {/* <Label className="form-label" required>
Paid By{" "} Paid By{" "}
</Label> </Label> */}
<EmployeeSearchInput {/* <EmployeeSearchInput
control={control} control={control}
name="paidById" name="paidById"
projectId={null} projectId={null}
forAll={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>
</div> </div>
@ -423,10 +468,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.gstNumber.message}</small> <small className="danger-text">{errors.gstNumber.message}</small>
)} )}
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col-md-6 text-start "> <div className="col-md-6 text-start ">
<Label htmlFor="currencyId" className="form-label" required> <Label htmlFor="currencyId" className="form-label" required>
Select Currency Select Currency
</Label> </Label>
@ -452,24 +496,26 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.currencyId.message}</small> <small className="danger-text">{errors.currencyId.message}</small>
)} )}
</div> </div>
{expenseCategory?.noOfPersonsRequired && ( {expenseCategory?.noOfPersonsRequired && (
<div className="col-md-6 text-start"> <div className="col-md-6 text-start">
<Label className="form-label" required>No. of Persons</Label> <Label className="form-label" required>
<input No. of Persons
type="number" </Label>
id="noOfPersons" <input
className="form-control form-control-sm" type="number"
{...register("noOfPersons")} id="noOfPersons"
inputMode="numeric" className="form-control form-control-sm"
/> {...register("noOfPersons")}
{errors.noOfPersons && ( inputMode="numeric"
<small className="danger-text"> />
{errors.noOfPersons.message} {errors.noOfPersons && (
</small> <small className="danger-text">
)} {errors.noOfPersons.message}
</div> </small>
)} )}
</div> </div>
)}
</div>
<div className="row my-2 text-start"> <div className="row my-2 text-start">
<div className="col-md-12"> <div className="col-md-12">

View File

@ -1,54 +1,80 @@
import { useState } from "react"; import { useState } from "react";
const PreviewDocument = ({ imageUrl }) => { const PreviewDocument = ({ imageUrl }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [rotation, setRotation] = useState(0); const [rotation, setRotation] = useState(0);
const [scale, setScale] = useState(1);
const zoomIn = () => setScale((prev) => Math.min(prev + 0.2, 3));
const zoomOut = () => setScale((prev) => Math.max(prev - 0.2, 0.4));
const resetAll = () => {
setRotation(0);
setScale(1);
};
return ( return (
<> <>
<div className="d-flex justify-content-start"> <div className="d-flex justify-content-start gap-3 mb-2">
<i <i
className="bx bx-rotate-right cursor-pointer" className="bx bx-rotate-right cursor-pointer fs-4"
title="Rotate"
onClick={() => setRotation((prev) => prev + 90)} onClick={() => setRotation((prev) => prev + 90)}
></i> ></i>
</div>
<div
className="position-relative d-flex flex-column justify-content-center align-items-center"
style={{ minHeight: "80vh" }}
>
{loading && (
<div className="text-secondary text-center mb-2">Loading...</div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center"> <i
<img className="bx bx-zoom-in cursor-pointer fs-4"
src={imageUrl} title="Zoom In"
alt="Full View" onClick={zoomIn}
className="img-fluid" ></i>
style={{
maxHeight: "80vh", <i
objectFit: "contain", className="bx bx-zoom-out cursor-pointer fs-4"
display: loading ? "none" : "block", title="Zoom Out"
transform: `rotate(${rotation}deg)`, onClick={zoomOut}
transition: "transform 0.3s ease", ></i>
}}
onLoad={() => setLoading(false)}
/>
</div> </div>
<div className="position-absolute bottom-0 start-0 justify-content-center gap-2"> <div
<button className="position-relative d-flex flex-column justify-content-center align-items-center overflow-hidden"
className="btn btn-outline-secondary" style={{ minHeight: "80vh" }}
onClick={() => setRotation(0)} >
title="Reset Rotation" {loading && (
> <div className="text-secondary text-center mb-2">
<i className="bx bx-reset"></i> Reset Loading...
</button> </div>
)}
<div className="mb-3 d-flex justify-content-center align-items-center">
<img
src={imageUrl}
alt="Full View"
className="img-fluid"
style={{
maxHeight: "80vh",
objectFit: "contain",
display: loading ? "none" : "block",
transform: `rotate(${rotation}deg) scale(${scale})`,
transition: "transform 0.3s ease",
cursor: "grab",
}}
onLoad={() => setLoading(false)}
/>
</div>
<div className="position-absolute bottom-0 start-0 m-2">
<button
className="btn btn-outline-secondary"
onClick={resetAll}
>
<i className="bx bx-reset"></i> Reset
</button>
</div>
</div> </div>
</div> </>
</>
); );
}; };
export default PreviewDocument; export default PreviewDocument;

View File

@ -50,8 +50,11 @@ const Header = () => {
const isRecurringExpense = /^\/recurring-payment$/.test(pathname); const isRecurringExpense = /^\/recurring-payment$/.test(pathname);
const isAdvancePayment = /^\/advance-payment$/.test(pathname); const isAdvancePayment = /^\/advance-payment$/.test(pathname);
const isServiceProjectPage = /^\/service-projects\/[0-9a-fA-F-]{36}$/.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 = [ const allowedProjectStatusIds = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba", "603e994b-a27f-4e5d-a251-f3d69b0498ba",

View File

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

View File

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

View File

@ -29,6 +29,7 @@ import Filelist from "../Expenses/Filelist";
import InputSuggestions from "../common/InputSuggestion"; import InputSuggestions from "../common/InputSuggestion";
import { useProfile } from "../../hooks/useProfile"; import { useProfile } from "../../hooks/useProfile";
import { blockUI } from "../../utils/blockUI"; import { blockUI } from "../../utils/blockUI";
import { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
function ManagePaymentRequest({ closeModal, requestToEdit = null }) { function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
const { const {
@ -234,10 +235,10 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
{/* Project and Category */} {/* Project and Category */}
<div className="row my-2 text-start"> <div className="row my-2 text-start">
<div className="col-md-6"> <div className="col-md-6">
<Label className="form-label" required> {/* <Label className="form-label" required>
Select Project Select Project
</Label> </Label> */}
<select {/* <select
className="form-select form-select-sm" className="form-select form-select-sm"
{...register("projectId")} {...register("projectId")}
disabled={ disabled={
@ -254,7 +255,23 @@ function ManagePaymentRequest({ closeModal, requestToEdit = null }) {
</option> </option>
)) ))
)} )}
</select> </select> */}
<SelectProjectField
label="Project"
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
shouldValidate: true,
})
}
disabled={
data?.recurringPayment?.isVariable && !isDraft && !isProcessed
}
/>
{errors.projectId && ( {errors.projectId && (
<small className="danger-text">{errors.projectId.message}</small> <small className="danger-text">{errors.projectId.message}</small>
)} )}

View File

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

View File

@ -148,7 +148,7 @@ const ViewPaymentRequest = ({ requestId }) => {
<div className="col-12 col-sm-6 "> <div className="col-12 col-sm-6 ">
<div className="row "> <div className="row ">
<div className="col-12 d-flex justify-content-between mb-6"> <div className="col-12 d-flex justify-content-between mb-6">
<div className="d-flex align-items-center"><span className="fw-semibold">PR No : </span><span className="fw-semibold ms-2"> {data?.paymentRequestUID}</span></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 <span
className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary" className={`badge bg-label-${getColorNameFromHex(data?.expenseStatus?.color) || "secondary"
}`} }`}

View File

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

View File

@ -8,7 +8,12 @@ const ProjectCardView = ({ data, currentPage, totalPages, paginate }) => {
return ( return (
<div className="row page-min-h"> <div className="row page-min-h">
{data?.length === 0 && ( {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) => ( {data?.map((project) => (

View File

@ -126,8 +126,8 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
return ( return (
<div className="card page-min-h py-4 px-6 shadow-sm"> <div className="card page-min-h py-4 px-6 shadow-sm">
<div className="table-responsive text-nowrap page-min-h"> <div className="table-responsive text-nowrap">
<table className="table table-hover align-middle m-0"> <table className="table table-hover align-middle m-0">
<thead className="border-bottom "> <thead className="border-bottom ">
<tr> <tr>
{projectColumns.map((col) => ( {projectColumns.map((col) => (
@ -143,77 +143,94 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.map((project) => ( {data?.length > 0 ? (
<tr key={project.id}> data?.map((project) => (
{projectColumns.map((col) => ( <tr key={project.id}>
<td {projectColumns.map((col) => (
key={col.key} <td
colSpan={col.colSpan} key={col.key}
className={`${col.className} py-5`} colSpan={col.colSpan}
style={{ paddingTop: "20px", paddingBottom: "20px" }} 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 {col.getValue
className="bx bx-dots-vertical-rounded bx-sm text-muted" ? col.getValue(project)
data-bs-toggle="tooltip" : project[col.key] || "N/A"}
data-bs-offset="0,8" </td>
data-bs-placement="top" ))}
data-bs-custom-class="tooltip-dark" <td
title="More Action" className={`mx-2 ${canManageProject ? "d-sm-table-cell" : "d-none"
></i> }`}
</button> >
<ul className="dropdown-menu dropdown-menu-end"> <div className="dropdown z-2">
<li> <button
<a type="button"
aria-label="click to View details" className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
className="dropdown-item cursor-pointer" data-bs-toggle="dropdown"
> aria-expanded="false"
<i className="bx bx-detail me-2"></i> >
<span className="align-left">View details</span> <i
</a> className="bx bx-dots-vertical-rounded bx-sm text-muted"
</li> data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<a
aria-label="click to View details"
className="dropdown-item cursor-pointer"
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li> <li>
<a <a
className="dropdown-item cursor-pointer" className="dropdown-item cursor-pointer"
onClick={() => onClick={() =>
setMangeProject({ setMangeProject({
isOpen: true, isOpen: true,
Project: project.id, Project: project.id,
}) })
} }
> >
<i className="bx bx-pencil me-2"></i> <i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span> <span className="align-left">Modify</span>
</a> </a>
</li> </li>
<li onClick={() => handleViewActivities(project.id)}> <li onClick={() => handleViewActivities(project.id)}>
<a className="dropdown-item cursor-pointer"> <a className="dropdown-item cursor-pointer">
<i className="bx bx-task me-2"></i> <i className="bx bx-task me-2"></i>
<span className="align-left">Activities</span> <span className="align-left">Activities</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</td>
</tr>
))
) : (
<tr
className="no-hover"
style={{
pointerEvents: "none",
backgroundColor: "transparent",
}}
>
<td
colSpan={projectColumns.length + 1}
className="text-center align-middle"
style={{ height: "300px", borderBottom: "none" }}
>
No Infra projects available
</td> </td>
</tr> </tr>
))} )}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -10,11 +10,13 @@ import {
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster"; import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
import showToast from "../../../services/toastService"; import showToast from "../../../services/toastService";
import { useOrganizationEmployees } from "../../../hooks/useOrganization"; import { useOrganizationEmployees } from "../../../hooks/useOrganization";
import { useDispatch } from "react-redux";
import { changeMaster } from "../../../slices/localVariablesSlice";
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => { const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const debounceSearchTerm = useDebounce(searchTerm, 500); const debounceSearchTerm = useDebounce(searchTerm, 500);
const dispatch = useDispatch();
const { const {
data: employeesData = [], data: employeesData = [],
isLoading, isLoading,
@ -45,6 +47,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
}); });
useEffect(() => { useEffect(() => {
dispatch(changeMaster("Job Role"));
if (employeesData?.data?.length > 0) { if (employeesData?.data?.length > 0) {
const available = employeesData.data.filter((emp) => { const available = employeesData.data.filter((emp) => {
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id); const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
@ -119,7 +122,7 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
status: true, status: true,
})); }));
handleAssignEmployee({ payload,actionType:"assign"} ); handleAssignEmployee({ payload, actionType: "assign" });
setEmployees((prev) => setEmployees((prev) =>
prev.map((emp) => ({ prev.map((emp) => ({
@ -132,26 +135,26 @@ const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
); );
}; };
if (isLoading) { if (isLoading) {
return ( <div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>) ; return (<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>);
} }
if (isError) { if (isError) {
return ( return (
<div className="page-min-h d-flex justify-content-center align-items-center "> <div className="page-min-h d-flex justify-content-center align-items-center ">
{error?.status === 400 ? ( {error?.status === 400 ? (
<p className="m-0">Enter employee you want to find.</p> <p className="m-0">Enter employee you want to find.</p>
) : ( ) : (
<p className="m-0 dange-text">Something went wrong. Please try again later.</p> <p className="m-0 dange-text">Something went wrong. Please try again later.</p>
)} )}
</div> </div>
); );
} }
if (employees.length === 0) { if (employees.length === 0) {
return(<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>) ; return (<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>);
} }
return ( return (
@ -183,9 +186,8 @@ if (employees.length === 0) {
onChange={(e) => onChange={(e) =>
handleSelectChange(index, "serviceId", e.target.value) handleSelectChange(index, "serviceId", e.target.value)
} }
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${ className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${emp.errors.serviceId ? "is-invalid" : ""
emp.errors.serviceId ? "is-invalid" : "" }`}
}`}
> >
<option value="">Select Service</option> <option value="">Select Service</option>
{services?.map((s) => ( {services?.map((s) => (
@ -205,9 +207,8 @@ if (employees.length === 0) {
onChange={(e) => onChange={(e) =>
handleSelectChange(index, "jobRole", e.target.value) handleSelectChange(index, "jobRole", e.target.value)
} }
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${ className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${emp.errors.jobRole ? "is-invalid" : ""
emp.errors.jobRole ? "is-invalid" : "" }`}
}`}
> >
<option value="">Select Job Role</option> <option value="">Select Job Role</option>
{jobRoles?.map((r) => ( {jobRoles?.map((r) => (

View File

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

View File

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

View File

@ -4,24 +4,49 @@ import {
getJobStatusBadge, getJobStatusBadge,
getNextBadgeColor, getNextBadgeColor,
} from "../../utils/appUtils"; } from "../../utils/appUtils";
import { useServiceProjectJobs } from "../../hooks/useServiceProject"; import { useServiceProjectJobs, useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE, JOBS_STATUS_IDS } from "../../utils/constants";
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../utils/dateUtils";
import { SpinnerLoader } from "../common/Loader"; import { SpinnerLoader } from "../common/Loader";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import ProjectPage from "../../pages/project/ProjectPage"; import ProjectPage from "../../pages/project/ProjectPage";
import { useServiceProjectJobContext } from "./Jobs"; import { useServiceProjectJobContext } from "./Jobs";
import ConfirmModal from "../common/ConfirmModal";
const JobList = () => { const JobList = ({ isArchive }) => {
const { setSelectedJob, setManageJob } = useServiceProjectJobContext(); const { setSelectedJob, setManageJob } = useServiceProjectJobContext();
const { mutate: UpdateJob,isPending } = useUpdateServiceProjectJob(() => {
});
const { projectId } = useParams(); const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProjectJobs( const { data, isLoading, isError, error } = useServiceProjectJobs(
ITEMS_PER_PAGE, ITEMS_PER_PAGE,
1, 1,
true, 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 = [ const jobGrid = [
{ {
@ -35,12 +60,17 @@ const JobList = () => {
label: "Title", label: "Title",
getValue: (e) => ( getValue: (e) => (
<span <span
className="fw-semibold text-truncate d-inline-block cursor-pointer" className={`fw-semibold text-truncate d-inline-block ${!isArchive ? "cursor-pointer" : ""}`}
style={{ style={{
maxWidth: "100%", maxWidth: "100%",
width: "210px", width: "210px",
}} }}
onClick={() => setSelectedJob({ showCanvas: true, job: e?.id })} onClick={() => {
if (!isArchive) {
setSelectedJob({ showCanvas: true, job: e?.id });
}
}}
title={e?.title}
> >
{e?.title} {e?.title}
</span> </span>
@ -49,6 +79,7 @@ const JobList = () => {
className: "text-start", className: "text-start",
}, },
{ {
key: "dueDate", key: "dueDate",
label: "Due On", label: "Due On",
@ -86,92 +117,155 @@ 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 ( return (
<div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive"> <>
<table {isArchiveModalOpen && (
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive" <ConfirmModal
aria-describedby="DataTables_Table_0_info" isOpen={isArchiveModalOpen}
> type={isArchiveAction ? "archive" : "Un-archive"}
<thead> header={isArchiveAction ? "Archive Job" : "Restore Job"}
<tr> message={
{jobGrid.map((col) => ( isArchiveAction
<th ? "Are you sure you want to archive this job?"
key={col.key} : "Are you sure you want to restore this job?"
className={`${col.align || "text-center"} ${ }
col.className || "" onSubmit={handleArchive}
}`} onClose={() => setIsArchiveModalOpen(false)}
scope="col" loading={isPending}
> />
<div className={col.className}>{col.label}</div> )}
<div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive">
<table
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive"
aria-describedby="DataTables_Table_0_info"
>
<thead>
<tr>
{jobGrid.map((col) => (
<th
key={col.key}
className={`${col.align || "text-center"} ${col.className || ""
}`}
scope="col"
>
<div className={col.className}>{col.label}</div>
</th>
))}
<th className="sorting_disabled text-center" aria-label="Actions">
Actions
</th> </th>
))} </tr>
<th className="sorting_disabled text-center" aria-label="Actions"> </thead>
Actions
</th>
</tr>
</thead>
<tbody> <tbody>
{Array.isArray(data?.data) && data.data.length > 0 ? ( {Array.isArray(data?.data) && data.data.length > 0 ? (
data.data.map((row, i) => ( data.data.map((row, i) => (
<tr key={i} className="text-start"> <tr key={i} className="text-start">
{jobGrid.map((col) => ( {jobGrid.map((col) => (
<td <td
key={col.key} key={col.key}
className={col.className} className={col.className}
onClick={() => // onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id }) // setSelectedJob({ showCanvas: true, job: row?.id })
} // }
> onClick={() => {
{col.getValue(row)} if (!isArchive) {
</td> setSelectedJob({ showCanvas: true, job: e?.id });
))}
<td>
<div className="dropdown text-center">
<button
className="btn btn-icon dropdown-toggle hide-arrow"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<div className="dropdown-menu dropdown-menu-end">
<button
className="dropdown-item py-1"
onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id })
} }
> }}
<i className="bx bx-detail bx-sm"></i> View
</button> >
{col.getValue(row)}
</td>
))}
<td>
<div className="dropdown text-center">
<button
className="btn btn-icon dropdown-toggle hide-arrow"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded bx-md"></i>
</button>
<div className="dropdown-menu dropdown-menu-end">
{!isArchive && (
<>
<button
className="dropdown-item py-1"
onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id })
}
>
<i className="bx bx-detail bx-sm"></i> View
</button>
<button
className="dropdown-item py-1"
onClick={() =>
setManageJob({ isOpen: true, jobId: row?.id })
}
>
<i className="bx bx-edit bx-sm"></i> Edit
</button>
</>
)}
{isArchive && (
<button
className="dropdown-item py-1"
onClick={() => {
setArchiveJobId(row.id);
setIsArchiveAction(false);
setIsArchiveModalOpen(true);
}}
>
<i className="bx bx-reset bx-sm"></i> Restore
</button>
)}
{!isArchive && canArchive(row?.status?.id) && (
<button
className="dropdown-item py-1"
onClick={() => {
setArchiveJobId(row.id);
setIsArchiveAction(true);
setIsArchiveModalOpen(true);
}}
>
<i className="bx bx-archive bx-sm"></i> Archive
</button>
)}
</div>
<>
<button
className="dropdown-item py-1"
onClick={() =>
setManageJob({ isOpen: true, jobId: row?.id })
}
>
<i className="bx bx-edit bx-sm"></i> Edit
</button>
</>
</div> </div>
</div> </td>
</tr>
))
) : (
<tr style={{ height: "200px" }}>
<td
colSpan={jobGrid.length + 1}
className="text-center border-0 align-middle"
>
{isLoading ? <SpinnerLoader /> : "Not Found Jobs."}
</td> </td>
</tr> </tr>
)) )}
) : ( </tbody>
<tr style={{ height: "200px" }}> </table>
<td </div>
colSpan={jobGrid.length + 1} </>
className="text-center border-0 align-middle"
>
{isLoading ? <SpinnerLoader /> : "Not Found Jobs."}
</td>
</tr>
)}
</tbody>
</table>
</div>
); );
}; };

View File

@ -5,8 +5,8 @@ import { useServiceProjects } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import OffcanvasComponent from "../common/OffcanvasComponent"; import OffcanvasComponent from "../common/OffcanvasComponent";
import showToast from "../../services/toastService"; import showToast from "../../services/toastService";
import ManageJob from "./ManageJob"; import ManageJob from "./ServiceProjectJob/ManageJob";
import ManageJobTicket from "./ManageJobTicket"; import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket";
import GlobalModel from "../common/GlobalModel"; import GlobalModel from "../common/GlobalModel";
import PreviewDocument from "../Expenses/PreviewDocument"; import PreviewDocument from "../Expenses/PreviewDocument";
@ -21,6 +21,8 @@ export const useServiceProjectJobContext = () => {
}; };
const Jobs = () => { const Jobs = () => {
const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null }); const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null });
const [showArchive, setShowArchive] = useState(false);
const [showCanvas, setShowCanvas] = useState(false); const [showCanvas, setShowCanvas] = useState(false);
const [selectedProject, setSelectedProject] = useState(null); const [selectedProject, setSelectedProject] = useState(null);
const [selectJob, setSelectedJob] = useState({ const [selectJob, setSelectedJob] = useState({
@ -58,21 +60,34 @@ const Jobs = () => {
<ManageJob Job={manageJob.jobId} /> <ManageJob Job={manageJob.jobId} />
</OffcanvasComponent> </OffcanvasComponent>
<div className="card page-min-h my-2 px-7 pb-4"> <div className="card page-min-h my-2 px-7 pb-4">
<div className="row"> <div className="row align-items-center py-4">
<div className="col-12 py-2 d-flex justify-content-end ">
<div className="px-2"> {/* LEFT — Tabs */}
<button <div className="col-12 col-md-6 text-start">
className="btn btn-sm btn-primary" <button
onClick={() => setManageJob({ isOpen: true, jobId: null })} type="button"
> className={`btn btn-sm ${showArchive ? "btn-secondary" : "btn-outline-secondary"}`}
<i className="bx bx-plus-circle bx-md me-2"></i>New Job onClick={() => setShowArchive(!showArchive)}
</button> >
</div> <i className="bx bx-archive bx-sm me-1 mt-1"></i> Archived
</button>
</div> </div>
<JobList filterByProject={selectedProject} /> {/* RIGHT — New Job button */}
<div className="col-12 col-md-6 d-flex justify-content-md-end mt-2 mt-md-0">
<button
className="btn btn-sm btn-primary"
onClick={() => setManageJob({ isOpen: true, jobId: null })}
>
<i className="bx bx-plus-circle bx-md me-2"></i>New Job
</button>
</div>
</div> </div>
{/* Job List */}
<JobList filterByProject={selectedProject} isArchive={showArchive} />
</div> </div>
</JonContext.Provider> </JonContext.Provider>
</> </>
); );

View File

@ -192,8 +192,8 @@ const ManageServiceProject = ({ serviceProjectId, onClose }) => {
{...register("statusId")} {...register("statusId")}
> >
<option>Select Service</option> <option>Select Service</option>
{PROJECT_STATUS.map((status) => ( {PROJECT_STATUS?.map((status) => (
<option value={status.id}>{status.label}</option> <option key={status.id} value={status.id}>{status.label}</option>
))} ))}
</select> </select>
{errors?.statusId && ( {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 SelectField from "../../common/Forms/SelectField";
import { useJobStatus } from "../../hooks/masterHook/useMaster"; import Error from "../../common/Error";
import { SpinnerLoader } from "../common/Loader";
import Error from "../common/Error";
import { z } from "zod"; import { z } from "zod";
import { import {
AppFormController, AppFormController,
AppFormProvider, AppFormProvider,
useAppForm, useAppForm,
} from "../../hooks/appHooks/useAppForm"; } from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { closePopup } from "../../slices/localVariablesSlice"; import { closePopup } from "../../../slices/localVariablesSlice";
import { useUpdateServiceProjectJob } from "../../hooks/useServiceProject"; import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject";
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
export const ChangeStatusSchema = z.object({ export const ChangeStatusSchema = z.object({
statusId: z.string().min(1, { message: "Please select status" }), statusId: z.string().min(1, { message: "Please select status" }),
@ -32,7 +31,7 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
} = methods; } = methods;
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => { const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
// handleClose(); handleClose();
}); });
const onSubmit = (formData) => { const onSubmit = (formData) => {
const payload = const payload =
@ -53,6 +52,11 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
}; };
return ( return (
<AppFormProvider {...methods}> <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)}> <form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2"> <div className="mb-2">
<AppFormController <AppFormController

View File

@ -1,16 +1,16 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Avatar from "../common/Avatar"; import Avatar from "../../common/Avatar";
import { useAppForm } from "../../hooks/appHooks/useAppForm"; import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { JobCommentSchema } from "./ServiceProjectSchema"; import { JobCommentSchema } from "../ServiceProjectSchema";
import { import {
useAddCommentJob, useAddCommentJob,
useJobComments, useJobComments,
} from "../../hooks/useServiceProject"; } from "../../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../../utils/constants";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../../utils/dateUtils";
import Filelist from "../Expenses/Filelist"; import Filelist from "../../Expenses/Filelist";
import { formatFileSize, getIconByFileType } from "../../utils/appUtils"; import { formatFileSize, getIconByFileType } from "../../../utils/appUtils";
const JobComments = ({ data }) => { const JobComments = ({ data }) => {
const { const {
@ -150,7 +150,7 @@ const JobComments = ({ data }) => {
type="submit" type="submit"
disabled={!watch("comment")?.trim() || isPending} disabled={!watch("comment")?.trim() || isPending}
> >
Submit Send
</button> </button>
</div> </div>
</form> </form>
@ -161,46 +161,48 @@ const JobComments = ({ data }) => {
const user = item?.createdBy; const user = item?.createdBy;
return ( return (
<div <div className="d-flex align-items-start mt-2 mx-0 px-0">
key={item.id} <Avatar
className="list-group-item border-0 border-bottom p-0" size="xs"
> firstName={user?.firstName}
<div className="d-flex align-items-start mt-2 mx-0 px-0"> lastName={user?.lastName}
<Avatar />
firstName={user?.firstName}
lastName={user?.lastName} <div className="w-100">
/> <div className="d-flex flex-row align-items-center gap-3 w-100">
<div className=""> <span className="fw-semibold">
<div className="d-flex flex-row gap-3"> {user?.firstName} {user?.lastName}
<span className="fw-semibold"> </span>
{user?.firstName} {user?.lastName}
</span> <span className="text-secondary">
<span className="text-secondary"> <em>{formatUTCToLocalTime(item?.createdAt, true)}</em>
<em>{formatUTCToLocalTime(item?.createdAt, true)}</em> </span>
</span>
</div> </div>
<div className="text-muted text-secondary">
{user?.jobRoleName} <div className="text-muted text-secondary">
</div> {user?.jobRoleName}
<div className="text-wrap"> </div>
<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="text-wrap">
{item.attachments?.map((file) => ( <p className="mb-1 mt-2 text-muted">{item.comment}</p>
<div className="d-flex align-items-center">
<i <div className="d-flex flex-wrap jusify-content-end gap-2 gap-sm-6">
className={`bx bx-xxl ${getIconByFileType( {item.attachments?.map((file) => (
file?.contentType <div className="d-flex align-items-center">
)} fs-3`} <i
></i> className={`bx bx-xxl ${getIconByFileType(
<div className="d-flex flex-column"> file?.contentType
<p className="m-0">{file.fileName}</p> )} fs-3`}
<small className="text-secondary"> ></i>
{formatFileSize(file.fileSize)} <div className="d-flex flex-column">
</small> <p className="m-0">{file.fileName}</p>
</div> <small className="text-secondary">
{formatFileSize(file.fileSize)}
</small>
</div> </div>
))} </div>
</div> ))}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -1,25 +1,27 @@
import React, { useEffect } from "react"; import React, { useEffect, useRef } from "react";
import { useServiceProjectJobDetails } from "../../hooks/useServiceProject"; import { useServiceProjectJobDetails } from "../../../hooks/useServiceProject";
import { SpinnerLoader } from "../common/Loader"; import { SpinnerLoader } from "../../common/Loader";
import Error from "../common/Error"; import Error from "../../common/Error";
import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { formatUTCToLocalTime } from "../../../utils/dateUtils";
import Avatar from "../common/Avatar"; import Avatar from "../../common/Avatar";
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; import EmployeeAvatarGroup from "../../common/EmployeeAvatarGroup";
import JobStatusLog from "./JobStatusLog"; import { daysLeft, getJobStatusBadge } from "../../../utils/appUtils";
import JobComments from "./JobComments"; import HoverPopup from "../../common/HoverPopup";
import { daysLeft, getJobStatusBadge } from "../../utils/appUtils";
import HoverPopup from "../common/HoverPopup";
import ChangeStatus from "./ChangeStatus"; import ChangeStatus from "./ChangeStatus";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { STATUS_JOB_CLOSED } from "../../utils/constants"; import { STATUS_JOB_CLOSED } from "../../../utils/constants";
import Tooltip from "../common/Tooltip"; 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 ManageJobTicket = ({ Job }) => {
const { projectId } = useParams(); const { projectId } = useParams();
const { data, isLoading, isError, error } = useServiceProjectJobDetails( const { data, isLoading, isError, error } = useServiceProjectJobDetails(
Job?.job Job?.job
); );
const drawerRef = useRef();
const tabsData = [ const tabsData = [
{ {
id: "comment", id: "comment",
@ -37,7 +39,7 @@ const ManageJobTicket = ({ Job }) => {
}, },
]; ];
if (isLoading) return <SpinnerLoader />; if (isLoading) return <JobDetailsSkeleton />;
if (isError) if (isError)
return ( return (
<div> <div>
@ -45,7 +47,11 @@ const ManageJobTicket = ({ Job }) => {
</div> </div>
); );
return ( return (
<div className="row text-start"> <div
className=" text-start position-relative"
ref={drawerRef}
style={{ overflow: "visible" }}
>
<div className="col-12"> <div className="col-12">
<h6 className="fs-5 fw-semibold">{data?.title}</h6> <h6 className="fs-5 fw-semibold">{data?.title}</h6>
<div className="d-flex justify-content-between align-items-start flex-wrap mb-2"> <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"} {data?.jobTicketUId || "N/A"}
</p> </p>
<div className="d-flex flex-column align-items-end gap-3 mb-3"> <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)}`}> <span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
{data?.status?.displayName} {data?.status?.displayName}
</span> </span>
{STATUS_JOB_CLOSED !== data?.status?.id && ( {STATUS_JOB_CLOSED !== data?.status?.id && (
<HoverPopup <HoverPopup
id="STATUS_CHANEG" id="STATUS_CHANEG"
title="Change Status"
Mode="click" Mode="click"
className="" className=""
align="right"
content={ content={
<ChangeStatus <ChangeStatus
statusId={data?.status?.id} statusId={data?.status?.id}
@ -90,23 +96,33 @@ const ManageJobTicket = ({ Job }) => {
<p>{data?.description || "N/A"}</p> <p>{data?.description || "N/A"}</p>
</div> </div>
<div className="d-flex justify-content-between mb-4"> <div className="d-flex justify-content-between align-items-center mb-4">
<div className="d-flex flex-row gap-1 fw-medium"> <div className="d-flex flex-row gap-1 text-secondry">
<i className="bx bx-calendar"></i>{" "} <i className="bx bx-calendar"></i>{" "}
<span> <span>
Created Date : {formatUTCToLocalTime(data?.createdAt, true)} Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
</span> </span>
</div> </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>
<div className="d-flex justify-content-md-between "> <div className="d-flex justify-content-md-between ">
<div className="d-flex flex-row gap-5"> <div className="d-flex flex-row gap-5">
<span className="fw-medium"> <span className="text-secondry">
<i className="bx bx-calendar"></i> Start Date :{" "} <i className="bx bx-calendar"></i> Start Date :{" "}
{formatUTCToLocalTime(data?.startDate)} {formatUTCToLocalTime(data?.startDate)}
</span>{" "} </span>{" "}
<i className="bx bx-right-arrow-alt"></i>{" "} <i className="bx bx-right-arrow-alt"></i>{" "}
<span className="fw-medium"> <span className="text-secondry">
<i className="bx bx-calendar"></i> Due on :{" "} <i className="bx bx-calendar"></i> Due on :{" "}
{formatUTCToLocalTime(data?.startDate)} {formatUTCToLocalTime(data?.startDate)}
</span> </span>
@ -116,7 +132,7 @@ const ManageJobTicket = ({ Job }) => {
const { days, color } = daysLeft(data?.startDate, data?.dueDate); const { days, color } = daysLeft(data?.startDate, data?.dueDate);
return ( return (
<span> <span>
<span className="fw-medium me-1">Days Left:</span> <span className="text-secondry me-1">Days Left:</span>
<span className={`badge bg-${color}`}> <span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"} {days !== null ? `${days} days` : "N/A"}
</span> </span>
@ -124,61 +140,82 @@ const ManageJobTicket = ({ Job }) => {
); );
})()} })()}
</div> </div>
<div className="d-block mt-4 mb-3"> {data?.projectBranch && (
<div className="row align-items-start align-items-md-start gap-2 mb-1"> <div className="d-flex gap-3 my-2 position-relative" ref={drawerRef} >
<div className="col-12 col-md-auto"> <span className="text-secondary">
<small className="fs-6 fw-medium">Created By</small> <i className="bx bx-buildings"></i> Branch Name:
</div> </span>
<div className="col d-flex flex-row align-items-center ">
<HoverPopup
id="BRANCH_DETAILS"
Mode="click"
align="auto"
boundaryRef={drawerRef}
content={<BranchDetails branch={data?.projectBranch?.id} />}
>
<span className="text-decoration-underline cursor-pointer">
{data?.projectBranch?.branchName}
</span>
</HoverPopup>
</div>
)}
<div className="border-top my-1">
<p className="m-0 py-1">
<i className="bx bx-group"></i> Peoples
</p>
{/* Created By */}
<div className="d-flex justify-content-between align-items-start w-100">
<p className="text-secondary m-0 me-3">Created By</p>
<div className="flex-grow-1 d-flex align-items-center gap-2">
<Avatar <Avatar
size="xs" size="xs"
firstName={data?.createdBy?.firstName} firstName={data?.createdBy?.firstName}
lastName={data?.createdBy?.lastName} lastName={data?.createdBy?.lastName}
/>{" "} />
<div className="d-flex flex-row align-items-center"> <div className="d-flex flex-column">
<p className="m-0">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</p> <p className="m-0 text-truncate">
<small className="text-secondary ms-1"> {data?.createdBy?.firstName} {data?.createdBy?.lastName}
({data?.createdBy?.jobRoleName}) </p>
<small className="text-secondary text-xs">
{data?.createdBy?.jobRoleName}
</small> </small>
</div> </div>
</div> </div>
</div> </div>
{data?.assignees?.length > 0 && ( {/* Assigned To */}
<div className="row align-items-start align-items-md-start gap-2"> <div className="d-flex flex-column flex-md-row align-items-start w-100 mt-2">
<div className="col-12 col-md-auto"> <p className="text-secondary m-0 me-3">Assigned To</p>
<small className="fs-6 fw-medium">Assigned To</small>
</div>
<div className="col"> <div className="flex-grow-1">
<div className="row gap-4"> <div className="d-flex flex-wrap gap-3">
{data?.assignees?.map((emp) => ( {data?.assignees?.map((emp) => (
<div <div key={emp.id} className="d-flex align-items-center">
key={emp.id} <Avatar
className="col-6 col-sm-6 col-md-4 col-lg-4" size="xs"
> firstName={emp.firstName}
<div className="d-flex align-items-center gap-2"> lastName={emp.lastName}
<Avatar />
size="xs"
firstName={emp.firstName}
lastName={emp.lastName}
/>
<div className="d-flex flex-column"> <div className="d-flex flex-column ms-2 text-truncate">
<span className=" text-truncate"> <span className="text-truncate">
{emp.firstName} {emp.lastName} {emp.firstName} {emp.lastName}
</span> </span>
<small className="text-secondary text-truncate"> <small className="text-secondary text-xs text-truncate">
{emp.jobRoleName} {emp.jobRoleName}
</small> </small>
</div>
</div>
</div> </div>
))} </div>
</div> ))}
</div> </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 { formatUTCToLocalTime } from "../../utils/dateUtils";
import ManageServiceProject from "./ManageServiceProject"; import ManageServiceProject from "./ManageServiceProject";
import GlobalModel from "../common/GlobalModel"; import GlobalModel from "../common/GlobalModel";
import { SpinnerLoader } from "../common/Loader";
import ServiceBranch from "./ServiceProjectBranch/ServiceBranch";
import ServiceProfile from "./ServiceProfile";
const ServiceProjectProfile = () => { const ServiceProjectProfile = () => {
const { projectId } = useParams(); const { projectId } = useParams();
const [IsOpenModal, setIsOpenModal] = useState(false); const [IsOpenModal, setIsOpenModal] = useState(false);
const { data, isLoading, isError, error } = useServiceProject(projectId); const { data, isLoading, isError, error } = useServiceProject(projectId);
if (isLoading) { if (isLoading)
return <div className="">Loadng.</div>; return (
} <div className="py-8">
<SpinnerLoader />
</div>
);
return ( return (
<> <>
{IsOpenModal && ( {IsOpenModal && (
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}> <GlobalModel
isOpen={IsOpenModal}
closeModal={() => setIsOpenModal(false)}
>
<ManageServiceProject <ManageServiceProject
serviceProjectId={projectId} serviceProjectId={projectId}
onClose={() => setIsOpenModal(false)} onClose={() => setIsOpenModal(false)}
@ -24,98 +33,13 @@ const ServiceProjectProfile = () => {
)} )}
<div className="row py-2"> <div className="row py-2">
<div className="col-md-6 col-lg-4 order-2 mb-6"> <div className="col-md-6 col-lg-5 order-2 mb-6">
<div className="card mb-4"> <ServiceProfile data={data} setIsOpenModal={setIsOpenModal}/>
<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>
{/* Content section that wraps nicely */}
<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"> {/* 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>
<div className="col-md-6 col-lg-7 order-2 mb-6">
<ServiceBranch />
</div>
</div> </div>
</> </>
); );

View File

@ -50,6 +50,10 @@ export const defaultProjectValues = {
//#endregion //#endregion
//#region JobSchema //#region JobSchema
export const TagSchema = z.object({ export const TagSchema = z.object({
@ -70,6 +74,8 @@ export const jobSchema = z.object({
tags: z.array(TagSchema).optional().default([]), tags: z.array(TagSchema).optional().default([]),
statusId: z.string().optional().nullable(), statusId: z.string().optional().nullable(),
projectBranchId: z.string().optional().nullable(),
}); });
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
@ -109,6 +115,52 @@ export const defaultJobValue = {
startDate: null, startDate: null,
dueDate: null, dueDate: null,
tags: [], tags: [],
branchId: null,
}; };
//#endregion //#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 ManageProject = useHasUserPermission(MANAGE_PROJECT);
const { setMangeProject, setManageServiceProject } = useProjectContext(); 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 handleClose = () => setShowModal(false);
const handleViewProject = () => { const handleViewProject = () => {
@ -43,10 +36,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
navigate(`/service-projects/${project.id}`); navigate(`/service-projects/${project.id}`);
} }
}; };
const handleViewActivities = () => {
dispatch(setProjectId(project.id));
navigate(`/activities/records?project=${project.id}`);
};
const handleManage = () => { const handleManage = () => {
if (isCore) { if (isCore) {
setMangeProject({ setMangeProject({
@ -68,6 +57,8 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
DeleteProject(projectId, false); DeleteProject(projectId, false);
}; };
return ( return (
<> <>
<ConfirmModal <ConfirmModal
@ -98,7 +89,7 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
> >
{project?.shortName ? project?.shortName : project?.name} {project?.shortName ? project?.shortName : project?.name}
</h5> </h5>
<div className="client-info text-body"> <div className="client-info text-body text-start">
<span>{project?.shortName ? project?.name : ""}</span> <span>{project?.shortName ? project?.name : ""}</span>
</div> </div>
</div> </div>
@ -138,14 +129,6 @@ const ServiceProjectCard = ({ project, isCore = true }) => {
<span className="align-left">Modify</span> <span className="align-left">Modify</span>
</a> </a>
</li> </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 && ( {!isCore && (
<li <li
onClick={() => 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>
<div className="col-12 d-flex flex-row gap-2 flex-wrap"> <div className="col-12 d-flex flex-row gap-2 flex-wrap">
{selectedEmployees.map((e) => ( {selectedEmployees.map((e) => (
<EmployeeChip handleRemove={handleRemove} employee={e} /> <EmployeeChip key={`${e.id}-emp`} handleRemove={handleRemove} employee={e} />
))} ))}
</div> </div>
</> </>

View File

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

View File

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

View File

@ -27,7 +27,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
const selectedProject = useSelectedProject(); const selectedProject = useSelectedProject();
const searchDebounce = useDebounce(searchString, 500); const searchDebounce = useDebounce(searchString, 500);
const { data, isLoading, isError, error } = useCollections( const { data, isLoading, isError, error } = useCollections(
selectedProject, selectedProject,
searchDebounce, searchDebounce,
@ -40,7 +40,6 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
); );
const { setProcessedPayment, setAddPayment, setViewCollection } = const { setProcessedPayment, setAddPayment, setViewCollection } =
useCollectionContext(); useCollectionContext();
const paginate = (page) => { const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) { if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page); setCurrentPage(page);
@ -113,6 +112,16 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
), ),
align: "text-center", align: "text-center",
}, },
{
key: "status",
label: "Status",
getValue: (col) => (
<span className={`badge bg-label-${col?.isActive ? "primary" : "danger"}`}>
{col?.isActive ? "Active" : "Inactive"}
</span>
),
align: "text-center",
},
{ {
key: "amount", key: "amount",
label: "Total Amount", label: "Total Amount",
@ -129,6 +138,7 @@ const CollectionList = ({ fromDate, toDate, isPending, searchString }) => {
), ),
align: "text-end", align: "text-end",
}, },
{ {
key: "balance", key: "balance",
label: "Balance", label: "Balance",

View File

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

View File

@ -1,45 +1,44 @@
import React from 'react' import React from "react";
export const EmployeeChip = ({handleRemove,employee}) => { export const EmployeeChip = ({ handleRemove, employee }) => {
return( return (
<span <span
key={employee?.id} key={employee?.id}
className="tagify__tag d-inline-flex align-items-center me-1 mb-1" className="tagify__tag d-inline-flex align-items-center me-1 mb-1"
role="listitem" role="listitem"
> >
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
{employee?.photo ? ( {employee?.photo ? (
<span className="tagify__tag__avatar-wrap me-1"> <span className="tagify__tag__avatar-wrap me-1">
<img <img
src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"} src={employee?.avataremployeerl || "/defaemployeelt-avatar.png"}
alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`} alt={`${employee?.firstName || ""} ${employee?.lastName || ""}`}
style={{ width: 12, height: 12, objectFit: "cover" }} style={{ width: 12, height: 12, objectFit: "cover" }}
/> />
</span> </span>
) : ( ) : (
<div className="avatar avatar-xs me-2"> <div className="avatar avatar-xs me-2">
<span className="avatar-initial roemployeended-circle bg-label-secondary"> <span className="avatar-initial roemployeended-circle bg-label-secondary">
{employee?.firstName?.[0] || ""} {employee?.firstName?.[0] || ""}
{employee?.lastName?.[0] || ""} {employee?.lastName?.[0] || ""}
</span> </span>
</div> </div>
)} )}
<div className="d-flex flex-colemployeemn"> <div className="d-flex flex-colemployeemn">
<span className="tagify__tag-text"> <span className="tagify__tag-text">
{employee?.firstName} {employee?.lastName} {employee?.firstName} {employee?.lastName}
</span> </span>
</div> </div>
</div> </div>
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(employee?.id)}
aria-label={`Remove ${employee?.firstName}`}
title="Remove"
/>
</span>
)
}
<bemployeetton
type="bemployeetton"
className="tagify__tag__removeBtn border-none"
onClick={() => handleRemove(employee?.id)}
aria-label={`Remove ${employee?.firstName}`}
title="Remove"
/>
</span>
);
};

View File

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

View File

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

View File

@ -2,15 +2,18 @@ import React, { useEffect, useRef, useState } from "react";
import Label from "../Label"; import Label from "../Label";
const InputSuggessionField = ({ const InputSuggessionField = ({
organizationList = [], suggesstionList = [],
value, value,
onChange, onChange,
error, error,
disabled=false disabled = false,
label = "Label",
placeholder = "Please Enter",
required = false,
isLoading = false,
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
useEffect(() => { useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
@ -21,12 +24,12 @@ const InputSuggessionField = ({
return () => document.removeEventListener("mousedown", handleClickOutside); 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) => { const handleSelect = (option) => {
onChange(option[valueKey]); onChange(option);
setOpen(false); setOpen(false);
}; };
@ -57,7 +60,7 @@ const InputSuggessionField = ({
{open && !isLoading && ( {open && !isLoading && (
<ul <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={{ style={{
position: "absolute", position: "absolute",
top: "100%", top: "100%",
@ -68,16 +71,14 @@ const InputSuggessionField = ({
overflow: "hidden", overflow: "hidden",
}} }}
> >
{options.map((option, i) => ( {suggesstionList.map((option, i) => (
<li key={i}> <li key={i}>
<button <button
type="button" type="button"
className={`dropdown-item ${ className={`dropdown-item ${option === value ? "active" : ""}`}
option[valueKey] === value ? "active" : ""
}`}
onClick={() => handleSelect(option)} onClick={() => handleSelect(option)}
> >
{option[labelKey]} {option}
</button> </button>
</li> </li>
))} ))}

View File

@ -3,6 +3,7 @@ import Label from "../Label";
import { useDebounce } from "../../../utils/appUtils"; import { useDebounce } from "../../../utils/appUtils";
import { useEmployeesName } from "../../../hooks/useEmployees"; import { useEmployeesName } from "../../../hooks/useEmployees";
import { useProjectBothName } from "../../../hooks/useProjects"; import { useProjectBothName } from "../../../hooks/useProjects";
import EmployeeRepository from "../../../repositories/EmployeeRepository";
const SelectEmployeeServerSide = ({ const SelectEmployeeServerSide = ({
label = "Select", label = "Select",
@ -18,6 +19,7 @@ const SelectEmployeeServerSide = ({
}) => { }) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300); const debounce = useDebounce(searchText, 300);
const [forcedSelected, setForcedSelected] = useState(null);
const { data, isLoading } = useEmployeesName( const { data, isLoading } = useEmployeesName(
projectId, projectId,
@ -34,22 +36,15 @@ const SelectEmployeeServerSide = ({
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim(); return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
}; };
/** -----------------------------
* SELECTED OPTION (SINGLE)
* ----------------------------- */
let selectedSingle = null; let selectedSingle = null;
if (!isMultiple) { if (!isMultiple) {
if (isFullObject && value) selectedSingle = value; if (isFullObject && value) selectedSingle = value;
else if (!isFullObject && 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 = []; let selectedList = [];
if (isMultiple && Array.isArray(value)) { if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value; if (isFullObject) selectedList = value;
else { else {
@ -57,54 +52,61 @@ const SelectEmployeeServerSide = ({
} }
} }
/** Main button label */
const displayText = !isMultiple const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder ? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0 : selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ") ? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder; : placeholder;
/** -----------------------------
* HANDLE OUTSIDE CLICK
* ----------------------------- */
useEffect(() => { useEffect(() => {
const handleClickOutside = (e) => { const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false); setOpen(false);
} }
}; };
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, []); }, []);
/** -----------------------------
* HANDLE SELECT
* ----------------------------- */
const handleSelect = (option) => { const handleSelect = (option) => {
if (!isMultiple) { if (!isMultiple) {
// SINGLE SELECT
if (isFullObject) onChange(option); if (isFullObject) onChange(option);
else onChange(option[valueKey]); else onChange(option[valueKey]);
setOpen(false);
} else { } else {
// MULTIPLE SELECT
let updated = []; let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]); const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
updated = exists
if (exists) { ? selectedList.filter((e) => e[valueKey] !== option[valueKey])
// remove : [...selectedList, option];
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
} else {
// add
updated = [...selectedList, option];
}
if (isFullObject) onChange(updated); if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey])); 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 ( return (
<div className="mb-3 position-relative" ref={dropdownRef}> <div className="mb-3 position-relative" ref={dropdownRef}>
{label && ( {label && (
@ -126,7 +128,6 @@ const SelectEmployeeServerSide = ({
</span> </span>
</button> </button>
{/* DROPDOWN */}
{open && ( {open && (
<ul <ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded" className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
@ -137,10 +138,10 @@ const SelectEmployeeServerSide = ({
zIndex: 1050, zIndex: 1050,
marginTop: "4px", marginTop: "4px",
borderRadius: "0.375rem", borderRadius: "0.375rem",
overflow: "hidden", padding: 0,
}} }}
> >
<div className="p-1"> <li className="p-1 sticky-top bg-white" style={{ zIndex: 10 }}>
<input <input
type="search" type="search"
value={searchText} value={searchText}
@ -148,7 +149,7 @@ const SelectEmployeeServerSide = ({
className="form-control form-control-sm" className="form-control form-control-sm"
placeholder="Search..." placeholder="Search..."
/> />
</div> </li>
{isLoading && ( {isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li> <li className="dropdown-item text-muted text-center">Loading...</li>
@ -168,10 +169,12 @@ const SelectEmployeeServerSide = ({
selectedSingle[valueKey] === option[valueKey]; selectedSingle[valueKey] === option[valueKey];
return ( return (
<li key={option[valueKey]}> <li key={option[valueKey]} className="px-1 rounded">
<button <button
type="button" type="button"
className={`dropdown-item ${isActive ? "active" : ""}`} className={`dropdown-item rounded ${
isActive ? "active" : ""
}`}
onClick={() => handleSelect(option)} onClick={() => handleSelect(option)}
> >
{getDisplayName(option)} {getDisplayName(option)}
@ -184,6 +187,7 @@ const SelectEmployeeServerSide = ({
</div> </div>
); );
}; };
export default SelectEmployeeServerSide; export default SelectEmployeeServerSide;
export const SelectProjectField = ({ export const SelectProjectField = ({
@ -196,6 +200,7 @@ export const SelectProjectField = ({
isFullObject = false, isFullObject = false,
isMultiple = false, isMultiple = false,
isAllProject = false, isAllProject = false,
disabled
}) => { }) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300); const debounce = useDebounce(searchText, 300);
@ -211,9 +216,6 @@ export const SelectProjectField = ({
return `${project.name || ""}`.trim(); return `${project.name || ""}`.trim();
}; };
/** -----------------------------
* SELECTED OPTION (SINGLE)
* ----------------------------- */
let selectedSingle = null; let selectedSingle = null;
if (!isMultiple) { if (!isMultiple) {
@ -222,9 +224,6 @@ export const SelectProjectField = ({
selectedSingle = options.find((o) => o[valueKey] === value); selectedSingle = options.find((o) => o[valueKey] === value);
} }
/** -----------------------------
* SELECTED OPTION (MULTIPLE)
* ----------------------------- */
let selectedList = []; let selectedList = [];
if (isMultiple && Array.isArray(value)) { if (isMultiple && Array.isArray(value)) {
@ -297,6 +296,7 @@ export const SelectProjectField = ({
open ? "show" : "" open ? "show" : ""
}`} }`}
onClick={() => setOpen((prev) => !prev)} onClick={() => setOpen((prev) => !prev)}
disabled={disabled}
> >
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}> <span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText} {displayText}
@ -337,6 +337,217 @@ export const SelectProjectField = ({
</li> </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 && {!isLoading &&
options.map((option) => { options.map((option) => {
const isActive = isMultiple const isActive = isMultiple

View File

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

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 { useDispatch, useSelector } from "react-redux";
import { import {
closePopup, closePopup,
@ -6,6 +6,10 @@ import {
togglePopup, togglePopup,
} from "../../slices/localVariablesSlice"; } from "../../slices/localVariablesSlice";
/**
* align: "auto" | "left" | "right"
* boundaryRef: optional ref to the drawer/container element to use as boundary
*/
const HoverPopup = ({ const HoverPopup = ({
id, id,
title, title,
@ -13,6 +17,8 @@ const HoverPopup = ({
children, children,
className = "", className = "",
Mode = "hover", Mode = "hover",
align = "auto",
boundaryRef = null,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const visible = useSelector((s) => s.localVariables.popups[id] || false); const visible = useSelector((s) => s.localVariables.popups[id] || false);
@ -23,11 +29,9 @@ const HoverPopup = ({
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (Mode === "hover") dispatch(openPopup(id)); if (Mode === "hover") dispatch(openPopup(id));
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (Mode === "hover") dispatch(closePopup(id)); if (Mode === "hover") dispatch(closePopup(id));
}; };
const handleClick = (e) => { const handleClick = (e) => {
if (Mode === "click") { if (Mode === "click") {
e.stopPropagation(); e.stopPropagation();
@ -35,74 +39,159 @@ const HoverPopup = ({
} }
}; };
// Close on outside click when using click mode
useEffect(() => { useEffect(() => {
if (Mode !== "click" || !visible) return; if (Mode !== "click" || !visible) return;
const handleOutside = (e) => { const handler = (e) => {
if ( if (
!popupRef.current?.contains(e.target) && popupRef.current &&
!triggerRef.current?.contains(e.target) !popupRef.current.contains(e.target) &&
triggerRef.current &&
!triggerRef.current.contains(e.target)
) { ) {
dispatch(closePopup(id)); 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(() => { useEffect(() => {
if (!visible || !popupRef.current) return; if (!visible || !popupRef.current || !triggerRef.current) return;
const popup = popupRef.current; // run in next frame so DOM/layout settles
const rect = popup.getBoundingClientRect(); requestAnimationFrame(() => {
const popup = popupRef.current;
popup.style.left = "50%"; // choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement)
popup.style.right = "auto"; const boundaryEl =
popup.style.transform = "translateX(-50%)"; (boundaryRef && boundaryRef.current) || popup.parentElement;
if (!boundaryEl) return;
if (rect.right > window.innerWidth) { const boundaryRect = boundaryEl.getBoundingClientRect();
popup.style.left = "auto"; const triggerRect = triggerRef.current.getBoundingClientRect();
popup.style.right = "0";
popup.style.transform = "none";
}
if (rect.left < 0) { // reset styles first
popup.style.left = "0"; popup.style.left = "";
popup.style.right = "auto"; popup.style.right = "";
popup.style.transform = "none"; popup.style.transform = "";
} popup.style.top = "";
}, [visible]);
return ( const popupRect = popup.getBoundingClientRect();
<div className="d-inline-block position-relative"> const parentRect = boundaryRect; // alias
<div
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
</div>
{visible && ( // Convert trigger center to parent coordinates
<div const triggerCenterX =
ref={popupRef} triggerRect.left + triggerRect.width / 2 - parentRect.left;
className={`bg-white border rounded shadow-sm p-3 w-max position-absolute top-100 mt-2 ${className}`}
style={{
zIndex: 2000,
left: "50%",
transform: "translateX(-50%)",
}}
onClick={(e) => e.stopPropagation()}
>
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
<div>{content}</div> // preferred left so popup center aligns to trigger center:
</div> const preferredLeft = triggerCenterX - popupRect.width / 2;
)}
// Helpers to set styles in parent's coordinate system:
const setLeft = (leftPx) => {
popup.style.left = `${leftPx}px`;
popup.style.right = "auto";
popup.style.transform = "none";
};
const setRight = (rightPx) => {
popup.style.left = "auto";
popup.style.right = `${rightPx}px`;
popup.style.transform = "none";
};
// If user forced align:
if (align === "left") {
// align popup's left to parent's left (0)
setLeft(0);
return;
}
if (align === "right") {
// align popup's right to parent's right (0)
setRight(0);
return;
}
if (align === "center") {
popup.style.left = "50%";
popup.style.right = "auto";
popup.style.transform = "translateX(-50%)";
return;
}
// align === "auto": try preferred centered position, but flip fully if overflow
// clamp preferredLeft to boundaries so it doesn't render partially outside
const leftIfCentered = Math.max(
0,
Math.min(preferredLeft, parentRect.width - popupRect.width)
);
// if centered fits, use it
if (leftIfCentered === preferredLeft) {
setLeft(leftIfCentered);
return;
}
// if centering would overflow right -> stick popup fully to left (left=0)
if (preferredLeft > parentRect.width - popupRect.width) {
// place popup so its right aligns to parent's right
// i.e., left = parent width - popup width
setLeft(parentRect.width - popupRect.width);
return;
}
// if centering would overflow left -> stick popup fully to left=0
if (preferredLeft < 0) {
setLeft(0);
return;
}
// fallback center
setLeft(leftIfCentered);
});
}, [visible, align, boundaryRef]);
return (
<div
className="d-inline-block position-relative" // <-- ADD THIS !!
style={{
maxWidth: "calc(700px - 100px)",
width: "100%",
wordWrap: "break-word",
overflow: "visible", // also make sure popup isn't clipped
}}
>
<div
className="d-inline-block"
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
</div> </div>
);
{visible && (
<div
ref={popupRef}
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
style={{
zIndex: 2000,
top: "100%",
width: "max-content",
minWidth: "120px",
}}
onClick={(e) => e.stopPropagation()}
>
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
<div>{content}</div>
</div>
)}
</div>
);
}; };
export default HoverPopup; export default HoverPopup;

View File

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

View File

@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import GlobalRepository from "../repositories/GlobalRepository"; import GlobalRepository from "../repositories/GlobalRepository";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
export const useDashboard_Data = ({ days, FromDate, projectId }) => { export const useDashboard_Data = ({ days, FromDate, projectId }) => {
const [dashboard_data, setDashboard_Data] = useState([]); const [dashboard_data, setDashboard_Data] = useState([]);
const [isLineChartLoading, setLoading] = useState(false); const [isLineChartLoading, setLoading] = useState(false);
@ -18,11 +17,13 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
try { try {
const payload = { const payload = {
days, days,
FromDate: FromDate || '', FromDate: FromDate || "",
projectId: projectId || null, projectId: projectId || null,
}; };
const response = await GlobalRepository.getDashboardProgressionData(payload); const response = await GlobalRepository.getDashboardProgressionData(
payload
);
setDashboard_Data(response.data); setDashboard_Data(response.data);
} catch (err) { } catch (err) {
setError("Failed to fetch dashboard data."); setError("Failed to fetch dashboard data.");
@ -38,123 +39,6 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
return { dashboard_data, loading: isLineChartLoading, error }; return { dashboard_data, loading: isLineChartLoading, error };
}; };
// export const useDashboard_AttendanceData = (date, projectId) => {
// const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
// const [isLineChartLoading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
// setDashboard_AttendanceData(response.data);
// } catch (err) {
// setError("Failed to fetch dashboard data.");
// console.error(err);
// } finally {
// setLoading(false);
// }
// };
// if (date && projectId !== null) {
// fetchData();
// }
// }, [date, projectId]);
// return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
// };
// 🔹 Dashboard Projects Card Data Hook
// export const useDashboardProjectsCardData = () => {
// const [projectsCardData, setProjectsData] = useState([]);
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchProjectsData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardProjectsCardData();
// setProjectsData(response.data);
// } catch (err) {
// setError("Failed to fetch projects card data.");
// console.error(err);
// } finally {
// setLoading(false);
// }
// };
// fetchProjectsData();
// }, []);
// return { projectsCardData, loading, error };
// };
// 🔹 Dashboard Teams Card Data Hook
// export const useDashboardTeamsCardData = (projectId) => {
// const [teamsCardData, setTeamsData] = useState({});
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchTeamsData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
// setTeamsData(response.data || {});
// } catch (err) {
// setError("Failed to fetch teams card data.");
// console.error("Error fetching teams card data:", err);
// setTeamsData({});
// } finally {
// setLoading(false);
// }
// };
// fetchTeamsData();
// }, [projectId]);
// return { teamsCardData, loading, error };
// };
// export const useDashboardTasksCardData = (projectId) => {
// const [tasksCardData, setTasksData] = useState({});
// const [loading, setLoading] = useState(false);
// const [error, setError] = useState("");
// useEffect(() => {
// const fetchTasksData = async () => {
// setLoading(true);
// setError("");
// try {
// const response = await GlobalRepository.getDashboardTasksCardData(projectId);
// setTasksData(response.data);
// } catch (err) {
// setError("Failed to fetch tasks card data.");
// console.error(err);
// setTasksData({});
// } finally {
// setLoading(false);
// }
// };
// fetchTasksData();
// }, [projectId]);
// return { tasksCardData, loading, error };
// };
export const useAttendanceOverviewData = (projectId, days) => { export const useAttendanceOverviewData = (projectId, days) => {
const [attendanceOverviewData, setAttendanceOverviewData] = useState([]); const [attendanceOverviewData, setAttendanceOverviewData] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -167,7 +51,10 @@ export const useAttendanceOverviewData = (projectId, days) => {
setError(""); setError("");
try { try {
const response = await GlobalRepository.getAttendanceOverview(projectId, days); const response = await GlobalRepository.getAttendanceOverview(
projectId,
days
);
setAttendanceOverviewData(response.data); setAttendanceOverviewData(response.data);
} catch (err) { } catch (err) {
setError("Failed to fetch attendance overview data."); setError("Failed to fetch attendance overview data.");
@ -182,7 +69,6 @@ export const useAttendanceOverviewData = (projectId, days) => {
return { attendanceOverviewData, loading, error }; return { attendanceOverviewData, loading, error };
}; };
// -------------------Query---------------------------- // -------------------Query----------------------------
// export const useDashboard_Data = (days, FromDate, projectId)=>{ // export const useDashboard_Data = (days, FromDate, projectId)=>{
@ -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) => { export const useDashboard_AttendanceData = (date, projectId) => {
return useQuery({ return useQuery({
queryKey: ["dashboardAttendances", date, projectId], queryKey: ["dashboardAttendances", date, projectId],
queryFn: async () => { queryFn: async () => {
const resp = await await GlobalRepository.getDashboardAttendanceData(
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId) date,
projectId
);
return resp.data; return resp.data;
} },
}) });
} };
export const useDashboardTeamsCardData = (projectId) => { export const useDashboardTeamsCardData = (projectId) => {
return useQuery({ return useQuery({
queryKey: ["dashboardTeams", projectId], queryKey: ["dashboardTeams", projectId],
queryFn: async () => { queryFn: async () => {
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId);
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
return resp.data; return resp.data;
} },
}) });
} };
export const useDashboardTasksCardData = (projectId) => { export const useDashboardTasksCardData = (projectId) => {
return useQuery({ return useQuery({
queryKey: ["dashboardTasks", projectId], queryKey: ["dashboardTasks", projectId],
queryFn: async () => { queryFn: async () => {
const resp = await GlobalRepository.getDashboardTasksCardData(projectId);
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
return resp.data; return resp.data;
} },
}) });
} };
// export const useAttendanceOverviewData = (projectId, days) => { // export const useAttendanceOverviewData = (projectId, days) => {
// return useQuery({ // return useQuery({
// queryKey:["dashboardAttendanceOverView",projectId], // queryKey:["dashboardAttendanceOverView",projectId],
@ -247,29 +141,30 @@ export const useDashboardProjectsCardData = () => {
return useQuery({ return useQuery({
queryKey: ["dashboardProjects"], queryKey: ["dashboardProjects"],
queryFn: async () => { queryFn: async () => {
const resp = await GlobalRepository.getDashboardProjectsCardData(); const resp = await GlobalRepository.getDashboardProjectsCardData();
return resp.data; return resp.data;
} },
}) });
} };
export const useExpenseAnalysis = (projectId, startDate, endDate) => { export const useExpenseAnalysis = (projectId, startDate, endDate) => {
const hasBothDates = !!startDate && !!endDate; const hasBothDates = !!startDate && !!endDate;
const noDatesSelected = !startDate && !endDate; const noDatesSelected = !startDate && !endDate;
const shouldFetch = const shouldFetch = noDatesSelected || hasBothDates;
noDatesSelected ||
hasBothDates;
return useQuery({ return useQuery({
queryKey: ["expenseAnalysis", projectId, startDate, endDate], queryKey: ["expenseAnalysis", projectId, startDate, endDate],
queryFn: async () => { queryFn: async () => {
const resp = await GlobalRepository.getExpenseData(projectId, startDate, endDate); const resp = await GlobalRepository.getExpenseData(
projectId,
startDate,
endDate
);
return resp.data; return resp.data;
}, },
enabled: shouldFetch, enabled: shouldFetch,
refetchOnWindowFocus: true, // refetch when you come back refetchOnWindowFocus: true, // refetch when you come back
refetchOnMount: "always", // always refetch on remount refetchOnMount: "always", // always refetch on remount
staleTime: 0, staleTime: 0,
}); });
}; };
@ -280,17 +175,20 @@ export const useExpenseStatus = (projectId) => {
queryFn: async () => { queryFn: async () => {
const resp = await GlobalRepository.getExpenseStatus(projectId); const resp = await GlobalRepository.getExpenseStatus(projectId);
return resp.data; return resp.data;
} },
}) });
} };
export const useExpenseDataByProject = (projectId, categoryId, months) => { export const useExpenseDataByProject = (projectId, categoryId, months) => {
return useQuery({ return useQuery({
queryKey: ["expenseByProject", projectId, categoryId, months], queryKey: ["expenseByProject", projectId, categoryId, months],
queryFn: async () => { queryFn: async () => {
const resp = await GlobalRepository.getExpenseDataByProject(projectId, categoryId, months); const resp = await GlobalRepository.getExpenseDataByProject(
projectId,
categoryId,
months
);
return resp.data; return resp.data;
}, },
}); });
}; };

View File

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

View File

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

View File

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

View File

@ -8,13 +8,14 @@ import { ServiceProjectRepository } from "../repositories/ServiceProjectReposito
import showToast from "../services/toastService"; import showToast from "../services/toastService";
//#region Service Project //#region Service Project
export const useServiceProjects = (pageSize, pageNumber) => { export const useServiceProjects = (pageSize, pageNumber, searchString) => {
return useQuery({ return useQuery({
queryKey: ["serviceProjects", pageSize, pageNumber], queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
queryFn: async () => { queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects( const response = await ServiceProjectRepository.GetServiceProjects(
pageSize, pageSize,
pageNumber pageNumber,
searchString
); );
return response.data; return response.data;
}, },
@ -154,16 +155,25 @@ export const useServiceProjectJobs = (
pageSize, pageSize,
pageNumber, pageNumber,
isActive = true, isActive = true,
project project,
isArchive
) => { ) => {
return useQuery({ return useQuery({
queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project], queryKey: [
"serviceProjectJobs",
pageSize,
pageNumber,
isActive,
project,
isArchive,
],
queryFn: async () => { queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobList( const resp = await ServiceProjectRepository.GetJobList(
pageSize, pageSize,
pageNumber, pageNumber,
isActive, isActive,
project project,
isArchive
); );
return resp.data; return resp.data;
}, },
@ -181,7 +191,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => {
); );
return resp.data; return resp.data;
}, },
enabled:!!jobId, enabled: !!jobId,
initialPageParam: pageNumber, initialPageParam: pageNumber,
@ -258,18 +268,25 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => {
export const useUpdateServiceProjectJob = (onSuccessCallback) => { export const useUpdateServiceProjectJob = (onSuccessCallback) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ id, payload }) => { mutationFn: async ({ id, payload, isArchiveAction = false }) => {
// Call the repository patch
const resp = await ServiceProjectRepository.UpdateJob(id, payload); const resp = await ServiceProjectRepository.UpdateJob(id, payload);
return resp; return { resp, isArchiveAction };
}, },
onSuccess: () => {
onSuccess: ({ isArchiveAction }) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] }); queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
queryClient.invalidateQueries({ queryKey: ["service-job"] }); queryClient.invalidateQueries({ queryKey: ["service-job"] });
if (onSuccessCallback) onSuccessCallback(); if (onSuccessCallback) onSuccessCallback();
showToast("Job Updated successfully", "success");
if (isArchiveAction) {
showToast("Job archived successfully", "success");
} else {
showToast("Job restored successfully", "success");
}
}, },
onError: (error) => { onError: (error) => {
showToast( showToast(
error?.response?.data?.message || error?.response?.data?.message ||
@ -282,3 +299,125 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
}; };
//#endregion //#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 AdvancePaymentList from "../../components/AdvancePayment/AdvancePaymentList";
import { employee } from "../../data/masters"; import { employee } from "../../data/masters";
import { formatFigure } from "../../utils/appUtils"; import { formatFigure } from "../../utils/appUtils";
import { useParams } from "react-router-dom";
import { useExpenseTransactions } from "../../hooks/useExpense";
export const AdvancePaymentContext = createContext(); export const AdvancePaymentContext = createContext();
export const useAdvancePaymentContext = () => { export const useAdvancePaymentContext = () => {
@ -25,14 +27,32 @@ export const useAdvancePaymentContext = () => {
return context; return context;
}; };
const AdvancePaymentPage = () => { 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 [balance, setBalance] = useState(null);
const { control, reset, watch } = useForm({ const { control, reset, watch } = useForm({
defaultValues: { defaultValues: {
employeeId: "", employeeId: employeeId || "",
searchString: "",
}, },
}); });
const selectedEmployeeId = watch("employeeId"); const selectedEmployeeId = employeeId || watch("employeeId");
const searchString = watch("searchString");
useEffect(() => { useEffect(() => {
const selectedEmpoyee = sessionStorage.getItem("transaction-empId"); const selectedEmpoyee = sessionStorage.getItem("transaction-empId");
reset({ reset({
@ -47,30 +67,20 @@ const AdvancePaymentPage = () => {
data={[ data={[
{ label: "Home", link: "/dashboard" }, { label: "Home", link: "/dashboard" },
{ label: "Finance", link: "/advance-payment" }, { 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="card px-4 py-2 page-min-h ">
<div className="row py-1"> <div className="row py-1 justify-content-end">
<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="col-md-8 d-flex align-items-center justify-content-end"> <div className="col-md-8 d-flex align-items-center justify-content-end">
{balance ? ( {balance ? (
<> <>
<label className="fs-5 fw-semibold">Current Balance : </label> <label className="fs-5 fw-semibold">Current Balance : </label>
<span <span
className={`${ className={`${balance > 0 ? "text-success" : "text-danger"
balance > 0 ? "text-success" : "text-danger" } fs-5 fw-bold ms-1`}
} fs-5 fw-bold ms-1`}
> >
{balance > 0 ? ( {balance > 0 ? (
<i className="bx bx-plus b-sm"></i> <i className="bx bx-plus b-sm"></i>
@ -88,7 +98,7 @@ const AdvancePaymentPage = () => {
)} )}
</div> </div>
</div> </div>
<AdvancePaymentList employeeId={selectedEmployeeId} /> <AdvancePaymentList employeeId={selectedEmployeeId} searchString={searchString} />
</div> </div>
</div> </div>
</AdvancePaymentContext.Provider> </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="row w-100">
<div className="col-md-auto d-flex justify-content-between align-items-center"> <div className="col-md-auto d-flex justify-content-between align-items-center">
<img <img
src="/img/brand/ofw-500x500.png" src="/img/brand/marco-250x250.png"
style={{ width: "40px" }} style={{ width: "40px" }}
className="me-2" className="me-2"
></img> ></img>
@ -401,15 +401,15 @@ const LandingPage = () => {
</p> </p>
<h5> Our Mission</h5>{" "} <h5> Our Mission</h5>{" "}
<p> <p>
At <OfwLabel></OfwLabel> At <OfwLabel></OfwLabel>, our mission is to empower organizations
, our mission is to empower organizations to manage their field to manage their field operations effortlessly helping teams stay
operations effortlessly helping teams stay organized, organized, accountable, and productive, no matter where they are.
accountable, and productive, no matter where they are. We aim to We aim to eliminate manual processes, data silos, and
eliminate manual processes, data silos, and communication gaps communication gaps that often slow down projects and increase
that often slow down projects and increase operational costs.{" "} operational costs. <br /> What We Do We provide a comprehensive
<br /> What We Do We provide a comprehensive suite of tools suite of tools designed to handle every critical aspect of field
designed to handle every critical aspect of field management management from workforce tracking to expense control and
from workforce tracking to expense control and reporting. With reporting. With
<OfwLabel></OfwLabel>, you can: <OfwLabel></OfwLabel>, you can:
</p> </p>
<ul> <ul>

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { api } from "../utils/axiosClient"; import { api } from "../utils/axiosClient";
const ProjectRepository = { 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) => getProjectByprojectId: (projetid) =>
api.get(`/api/project/details/${projetid}`), api.get(`/api/project/details/${projetid}`),

View File

@ -1,10 +1,12 @@
import { isAction } from "@reduxjs/toolkit";
import { api } from "../utils/axiosClient"; import { api } from "../utils/axiosClient";
export const ServiceProjectRepository = { export const ServiceProjectRepository = {
//#region Service Project
CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data), CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data),
GetServiceProjects: (pageSize, pageNumber) => GetServiceProjects: (pageSize, pageNumber,searchString) =>
api.get( 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}`), GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`),
UpdateServiceProject: (id, data) => UpdateServiceProject: (id, data) =>
@ -17,12 +19,14 @@ export const ServiceProjectRepository = {
api.get( api.get(
`/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} ` `/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} `
), ),
//#endregion
//#region Job //#region Job
CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data), CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data),
GetJobList: (pageSize, pageNumber, isActive, projectId) => GetJobList: (pageSize, pageNumber, isActive, projectId,isArchive) =>
api.get( 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}`), GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`),
AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data), 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, { api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, {
"Content-Type": "application/json-patch+json", "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 RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage";
import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage"; import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage";
import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail"; 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( const router = createBrowserRouter(
[ [
{ {
@ -116,7 +117,8 @@ const router = createBrowserRouter(
{ path: "/expenses", element: <ExpensePage /> }, { path: "/expenses", element: <ExpensePage /> },
{ path: "/payment-request", element: <PaymentRequestPage /> }, { path: "/payment-request", element: <PaymentRequestPage /> },
{ path: "/recurring-payment", element: <RecurringExpensePage /> }, { 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: "/collection", element: <CollectionPage /> },
{ path: "/masters", element: <MasterPage /> }, { path: "/masters", element: <MasterPage /> },
{ path: "/tenants", element: <TenantPage /> }, { path: "/tenants", element: <TenantPage /> },

View File

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

View File

@ -210,4 +210,35 @@ export const PAYEE_RECURRING_EXPENSE = [
//#region Service Project and Jobs //#region Service Project and Jobs
export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69" export const STATUS_JOB_CLOSED = "3ddeefb5-ae3c-4e10-a922-35e0a452bb69"
//#endregion //#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",
},
];