Merge branch 'Project_Branch_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Weidget_Dashoard_Jobs

This commit is contained in:
Kartik Sharma 2025-11-24 17:10:32 +05:30
commit 1eeff79cf7
27 changed files with 623 additions and 320 deletions

View File

@ -32,8 +32,11 @@ import Label from "../common/Label";
import EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist";
import { DEFAULT_CURRENCY } from "../../utils/constants";
import SelectEmployeeServerSide, { SelectProjectField } from "../common/Forms/SelectFieldServerSide";
import SelectEmployeeServerSide, {
SelectProjectField,
} from "../common/Forms/SelectFieldServerSide";
import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject";
import { AppFormController } from "../../hooks/appHooks/useAppForm";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const {
@ -153,15 +156,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
}
};
const { mutate: AllocationTeam, isPending1 } = useAllocationServiceProjectTeam(
() => {
const { mutate: AllocationTeam, isPending1 } =
useAllocationServiceProjectTeam(() => {
setSelectedEmployees([]);
setSeletingEmp({
employee: null,
isOpen: false,
});
}
);
});
useEffect(() => {
if (expenseToEdit && data) {
reset({
@ -264,7 +266,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
required
placeholder="Select Project"
value={watch("projectId")}
onChange={(val) =>
setValue("projectId", val, {
shouldDirty: true,
@ -337,14 +338,28 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
)}
</div>
<div className="col-12 col-md-6 text-start">
<Label className="form-label" required>
{/* <Label className="form-label" required>
Paid By{" "}
</Label>
<EmployeeSearchInput
</Label> */}
{/* <EmployeeSearchInput
control={control}
name="paidById"
projectId={null}
forAll={true}
/> */}
<AppFormController
name="paidById"
control={control}
render={({ field }) => (
<SelectEmployeeServerSide
label="Paid By" required
value={field.value}
onChange={field.onChange}
isFullObject={false} // because using ID
/>
)}
/>
</div>
</div>
@ -453,7 +468,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.gstNumber.message}</small>
)}
</div>
</div>
<div className="row">
<div className="col-md-6 text-start ">
@ -484,7 +498,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div>
{expenseCategory?.noOfPersonsRequired && (
<div className="col-md-6 text-start">
<Label className="form-label" required>No. of Persons</Label>
<Label className="form-label" required>
No. of Persons
</Label>
<input
type="number"
id="noOfPersons"

View File

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

View File

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

View File

@ -126,7 +126,7 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
return (
<div className="card page-min-h py-4 px-6 shadow-sm">
<div className="table-responsive text-nowrap page-min-h">
<div className="table-responsive text-nowrap">
<table className="table table-hover align-middle m-0">
<thead className="border-bottom ">
<tr>
@ -143,7 +143,8 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</tr>
</thead>
<tbody>
{data?.map((project) => (
{data?.length > 0 ? (
data?.map((project) => (
<tr key={project.id}>
{projectColumns.map((col) => (
<td
@ -158,8 +159,7 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</td>
))}
<td
className={`mx-2 ${
canManageProject ? "d-sm-table-cell" : "d-none"
className={`mx-2 ${canManageProject ? "d-sm-table-cell" : "d-none"
}`}
>
<div className="dropdown z-2">
@ -213,7 +213,24 @@ const ProjectListView = ({ data, currentPage, totalPages, paginate }) => {
</div>
</td>
</tr>
))}
))
) : (
<tr
className="no-hover"
style={{
pointerEvents: "none",
backgroundColor: "transparent",
}}
>
<td
colSpan={projectColumns.length + 1}
className="text-center align-middle"
style={{ height: "300px", borderBottom: "none" }}
>
No Infra projects available
</td>
</tr>
)}
</tbody>
</table>
</div>

View File

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

View File

@ -130,7 +130,7 @@ const JobList = ({ isArchive }) => {
{isArchiveModalOpen && (
<ConfirmModal
isOpen={isArchiveModalOpen}
type={isArchiveAction ? "success" : "undo"}
type={isArchiveAction ? "archive" : "Un-archive"}
header={isArchiveAction ? "Archive Job" : "Restore Job"}
message={
isArchiveAction

View File

@ -72,11 +72,11 @@ const Jobs = () => {
>
{showArchive ? (
<>
<i className="bx bx-list-ul me-1 mt-1"></i> Show Active Jobs
<i className="bx bx-list-ul me-1 mt-1"></i> Show Active
</>
) : (
<>
<i className="bx bx-archive me-1 mt-1"></i> Show Archived Jobs
<i className="bx bx-archive me-1 mt-1"></i> Show Archived
</>
)}
</button>

View File

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

View File

@ -103,7 +103,7 @@ const ServiceBranch = () => {
htmlFor="inactiveEmployeesCheckbox"
className="ms-2 text-xs"
>
Show Deleted Branches
{!showInactive ? "Show Deleted" : "Hide Deleted"}
</label>
</div>
<div className="d-flex justify-content-end">

View File

@ -31,7 +31,7 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => {
} = methods;
const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => {
// handleClose();
handleClose();
});
const onSubmit = (formData) => {
const payload =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@ const InputSuggessionField = ({
}) => {
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
console.log(suggesstionList)
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
@ -61,7 +60,7 @@ console.log(suggesstionList)
{open && !isLoading && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn"
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded overflow-x-hidden"
style={{
position: "absolute",
top: "100%",

View File

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

View File

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

View File

@ -20,14 +20,15 @@ export const useCurrentService = () => {
// ------------------------------Query-------------------
export const useProjects = (pageSize, pageNumber) => {
export const useProjects = (pageSize, pageNumber,searchString) => {
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
return useQuery({
queryKey: ["ProjectsList", pageSize, pageNumber],
queryKey: ["ProjectsList", pageSize, pageNumber,searchString],
queryFn: async () => {
const response = await ProjectRepository.getProjectList(
pageSize,
pageNumber
pageNumber,
searchString,
);
return response?.data;
},

View File

@ -8,13 +8,14 @@ import { ServiceProjectRepository } from "../repositories/ServiceProjectReposito
import showToast from "../services/toastService";
//#region Service Project
export const useServiceProjects = (pageSize, pageNumber) => {
export const useServiceProjects = (pageSize, pageNumber, searchString) => {
return useQuery({
queryKey: ["serviceProjects", pageSize, pageNumber],
queryKey: ["serviceProjects", pageSize, pageNumber, searchString],
queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects(
pageSize,
pageNumber
pageNumber,
searchString
);
return response.data;
},

View File

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

View File

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

View File

@ -41,10 +41,11 @@ const ProjectPage = () => {
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 [selectedStatuses, setSelectedStatuses] = useState(
PROJECT_STATUS.map((s) => s.id)
@ -64,13 +65,11 @@ const ProjectPage = () => {
manageServiceProject,
};
const handleToggleProject = (value) => {
setCoreProjects(value);
sessionStorage.setItem("whichProjectDisplay", String(value));
};
return (
<ProjectContext.Provider value={contextDispatcher}>
<div className="container-fluid">
@ -90,7 +89,8 @@ const ProjectPage = () => {
{/* Service Project Button */}
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${!coreProjects ? "btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${
!coreProjects ? "btn-primary text-white" : ""
}`}
onClick={() => handleToggleProject(false)}
>
@ -99,20 +99,18 @@ const ProjectPage = () => {
{/* Organization Project Button */}
<button
type="button"
className={`btn px-2 py-1 rounded-0 text-tiny ${coreProjects ? "btn-primary text-white" : ""
className={`btn px-2 py-1 rounded-0 text-tiny ${
coreProjects ? "btn-primary text-white" : ""
}`}
onClick={() => handleToggleProject(true)}
>
Infra Project
</button>
</div>
</div>
{/* RIGHT SIDE — SEARCH + CARD/LIST + DROPDOWN */}
<div className="d-flex flex-wrap align-items-center justify-content-end">
{/* Search */}
<div className="me-2" style={{ minWidth: "200px" }}>
<input
@ -131,7 +129,9 @@ const ProjectPage = () => {
<div className="d-flex gap-2">
<button
type="button"
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary"}`}
className={`btn btn-sm p-1 ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
title="Card View"
>
@ -140,7 +140,9 @@ 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"
>
@ -180,10 +182,14 @@ const ProjectPage = () => {
<button
type="button"
className="btn btn-primary btn-sm d-flex align-items-center my-2"
onClick={() =>
onClick={
() =>
coreProjects
? setMangeProject({ isOpen: true, Project: null }) // Organization Project Infra
: setManageServiceProject({ isOpen: true, Project: null }) // Service Project
: setManageServiceProject({
isOpen: true,
Project: null,
}) // Service Project
}
>
<i className="bx bx-plus-circle me-2"></i>
@ -195,11 +201,22 @@ const ProjectPage = () => {
</div>
</div>
{coreProjects ? <ProjectsDisplay listView={listView}
{coreProjects ? (
<ProjectsDisplay
listView={listView}
searchTerm={searchTerm}
selectedStatuses={selectedStatuses}
handleStatusChange={handleStatusChange} /> : <ServiceProjectDisplay listView={listView}
selectedStatuses={selectedStatuses} />}
handleStatusChange={handleStatusChange}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
/>
) : (
<ServiceProjectDisplay
listView={listView}
searchTerm={searchTerm}
selectedStatuses={selectedStatuses}
/>
)}
</div>
</ProjectContext.Provider>
);

View File

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

View File

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

View File

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

View File

@ -4,9 +4,9 @@ import { api } from "../utils/axiosClient";
export const ServiceProjectRepository = {
//#region Service Project
CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data),
GetServiceProjects: (pageSize, pageNumber) =>
GetServiceProjects: (pageSize, pageNumber,searchString) =>
api.get(
`/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}`
`/api/ServiceProject/list?pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}`
),
GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`),
UpdateServiceProject: (id, data) =>