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 EmployeeSearchInput from "../common/EmployeeSearchInput";
import Filelist from "./Filelist"; import Filelist from "./Filelist";
import { DEFAULT_CURRENCY } from "../../utils/constants"; import { DEFAULT_CURRENCY } from "../../utils/constants";
import SelectEmployeeServerSide, { SelectProjectField } from "../common/Forms/SelectFieldServerSide"; import SelectEmployeeServerSide, {
SelectProjectField,
} from "../common/Forms/SelectFieldServerSide";
import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject"; import { useAllocationServiceProjectTeam } from "../../hooks/useServiceProject";
import { AppFormController } from "../../hooks/appHooks/useAppForm";
const ManageExpense = ({ closeModal, expenseToEdit = null }) => { const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
const { const {
@ -153,15 +156,14 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
} }
}; };
const { mutate: AllocationTeam, isPending1 } = useAllocationServiceProjectTeam( const { mutate: AllocationTeam, isPending1 } =
() => { useAllocationServiceProjectTeam(() => {
setSelectedEmployees([]); setSelectedEmployees([]);
setSeletingEmp({ setSeletingEmp({
employee: null, employee: null,
isOpen: false, isOpen: false,
}); });
} });
);
useEffect(() => { useEffect(() => {
if (expenseToEdit && data) { if (expenseToEdit && data) {
reset({ reset({
@ -180,17 +182,17 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
currencyId: data.currency.id || DEFAULT_CURRENCY, currencyId: data.currency.id || DEFAULT_CURRENCY,
billAttachments: data.documents billAttachments: data.documents
? data.documents.map((doc) => ({ ? data.documents.map((doc) => ({
fileName: doc.fileName, fileName: doc.fileName,
base64Data: null, base64Data: null,
contentType: doc.contentType, contentType: doc.contentType,
documentId: doc.documentId, documentId: doc.documentId,
fileSize: 0, fileSize: 0,
description: "", description: "",
preSignedUrl: doc.preSignedUrl, preSignedUrl: doc.preSignedUrl,
isActive: doc.isActive ?? true, isActive: doc.isActive ?? true,
})) }))
: [], : [],
}); });
} }
}, [data, reset]); }, [data, reset]);
const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() => const { mutate: ExpenseUpdate, isPending } = useUpdateExpense(() =>
@ -264,7 +266,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
required required
placeholder="Select Project" placeholder="Select Project"
value={watch("projectId")} value={watch("projectId")}
onChange={(val) => onChange={(val) =>
setValue("projectId", val, { setValue("projectId", val, {
shouldDirty: true, shouldDirty: true,
@ -337,16 +338,30 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
)} )}
</div> </div>
<div className="col-12 col-md-6 text-start"> <div className="col-12 col-md-6 text-start">
<Label className="form-label" required> {/* <Label className="form-label" required>
Paid By{" "} Paid By{" "}
</Label> </Label> */}
<EmployeeSearchInput {/* <EmployeeSearchInput
control={control} control={control}
name="paidById" name="paidById"
projectId={null} projectId={null}
forAll={true} forAll={true}
/> */}
<AppFormController
name="paidById"
control={control}
render={({ field }) => (
<SelectEmployeeServerSide
label="Paid By" required
value={field.value}
onChange={field.onChange}
isFullObject={false} // because using ID
/>
)}
/> />
</div> </div>
</div> </div>
<div className="row my-2 text-start"> <div className="row my-2 text-start">
@ -453,7 +468,6 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
<small className="danger-text">{errors.gstNumber.message}</small> <small className="danger-text">{errors.gstNumber.message}</small>
)} )}
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col-md-6 text-start "> <div className="col-md-6 text-start ">
@ -484,7 +498,9 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
</div> </div>
{expenseCategory?.noOfPersonsRequired && ( {expenseCategory?.noOfPersonsRequired && (
<div className="col-md-6 text-start"> <div className="col-md-6 text-start">
<Label className="form-label" required>No. of Persons</Label> <Label className="form-label" required>
No. of Persons
</Label>
<input <input
type="number" type="number"
id="noOfPersons" id="noOfPersons"
@ -570,7 +586,7 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
(fileError?.fileSize?.message || (fileError?.fileSize?.message ||
fileError?.contentType?.message || fileError?.contentType?.message ||
fileError?.base64Data?.message, fileError?.base64Data?.message,
fileError?.documentId?.message) fileError?.documentId?.message)
} }
</div> </div>
))} ))}
@ -595,8 +611,8 @@ const ManageExpense = ({ closeModal, expenseToEdit = null }) => {
{isPending || createPending {isPending || createPending
? "Please Wait..." ? "Please Wait..."
: expenseToEdit : expenseToEdit
? "Update" ? "Update"
: "Save as Draft"} : "Save as Draft"}
</button> </button>
</div> </div>
</form> </form>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,11 +72,11 @@ const Jobs = () => {
> >
{showArchive ? ( {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> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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