Compare commits

..

1 Commits

Author SHA1 Message Date
246abee37b Adding Billed To field in Manage Collection. 2025-11-19 11:23:38 +05:30
70 changed files with 1205 additions and 3346 deletions

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

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

View File

@ -1,100 +0,0 @@
import React from 'react'
import Avatar from "../../components/common/Avatar"; // <-- ADD THIS
import { useExpenseAllTransactionsList } from '../../hooks/useExpense';
import { useNavigate } from 'react-router-dom';
import { formatFigure } from '../../utils/appUtils';
const AdvancePaymentList1 = ({ searchString }) => {
const { data, isError, isLoading, error } =
useExpenseAllTransactionsList(searchString);
const rows = data || [];
const navigate = useNavigate();
const columns = [
{
key: "employee",
label: "Employee Name",
align: "text-start",
customRender: (r) => (
<div className="d-flex align-items-center gap-2" onClick={() => navigate(`/advance-payment/${r.id}`)}
style={{ cursor: "pointer" }}>
<Avatar firstName={r.firstName} lastName={r.lastName} />
<span className="fw-medium">
{r.firstName} {r.lastName}
</span>
</div>
),
},
{
key: "jobRoleName",
label: "Job Role",
align: "text-start",
customRender: (r) => (
<span className="fw-semibold">
{r.jobRoleName}
</span>
),
},
{
key: "balanceAmount",
label: "Balance (₹)",
align: "text-end",
customRender: (r) => (
<span className="fw-semibold fs-6">
{formatFigure(r.balanceAmount, {
// type: "currency",
currency: "INR",
})}
</span>
),
},
];
if (isLoading) return <p className="text-center py-4">Loading...</p>;
if (isError) return <p className="text-center py-4 text-danger">{error.message}</p>;
return (
<div className="card-datatable" id="payment-request-table">
<div className="mx-2">
<table className="table border-top dataTable text-nowrap align-middle">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} className={`sorting ${col.align}`}>
{col.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.length > 0 ? (
rows.map((row) => (
<tr key={row.id} className="align-middle" style={{ height: "40px" }}>
{columns.map((col) => (
<td key={col.key} className={`d-table-cell ${col.align} py-3`}>
{col.customRender
? col.customRender(row)
: col.getValue(row)}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center border-0 py-3">
No Employees Found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
export default AdvancePaymentList1;

View File

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

View File

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

View File

@ -50,11 +50,8 @@ const Header = () => {
const isRecurringExpense = /^\/recurring-payment$/.test(pathname);
const isAdvancePayment = /^\/advance-payment$/.test(pathname);
const isServiceProjectPage = /^\/service-projects\/[0-9a-fA-F-]{36}$/.test(pathname);
const isAdvancePayment1 =
/^\/advance-payment(\/[0-9a-fA-F-]{36})?$/.test(pathname);
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage || isAdvancePayment1);
return !(isDirectoryPath || isProfilePage || isExpensePage || isPaymentRequest || isRecurringExpense || isAdvancePayment ||isServiceProjectPage);
};
const allowedProjectStatusIds = [
"603e994b-a27f-4e5d-a251-f3d69b0498ba",

View File

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

View File

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

View File

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

View File

@ -85,7 +85,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter
key: "paymentRequestUID",
label: "Request ID",
align: "text-start mx-2",
getValue: (e) => <div className="d-flex"><span>{e.paymentRequestUID || "N/A"}</span> {e.isAdvancePayment && <span class="ms-1 badge bg-label-warning text-xxs" >Adv</span>}</div>,
getValue: (e) => e.paymentRequestUID || "N/A",
},
{
key: "title",

View File

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

View File

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

View File

@ -8,12 +8,7 @@ const ProjectCardView = ({ data, currentPage, totalPages, paginate }) => {
return (
<div className="row page-min-h">
{data?.length === 0 && (
<div
className="col-12 d-flex justify-content-center align-items-center"
style={{ minHeight: "250px" }}
>
<p className="text-center text-muted m-0">No Infra projects found.</p>
</div>
<p className="text-center text-muted">No projects found.</p>
)}
{data?.map((project) => (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,17 @@
import SelectField from "../../common/Forms/SelectField";
import Error from "../../common/Error";
import SelectField from "../common/Forms/SelectField";
import { useJobStatus } from "../../hooks/masterHook/useMaster";
import { SpinnerLoader } from "../common/Loader";
import Error from "../common/Error";
import { z } from "zod";
import {
AppFormController,
AppFormProvider,
useAppForm,
} from "../../../hooks/appHooks/useAppForm";
} from "../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDispatch, useSelector } from "react-redux";
import { closePopup } from "../../../slices/localVariablesSlice";
import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject";
import { useJobStatus } from "../../../hooks/masterHook/useMaster";
import { closePopup } from "../../slices/localVariablesSlice";
import { useUpdateServiceProjectJob } from "../../hooks/useServiceProject";
export const ChangeStatusSchema = z.object({
statusId: z.string().min(1, { message: "Please select status" }),
@ -31,7 +32,7 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
} = methods;
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
handleClose();
// handleClose();
});
const onSubmit = (formData) => {
const payload =
@ -52,11 +53,6 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
};
return (
<AppFormProvider {...methods}>
<div className="d-flex mb-2">
<span className="fs-6 fw-medium">
Change Status
</span>
</div>
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2">
<AppFormController

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,95 +0,0 @@
import React from 'react'
import { formatUTCToLocalTime } from '../../utils/dateUtils'
const ServiceProfile = ({data,setIsOpenModal}) => {
return (
<div className="card mb-4 h-100">
<div className="card-header text-start">
<h5 className="card-action-title mb-0 ps-1">
{" "}
<i className="fa fa-building rounded-circle text-primary"></i>
<span className="ms-2 fw-bold">Project Profile</span>
</h5>
</div>
<div className="card-body">
<ul className="list-unstyled my-3 ps-0 text-start">
<li className="d-flex mb-3">
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
<i className="bx bx-cog"></i>
<span className="fw-medium mx-2 text-nowrap">Name:</span>
</div>
<div className="flex-grow-1 text-start text-wrap">
{data.name}
</div>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-fingerprint"></i>
<span className="fw-medium mx-2">Nick Name:</span>
</div>
<span>{data.shortName}</span>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-check"></i>
<span className="fw-medium mx-2">Assign Date:</span>
</div>
<span>
{data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"}
</span>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-trophy"></i>
<span className="fw-medium mx-2">Status:</span>
</div>
<span>{data?.status.status}</span>
</li>
<li className="d-flex mb-3">
<div className="d-flex align-items-center" style={{ width: '150px' }}>
<i className="bx bx-user"></i>
<span className="fw-medium mx-2">Contact:</span>
</div>
<span>{data.contactName}</span>
</li>
<li className="d-flex mb-3">
{/* Label section with icon */}
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
<i className="bx bx-flag mt-1"></i>
<span className="fw-medium mx-2 text-nowrap">Address:</span>
</div>
{/* Content section that wraps nicely */}
<div className="flex-grow-1 text-start text-wrap">
{data.address}
</div>
</li>
<li className="d-flex justify-content-center mt-4">
<a className="d-flex justify-content-center mt-4">
<button
type="button"
className="btn btn-sm btn-primary"
data-bs-toggle="modal"
data-bs-target="#edit-project-modal"
onClick={() => setIsOpenModal(true)}
>
Modify Details
</button>
</a>
</li>
</ul>
</div>
</div>
)
}
export default ServiceProfile

View File

@ -1,86 +0,0 @@
import React, { useState } from "react";
import { useBranchDetails } from "../../../hooks/useServiceProject";
import { SpinnerLoader } from "../../common/Loader";
import Error from "../../common/Error";
import { BranchDetailsSkeleton } from "../ServiceProjectSeketon";
const BranchDetails = ({ branch }) => {
const [copied, setCopied] = useState(false);
const { data, isLoading, isError, error } = useBranchDetails(branch);
if (isLoading) return <BranchDetailsSkeleton />;
if (isError) return <Error error={error} />;
let contactInfo = [];
try {
contactInfo = JSON.parse(data?.contactInformation || "[]");
} catch (e) {}
const googleMapUrl = data?.googleMapUrl || data?.locationLink;
const handleCopy = async () => {
if (!googleMapUrl) return;
await navigator.clipboard.writeText(googleMapUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="w-100">
<div className="d-flex mb-2 align-items-center">
<i className="bx bx-buildings bx-sm me-2 text-primary"></i>
<span className="fw-semibold">Branch Details</span>
</div>
<DetailRow label="Branch Name" value={data?.branchName} />
<DetailRow label="Type" value={data?.branchType} />
<DetailRow label="Address" value={data?.address} />
{/* Contact persons */}
{contactInfo.map((person, index) => (
<div key={index} className="mb-2">
<div className="fw-medium text-primary">{person.contactPerson}</div>
<DetailRow label="Role" value={person.designation} />
<DetailRow label="Emails" value={person.contactEmails.join(", ")} />
<DetailRow label="Phone" value={person.contactNumbers.join(", ")} />
</div>
))}
{/* Map Link */}
{googleMapUrl && (
<div className="mt-2">
<a
href={googleMapUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary text-decoration-underline"
>
View on Google Maps
</a>
<button
className="btn btn-xs btn-secondry border ms-2"
onClick={handleCopy}
>
<i className={`bx bx-xs me-1 ${copied ? "bxs-copy-alt":"bx-copy-alt"} `} ></i> {copied ? "Copied!" : "Copy"}
</button>
</div>
)}
</div>
);
};
const DetailRow = ({ label, value }) => (
<div className="d-flex mb-1">
<div className="text-secondary" style={{ width: "90px", flexShrink: 0 }}>
{label}:
</div>
<div className="" style={{ wordBreak: "break-word" }}>
{value || "N/A"}
</div>
</div>
);
export default BranchDetails;

View File

@ -1,358 +0,0 @@
import React, { useEffect } from "react";
import { useProjectName } from "../../../hooks/useProjects";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import Label from "../../common/Label";
import {
useBranchDetails,
useBranchTypes,
useCreateBranch,
useServiceProjects,
useUpdateBranch,
} from "../../../hooks/useServiceProject";
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { useParams } from "react-router-dom";
import { BranchSchema, defaultBranches } from "../ServiceProjectSchema";
import InputSuggessionField from "../../common/Forms/InputSuggesstionField";
import InputSuggestions from "../../common/InputSuggestion";
const ManageBranch = ({ closeModal, BranchToEdit = null }) => {
const {
data,
isLoading,
isError,
error: requestError,
} = useBranchDetails(BranchToEdit);
const { data: branchTypes } = useBranchTypes();
const [contacts, setContacts] = React.useState([
{
contactPerson: "",
designation: "",
contactEmails: [""],
contactNumbers: [""],
},
]);
const { projectId } = useParams();
const schema = BranchSchema();
const {
register,
control,
watch,
handleSubmit,
setValue,
reset,
formState: { errors },
} = useAppForm({
resolver: zodResolver(schema),
defaultValues: {
...defaultBranches,
projectId: projectId || "",
},
});
const handleClose = () => {
reset();
closeModal();
};
useEffect(() => {
if (BranchToEdit && data) {
reset({
branchName: data.branchName || "",
projectId: data.project?.id || projectId || "",
address: data.address || "",
branchType: data.branchType || "",
googleMapUrl: data.googleMapUrl || "",
});
if (data.contactInformation) {
try {
setContacts(JSON.parse(data.contactInformation));
} catch {
setContacts([]);
}
}
}
}, [data, reset]);
const { mutate: CreateServiceBranch, isPending: createPending } =
useCreateBranch(() => {
handleClose();
});
const { mutate: ServiceBranchUpdate, isPending } = useUpdateBranch(() =>
handleClose()
);
const onSubmit = (formdata) => {
let payload = {
...data,
...formdata,
projectId,
contactInformation: JSON.stringify(contacts), // important
};
if (BranchToEdit) {
ServiceBranchUpdate({ id: data.id, payload });
} else {
CreateServiceBranch(payload);
}
};
return (
<div className="container p-3">
<h5 className="m-0">
{BranchToEdit ? "Update Branch" : "Create Branch"}
</h5>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="branchName" className="form-label" required>
Branch Name
</Label>
<input
type="text"
id="branchName"
className="form-control form-control-sm"
{...register("branchName")}
placeholder="Enter Branch"
/>
{errors.branchName && (
<small className="danger-text">{errors.branchName.message}</small>
)}
</div>
<div className="col-md-6">
<Label htmlFor="branchType" className="form-label" required>
Branch Type
</Label>
<InputSuggestions
organizationList={branchTypes}
value={watch("branchType") || ""}
onChange={(val) =>
setValue("branchType", val, { shouldValidate: true })
}
error={errors.branchType?.message}
/>
</div>
</div>
<div className="row my-2 text-start">
<div className="col-md-6">
<Label htmlFor="googleMapUrl" className="form-label">
Google Map URL
</Label>
<input
type="text"
id="googleMapUrl"
className="form-control form-control-sm"
{...register("googleMapUrl")}
/>
{errors.googleMapUrl && (
<small className="danger-text">
{errors.googleMapUrl.message}
</small>
)}
</div>
</div>
<div className="row my-2 text-start">
<div className="col-12">
<Label className="form-label" required>
Contact Persons
</Label>
{contacts.map((item, index) => (
<div key={index} className="border rounded p-2 mb-3">
<div className="d-flex justify-content-end py-1">
{" "}
<div className="col-md-1 d-flex align-items-center">
<i
className="bx bx-trash text-danger cursor-pointer"
onClick={() =>
setContacts(contacts.filter((_, i) => i !== index))
}
></i>
</div>
</div>
<div className="row mb-2">
<div className=" col-md-6">
<Label className="form-label">Contact Name</Label>
<input
type="text"
placeholder="Contact Name"
className="form-control form-control-sm"
value={item.contactPerson}
onChange={(e) => {
const list = [...contacts];
list[index].contactPerson = e.target.value;
setContacts(list);
}}
/>
</div>
<div className="col-md-6">
<Label className="form-label">Designation</Label>
<input
type="text"
placeholder="Designation"
className="form-control form-control-sm"
value={item.designation}
onChange={(e) => {
const list = [...contacts];
list[index].designation = e.target.value;
setContacts(list);
}}
/>
</div>
</div>
{/* Numbers Section */}
<Label className="form-label">Contact Numbers</Label>
{item.contactNumbers.map((num, numIndex) => (
<div
key={numIndex}
className="d-flex gap-2 mb-2 align-items-center"
>
<input
type="text"
placeholder="Number"
className="form-control form-control-sm"
maxLength={10}
value={num}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ""); // remove non-digit characters
const list = [...contacts];
list[index].contactNumbers[numIndex] = value;
setContacts(list);
}}
/>
{/* Show PLUS only on last row */}
{numIndex === item.contactNumbers.length - 1 ? (
<i
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactNumbers.push("");
setContacts(list);
}}
></i>
) : (
<i
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactNumbers.splice(numIndex, 1);
setContacts(list);
}}
></i>
)}
</div>
))}
<hr />
{/* Emails Section */}
<Label className="form-label">Contact Emails</Label>
{item.contactEmails.map((email, emailIndex) => (
<div
key={emailIndex}
className="d-flex gap-2 mb-2 align-items-center"
>
<input
type="email"
placeholder="Email"
className="form-control form-control-sm"
value={email}
onChange={(e) => {
const list = [...contacts];
list[index].contactEmails[emailIndex] = e.target.value;
setContacts(list);
}}
/>
{/* Show PLUS only on the last row */}
{emailIndex === item.contactEmails.length - 1 ? (
<i
className="bx bx-plus-circle text-primary cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactEmails.push("");
setContacts(list);
}}
></i>
) : (
<i
className="bx bx-minus-circle text-danger cursor-pointer fs-5"
onClick={() => {
const list = [...contacts];
list[index].contactEmails.splice(emailIndex, 1);
setContacts(list);
}}
></i>
)}
</div>
))}
</div>
))}
<button
type="button"
className="btn btn-sm btn-primary mt-2"
onClick={() =>
setContacts([
...contacts,
{
contactPerson: "",
designation: "",
contactEmails: [""], // important
contactNumbers: [""], // important
},
])
}
>
<i className="bx bx-plus"></i> Add Contact
</button>
</div>
</div>
<div className="row my-2 text-start">
<div className="col-12">
<Label htmlFor="address" className="form-label" required>
Address
</Label>
<textarea
id="address"
className="form-control form-control-sm"
{...register("address")}
/>
{errors.address && (
<small className="danger-text">{errors.address.message}</small>
)}
</div>
</div>
<div className="d-flex justify-content-end gap-3">
<button
type="reset"
onClick={handleClose}
className="btn btn-label-secondary btn-sm mt-3"
>
Cancel
</button>
<button type="submit" className="btn btn-primary btn-sm mt-3">
{isPending ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</div>
);
};
export default ManageBranch;

View File

@ -1,283 +0,0 @@
import React, { useState } from "react";
import GlobalModel from "../../common/GlobalModel";
import ManageBranch from "./ManageBranch";
import { useBranches, useDeleteBranch } from "../../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../../utils/constants";
import { useDebounce } from "../../../utils/appUtils";
import { useParams } from "react-router-dom";
import Pagination from "../../common/Pagination";
import ConfirmModal from "../../common/ConfirmModal";
import { SpinnerLoader } from "../../common/Loader";
const ServiceBranch = () => {
const { projectId } = useParams();
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [showInactive, setShowInactive] = useState(false);
const [manageState, setManageState] = useState({
IsOpen: false,
branchId: null,
});
const { mutate: DeleteBranch, isPending } = useDeleteBranch();
const [deletingId, setDeletingId] = useState(null);
const [search, setSearch] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const debouncedSearch = useDebounce(search, 500);
const { data, isLoading, isError, error } = useBranches(
projectId,
!showInactive,
ITEMS_PER_PAGE - 12,
currentPage,
debouncedSearch
);
const paginate = (page) => {
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
setCurrentPage(page);
}
};
const columns = [
{
key: "branchName",
label: "Name",
align: "text-start",
getValue: (e) => e?.branchName || "N/A",
},
];
const handleDelete = (id) => {
setDeletingId(id);
DeleteBranch(
{ id, isActive: showInactive },
{
onSettled: () => {
setDeletingId(null);
setIsDeleteModalOpen(false);
},
}
);
};
return (
<>
{IsDeleteModalOpen && (
<ConfirmModal
isOpen={IsDeleteModalOpen}
type={!showInactive ? "delete" : "undo"}
header={!showInactive ? "Delete Branch" : "Restore Branch"}
message={
!showInactive
? "Are you sure you want delete?"
: "Are you sure you want restore?"
}
onSubmit={handleDelete}
onClose={() => setIsDeleteModalOpen(false)}
loading={isPending}
paramData={deletingId}
/>
)}
<div className="card h-100 table-responsive px-sm-4">
<div className="card-datatable" id="payment-request-table">
{/* Header Section */}
<div className="row align-items-center justify-content-between mt-3 mx-1">
<div className="col-md-4 col-sm-12 ms-n3 text-start ">
<h5 className="mb-0">
<i className="bx bx-buildings text-primary"></i>
<span className="ms-2 fw-bold">Branchs</span>
</h5>
</div>
{/* Flex container for toggle + button */}
<div className="col-md-8 col-sm-12 text-end">
<div className="d-flex justify-content-end gap-2">
<div className="form-check form-switch d-inline-flex align-items-center">
<input
type="checkbox"
className="form-check-input mt-1"
id="inactiveEmployeesCheckbox"
checked={showInactive}
onChange={() => setShowInactive(!showInactive)}
/>
<label
htmlFor="inactiveEmployeesCheckbox"
className="ms-2 text-xs"
>
{!showInactive ? "Show Deleted" : "Hide Deleted"}
</label>
</div>
<div className="d-flex justify-content-end">
<button
className="btn btn-sm btn-primary"
type="button"
onClick={() =>
setManageState({
IsOpen: true,
branchId: null,
})
}
>
<i className="bx bx-sm bx-plus-circle me-2"></i>
Add Branch
</button>
</div>
</div>
</div>
</div>
<div className="mx-2 mt-3">
<table className="table border-top text-nowrap align-middle table-borderless">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} className={col.align}>
{col.label}
</th>
))}
<th className="text-center">Action</th>
</tr>
</thead>
<tbody>
{isLoading && (
<tr>
<td
colSpan={columns.length + 1}
className="text-center py-5"
style={{
height: "200px",
verticalAlign: "middle",
}}
>
<div className="d-flex justify-content-center align-items-center w-100 h-100">
<SpinnerLoader />
</div>
</td>
</tr>
)}
{isError && (
<tr>
<td
colSpan={columns.length + 1}
className="text-center text-danger py-5"
>
{error?.message || "Error loading branches"}
</td>
</tr>
)}
{!isLoading &&
!isError &&
data?.data?.length > 0 &&
data.data.map((branch) => (
<tr key={branch.id} style={{ height: "35px" }}>
{columns.map((col) => (
<td key={col.key} className={`${col.align} py-3`}>
{col.getValue(branch)}
</td>
))}
<td className="text-center">
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
>
<i className="bx bx-dots-vertical-rounded text-muted p-0"></i>
</button>
<ul className="dropdown-menu dropdown-menu-end w-auto">
{!showInactive ? (
<>
<li
onClick={() =>
setManageState({
IsOpen: true,
branchId: branch.id,
})
}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit text-primary bx-xs me-2"></i>
Modify
</a>
</li>
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(branch.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-trash text-danger bx-xs me-2"></i>
Delete
</a>
</li>
</>
) : (
<li
onClick={() => {
setIsDeleteModalOpen(true);
setDeletingId(branch.id);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-undo text-danger me-2"></i>
Restore
</a>
</li>
)}
</ul>
</div>
</td>
</tr>
))}
{!isLoading &&
!isError &&
(!data?.data || data.data.length === 0) && (
<tr>
<td
colSpan={columns.length + 1}
className="text-center py-12"
>
No Branch Found
</td>
</tr>
)}
</tbody>
</table>
{data?.data?.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={data.totalPages}
onPageChange={paginate}
/>
)}
</div>
{manageState.IsOpen && (
<GlobalModel
isOpen
size="md"
closeModal={() =>
setManageState({ IsOpen: false, branchId: null })
}
>
<ManageBranch
key={manageState.branchId ?? "new"}
BranchToEdit={manageState.branchId}
closeModal={() =>
setManageState({ IsOpen: false, branchId: null })
}
/>
</GlobalModel>
)}
</div>
</div>
</>
);
};
export default ServiceBranch;

View File

@ -1,80 +0,0 @@
import React from "react";
import { useAppForm } from "../../../hooks/appHooks/useAppForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { JobCommentSchema } from "../ServiceProjectSchema";
import Avatar from "../../common/Avatar";
import Filelist from "../../Expenses/Filelist";
const UpdateJobComment = () => {
const {
register,
handleSubmit,
watch,
reset,
setValue,
formState: { errors },
} = useAppForm({
resolver: zodResolver(JobCommentSchema),
defaultValues: { comment: "", attachments: [] },
});
const onSubmit = () => {};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex">
<Avatar firstName={"A"} lastName={"D"} />
<div className="flex-grow-1">
<textarea
className="form-control"
rows={3}
placeholder="Write your comment..."
{...register("comment")}
></textarea>
{errors?.comment && (
<small className="danger-text">{errors?.comment?.message}</small>
)}
</div>
</div>
{/* <div className="flex-grow-1 ms-10 mt-2">
{files?.length > 0 && (
<Filelist files={} removeFile={removeFile} />
)}
</div> */}
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
<div
onClick={() => document.getElementById("attachments").click()}
className="cursor-pointer"
style={{ whiteSpace: "nowrap" }}
>
<input
type="file"
accept=".pdf,.jpg,.jpeg,.png"
id="attachments"
multiple
className="d-none"
{...register("attachments")}
onChange={(e) => {
onFileChange(e);
e.target.value = "";
}}
/>
<i className="bx bx-sm bx-paperclip mb-1 me-1"></i>
Add Attachment
</div>
<button
className="btn btn-primary btn-sm px-1 py-1"
type="submit"
disabled={!watch("comment")?.trim() || isPending}
>
Submit
</button>
</div>
</form>
</div>
);
};
export default UpdateJobComment;

View File

@ -4,27 +4,18 @@ import { useServiceProject } from "../../hooks/useServiceProject";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
import ManageServiceProject from "./ManageServiceProject";
import GlobalModel from "../common/GlobalModel";
import { SpinnerLoader } from "../common/Loader";
import ServiceBranch from "./ServiceProjectBranch/ServiceBranch";
import ServiceProfile from "./ServiceProfile";
const ServiceProjectProfile = () => {
const { projectId } = useParams();
const [IsOpenModal, setIsOpenModal] = useState(false);
const { data, isLoading, isError, error } = useServiceProject(projectId);
if (isLoading)
return (
<div className="py-8">
<SpinnerLoader />
</div>
);
if (isLoading) {
return <div className="">Loadng.</div>;
}
return (
<>
{IsOpenModal && (
<GlobalModel
isOpen={IsOpenModal}
closeModal={() => setIsOpenModal(false)}
>
<GlobalModel isOpen={IsOpenModal} closeModal={() => setIsOpenModal(false)}>
<ManageServiceProject
serviceProjectId={projectId}
onClose={() => setIsOpenModal(false)}
@ -33,13 +24,98 @@ const ServiceProjectProfile = () => {
)}
<div className="row py-2">
<div className="col-md-6 col-lg-5 order-2 mb-6">
<ServiceProfile data={data} setIsOpenModal={setIsOpenModal}/>
<div className="col-md-6 col-lg-4 order-2 mb-6">
<div className="card mb-4">
<div className="card-header text-start">
<h5 className="card-action-title mb-0 ps-1">
{" "}
<i className="fa fa-building rounded-circle text-primary"></i>
<span className="ms-2 fw-bold">Project Profile</span>
</h5>
</div>
<div className="card-body">
<ul className="list-unstyled my-3 ps-0 text-start">
<li className="d-flex mb-3">
<div className="d-flex align-items-start" style={{ minWidth: "150px" }}>
<i className="bx bx-cog"></i>
<span className="fw-medium mx-2 text-nowrap">Name:</span>
</div>
{/* 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 className="col-md-6 col-lg-7 order-2 mb-6">
<ServiceBranch />
</div>
</div>
</>
);

View File

@ -50,10 +50,6 @@ export const defaultProjectValues = {
//#endregion
//#region JobSchema
export const TagSchema = z.object({
@ -74,8 +70,6 @@ export const jobSchema = z.object({
tags: z.array(TagSchema).optional().default([]),
statusId: z.string().optional().nullable(),
projectBranchId: z.string().optional().nullable(),
});
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
@ -115,52 +109,6 @@ export const defaultJobValue = {
startDate: null,
dueDate: null,
tags: [],
branchId: null,
};
//#endregion
//#region Branch
export const BranchSchema = () =>
z.object({
projectId: z
.string()
.trim()
.min(1, { message: "Project is required" }),
branchName: z
.string()
.trim()
.min(1, { message: "Branch Name is required" }),
contactInformation: z.string().optional(),
address: z
.string()
.trim()
.min(1, { message: "Address is required" }),
branchType: z
.string()
.trim()
.min(1, { message: "Branch Type is required" }),
googleMapUrl: z
.string()
});
export const defaultBranches = {
branchName: "",
projectId: "",
contactInformation: "",
address: "",
branchType: "",
googleMapUrl: "",
};
//#endregion

View File

@ -1,138 +0,0 @@
import React from "react";
const SkeletonLine = ({ height = 18, width = "100%", className = "" }) => (
<div
className={`skeleton ${className}`}
style={{
height,
width,
borderRadius: "4px",
}}
></div>
);
export const BranchDetailsSkeleton = () => {
return (
<div className="w-100">
<div className="d-flex mb-3">
<SkeletonLine height={22} width="280px" />
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8">
<SkeletonLine height={16} width="90%" />
</div>
</div>
<div className="row mb-2">
<div className="col-4">
<SkeletonLine height={16} width="70%" />
</div>
<div className="col-8 d-flex gap-2 align-items-center">
<SkeletonLine height={16} width="60%" />
<SkeletonLine height={16} width="20px" />
</div>
</div>
</div>
);
};
export const JobDetailsSkeleton = () => {
return (
<div className="row text-start">
<div className="col-12">
{/* Title */}
<SkeletonLine height={24} width="50%" />
{/* Job ID + Status */}
<div className="d-flex justify-content-between align-items-start flex-wrap mb-3 mt-2">
<SkeletonLine height={18} width="30%" />
<div className="d-flex flex-row gap-2">
<SkeletonLine height={22} width="70px" />
<SkeletonLine height={22} width="22px" />
</div>
</div>
{/* Description */}
<SkeletonLine height={40} width="100%" />
{/* Created Date */}
<div className="d-flex my-3">
<SkeletonLine height={16} width="40%" />
</div>
{/* Start / Due Date */}
<div className="d-flex justify-content-between mb-4">
<SkeletonLine height={16} width="50%" />
<SkeletonLine height={22} width="70px" />
</div>
{/* Branch Name */}
<div className="d-flex flex-row gap-3 my-2">
<SkeletonLine height={16} width="30%" />
<SkeletonLine height={16} width="40%" />
</div>
{/* Created By */}
<div className="row align-items-center my-3">
<div className="col-12 col-md-auto mb-2">
<SkeletonLine height={16} width="80px" />
</div>
<div className="col d-flex align-items-center gap-2">
<SkeletonLine height={30} width="30px" /> {/* Avatar */}
<SkeletonLine height={16} width="40%" />
</div>
</div>
{/* Assigned To */}
<div className="row mt-2">
<div className="col-12 col-md-auto mb-2">
<SkeletonLine height={16} width="90px" />
</div>
</div>
{/* Tabs */}
<div className="mt-4">
<div className="d-flex gap-3 mb-3">
<SkeletonLine height={35} width="80px" />
<SkeletonLine height={35} width="80px" />
<SkeletonLine height={35} width="80px" />
</div>
<SkeletonLine height={150} width="100%" />
</div>
</div>
</div>
);
};

View File

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

View File

@ -1,209 +0,0 @@
import React, { useState } from "react";
import { MANAGE_PROJECT, PROJECT_STATUS } from "../../../utils/constants";
import { useProjects } from "../../../hooks/useProjects";
import { formatNumber, formatUTCToLocalTime } from "../../../utils/dateUtils";
import ProgressBar from "../../common/ProgressBar";
import {
getProjectStatusColor,
getProjectStatusName,
} from "../../../utils/projectStatus";
import { useDispatch } from "react-redux";
import { setProjectId } from "../../../slices/localVariablesSlice";
import { useNavigate } from "react-router-dom";
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
import { useProjectContext } from "../../../pages/project/ProjectPage";
import usePagination from "../../../hooks/usePagination";
import Pagination from "../../common/Pagination";
const ServiceProjectList = ({
data,
currentPage,
totalPages,
paginate,
isCore = true,
}) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { setMangeProject, setManageServiceProject } = useProjectContext();
const handleClose = () => setShowModal(false);
// check Permissions
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
const projectColumns = [
{
key: "projectName",
label: "Project Name",
className: "text-start py-3",
getValue: (p) => (
<div
className="text-primary cursor-pointer fw-bold py-3"
onClick={() => {
dispatch(setProjectId(p.id));
navigate(`/service-projects/${p.id}`);
}}
>
{p.shortName ? `${p.name} (${p.shortName})` : p.name}
</div>
),
},
{
key: "client.contactPerson",
label: "Contact Person",
className: "text-start small",
getValue: (p) => p.client?.contactPerson || "N/A",
},
{
key: "assignedDate",
label: "Assign Date",
className: "text-center small",
getValue: (p) => formatUTCToLocalTime(p.assignedDate),
},
{
key: "status",
label: "Status",
className: "text-center small",
getValue: (p) => (
<span className={`badge ${getProjectStatusColor(p.status?.id)}`}>
{p.status?.status}
</span>
),
},
];
const handleViewProject = (p) => {
if (isCore) {
dispatch(setProjectId(p.id));
navigate(`/projects/details`);
} else {
navigate(`/service-projects/${p.id}`);
}
};
const handleManage = (p) => {
if (isCore) {
setMangeProject({
isOpen: true,
Project: p.id,
});
} else {
setManageServiceProject({
isOpen: true,
project: p.id,
});
}
};
return (
<div>
<div className="card page-min-h py-4 px-6 shadow-sm">
<div className="table-responsive text-nowrap page-min-h">
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
{projectColumns.map((col) => (
<th
key={col.key}
colSpan={col.colSpan}
className={`${col.className} table_header_border`}
>
{col.label}
</th>
))}
<th className="text-center py-3">Action</th>
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map((project) => (
<tr key={project.id}>
{projectColumns.map((col) => (
<td
key={col.key}
colSpan={col.colSpan}
className={`${col.className} py-5`}
style={{ paddingTop: "20px", paddingBottom: "20px" }}
>
{col.getValue
? col.getValue(project)
: project[col.key] || "N/A"}
</td>
))}
<td
className={`mx-2 ${
canManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
<div className="dropdown z-2">
<button
type="button"
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded bx-sm text-muted"
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip-dark"
title="More Action"
></i>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<a
aria-label="click to View details"
className="dropdown-item"
onClick={() => handleViewProject(project)}
>
<i className="bx bx-detail me-2"></i>
<span className="align-left">View details</span>
</a>
</li>
<li>
<a
className="dropdown-item"
onClick={() => handleManage(project)}
>
<i className="bx bx-pencil me-2"></i>
<span className="align-left">Modify</span>
</a>
</li>
</ul>
</div>
</td>
</tr>
))
) : (
<tr
className="no-hover"
style={{
pointerEvents: "none",
backgroundColor: "transparent",
}}
>
<td
colSpan={projectColumns.length + 1}
className="text-center align-middle"
style={{ height: "300px", borderBottom: "none" }}
>
No Service projects available
</td>
</tr>
)}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
paginate={paginate}
/>
</div>
</div>
);
};
export default ServiceProjectList;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import Label from "../Label";
import { useDebounce } from "../../../utils/appUtils";
import { useEmployeesName } from "../../../hooks/useEmployees";
import { useProjectBothName } from "../../../hooks/useProjects";
import EmployeeRepository from "../../../repositories/EmployeeRepository";
const SelectEmployeeServerSide = ({
label = "Select",
@ -19,7 +18,6 @@ const SelectEmployeeServerSide = ({
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const [forcedSelected, setForcedSelected] = useState(null);
const { data, isLoading } = useEmployeesName(
projectId,
@ -36,361 +34,6 @@ const SelectEmployeeServerSide = ({
return `${emp.firstName || ""} ${emp.lastName || ""}`.trim();
};
let selectedSingle = null;
if (!isMultiple) {
if (isFullObject && value) selectedSingle = value;
else if (!isFullObject && value)
selectedSingle =
options.find((o) => o[valueKey] === value) || forcedSelected;
}
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
}
}
const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder;
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleSelect = (option) => {
if (!isMultiple) {
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
setOpen(false);
} else {
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
updated = exists
? selectedList.filter((e) => e[valueKey] !== option[valueKey])
: [...selectedList, option];
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
useEffect(() => {
if (!value || isFullObject) return;
const exists = options.some((o) => o[valueKey] === value);
if (exists) return;
const loadSingleEmployee = async () => {
try {
const emp = await EmployeeRepository.getEmployeeName(
null,
null,
true,
value
);
setForcedSelected(emp.data[0]);
} catch (err) {
console.error("Failed to load selected employee", err);
}
};
loadSingleEmployee();
}, [value, options, isFullObject, valueKey]);
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
padding: 0,
}}
>
<li className="p-1 sticky-top bg-white" style={{ zIndex: 10 }}>
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
/>
</li>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]} className="px-1 rounded">
<button
type="button"
className={`dropdown-item rounded ${
isActive ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export default SelectEmployeeServerSide;
export const SelectProjectField = ({
label = "Select",
placeholder = "Select Project",
required = false,
value = null,
onChange,
valueKey = "id",
isFullObject = false,
isMultiple = false,
isAllProject = false,
disabled
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useProjectBothName(debounce);
const options = data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (project) => {
if (!project) return "";
return `${project.name || ""}`.trim();
};
let selectedSingle = null;
if (!isMultiple) {
if (isFullObject && value) selectedSingle = value;
else if (!isFullObject && value)
selectedSingle = options.find((o) => o[valueKey] === value);
}
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
}
}
/** Main button label */
const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder;
/** -----------------------------
* HANDLE OUTSIDE CLICK
* ----------------------------- */
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
/** -----------------------------
* HANDLE SELECT
* ----------------------------- */
const handleSelect = (option) => {
if (!isMultiple) {
// SINGLE SELECT
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
} else {
// MULTIPLE SELECT
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
if (exists) {
// remove
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
} else {
// add
updated = [...selectedList, option];
}
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
disabled={disabled}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "2px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
<div className="p-1">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
/>
</div>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]} className="px-1 rounded w-full">
<button
type="button"
className={`dropdown-item rounded d-block text-truncate w-100 ${
isActive ? "active" : ""
}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export const SelectFieldSearch = ({
label = "Select",
placeholder = "Select ",
required = false,
value = null,
onChange,
valueKey = "id",
labelKey = "name",
disabled = false,
isFullObject = false,
isMultiple = false,
hookParams,
useFetchHook,
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useFetchHook(...hookParams, debounce);
const options = data?.data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (entity) => {
if (!entity) return "";
return `${entity[labelKey] || ""}`.trim();
};
/** -----------------------------
* SELECTED OPTION (SINGLE)
* ----------------------------- */
@ -406,6 +49,7 @@ export const SelectFieldSearch = ({
* SELECTED OPTION (MULTIPLE)
* ----------------------------- */
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
@ -434,36 +78,6 @@ export const SelectFieldSearch = ({
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
* ----------------------------- */
@ -502,10 +116,9 @@ export const SelectFieldSearch = ({
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
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" : ""}`}>
@ -516,13 +129,190 @@ export const SelectFieldSearch = ({
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "2px",
marginTop: "4px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
<div className="p-1">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
/>
</div>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">
No results found
</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]}>
<button
type="button"
className={`dropdown-item ${isActive ? "active" : ""}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export default SelectEmployeeServerSide;
export const SelectProjectField = ({
label = "Select",
placeholder = "Select Project",
required = false,
value = null,
onChange,
valueKey = "id",
isFullObject = false,
isMultiple = false,
isAllProject = false,
}) => {
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useProjectBothName(debounce);
const options = data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (project) => {
if (!project) return "";
return `${project.name || ""}`.trim();
};
/** -----------------------------
* 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);
}, []);
/** -----------------------------
* HANDLE SELECT
* ----------------------------- */
const handleSelect = (option) => {
if (!isMultiple) {
// SINGLE SELECT
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
} else {
// MULTIPLE SELECT
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
if (exists) {
// remove
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
} else {
// add
updated = [...selectedList, option];
}
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "2px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
@ -534,7 +324,6 @@ export const SelectFieldSearch = ({
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
disabled={disabled}
/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,14 +8,13 @@ import { ServiceProjectRepository } from "../repositories/ServiceProjectReposito
import showToast from "../services/toastService";
//#region Service Project
export const useServiceProjects = (pageSize, pageNumber, searchString) => {
export const useServiceProjects = (pageSize, pageNumber) => {
return useQuery({
queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
queryKey: ["serviceProjects", pageSize, pageNumber],
queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects(
pageSize,
pageNumber,
searchString
pageNumber
);
return response.data;
},
@ -155,25 +154,16 @@ export const useServiceProjectJobs = (
pageSize,
pageNumber,
isActive = true,
project,
isArchive
project
) => {
return useQuery({
queryKey: [
"serviceProjectJobs",
pageSize,
pageNumber,
isActive,
project,
isArchive,
],
queryKey: ["serviceProjectJobs", pageSize, pageNumber, isActive, project],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetJobList(
pageSize,
pageNumber,
isActive,
project,
isArchive
project
);
return resp.data;
},
@ -191,7 +181,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => {
);
return resp.data;
},
enabled: !!jobId,
enabled:!!jobId,
initialPageParam: pageNumber,
@ -268,25 +258,18 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => {
export const useUpdateServiceProjectJob = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload, isArchiveAction = false }) => {
mutationFn: async ({ id, payload }) => {
// Call the repository patch
const resp = await ServiceProjectRepository.UpdateJob(id, payload);
return { resp, isArchiveAction };
return resp;
},
onSuccess: ({ isArchiveAction }) => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] });
queryClient.invalidateQueries({ queryKey: ["service-job"] });
if (onSuccessCallback) onSuccessCallback();
if (isArchiveAction) {
showToast("Job archived successfully", "success");
} else {
showToast("Job restored successfully", "success");
}
showToast("Job Updated successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
@ -299,125 +282,3 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => {
};
//#endregion
//#region Branch
export const useBranches = (
projectId,
isActive,
pageSize,
pageNumber,
searchString
) => {
return useQuery({
queryKey: [
"branches",
projectId,
isActive,
pageSize,
pageNumber,
searchString,
],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetBranchList(
projectId,
isActive,
pageSize,
pageNumber,
searchString
);
return resp.data;
},
enabled: !!projectId,
});
};
export const useBranchTypes = () => {
return useQuery({
queryKey: ["branch_Type"],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetBranchTypeList();
return resp.data;
},
});
};
export const useBranchDetails = (id) => {
return useQuery({
queryKey: ["branch", id],
queryFn: async () => {
const resp = await ServiceProjectRepository.GetBranchDetail(id);
return resp.data;
},
enabled: !!id,
});
};
export const useCreateBranch = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload) => {
await ServiceProjectRepository.CreateBranch(payload);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast("Branches Created Successfully", "success");
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(
error.message || "Something went wrong please try again !",
"error"
);
},
});
};
export const useUpdateBranch = (onSuccessCallBack) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) =>
await ServiceProjectRepository.UpdateBranch(id, payload),
onSuccess: (_, variables) => {
// remove old single-branch cache
queryClient.removeQueries({ queryKey: ["branch", variables.id] });
// refresh list
queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast("Branch updated successfully", "success");
onSuccessCallBack?.();
},
onError: () => {
showToast("Something went wrong. Please try again later.", "error");
},
});
};
export const useDeleteBranch = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, isActive }) =>
await ServiceProjectRepository.DeleteBranch(id, isActive),
onSuccess: (_, variable) => {
queryClient.invalidateQueries({ queryKey: ["branches"] });
showToast(
`Branch ${variable.isActive ? "restored" : "deleted"} successfully`,
"success"
);
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to delete branch",
"error"
);
},
});
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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