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

This commit is contained in:
Kartik Sharma 2025-11-18 17:01:22 +05:30
commit f997c2f2cc
11 changed files with 443 additions and 216 deletions

View File

@ -22,7 +22,7 @@ const JobComments = ({ data }) => {
formState: { errors }, formState: { errors },
} = useAppForm({ } = useAppForm({
resolver: zodResolver(JobCommentSchema), resolver: zodResolver(JobCommentSchema),
defaultValues: { comment: "", attachments: [] } defaultValues: { comment: "", attachments: [] },
}); });
const { const {
@ -118,21 +118,16 @@ const JobComments = ({ data }) => {
)} )}
</div> </div>
</div> </div>
<div className="d-flex flex-column flex-md-row justify-content-between align-items-start">
{/* LEFT SIDE → Uploaded Files */}
<div className="flex-grow-1 ms-10 mt-2"> <div className="flex-grow-1 ms-10 mt-2">
{files?.length > 0 && ( {files?.length > 0 && (
<Filelist files={files} removeFile={removeFile} /> <Filelist files={files} removeFile={removeFile} />
)} )}
</div> </div>
<div className="d-flex justify-content-end gap-2 align-items-center text-end mt-3 ms-10 ms-md-0">
{/* RIGHT SIDE → Add Attachment + Submit */}
<div className="d-flex gap-3 align-items-center text-end mt-3 ms-10 ms-md-0">
<div <div
onClick={() => document.getElementById("attachments").click()} onClick={() => document.getElementById("attachments").click()}
className="cursor-pointer" className="cursor-pointer"
style={{ whiteSpace: 'nowrap' }} style={{ whiteSpace: "nowrap" }}
> >
<input <input
type="file" type="file"
@ -155,13 +150,9 @@ const JobComments = ({ data }) => {
type="submit" type="submit"
disabled={!watch("comment")?.trim() || isPending} disabled={!watch("comment")?.trim() || isPending}
> >
<i className="bx bx-xs bx-send me-1"></i>
Submit Submit
</button> </button>
</div> </div>
</div>
</form> </form>
</div> </div>
<div className="card-body p-0"> <div className="card-body p-0">

View File

@ -1,5 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { daysLeft, getNextBadgeColor } from "../../utils/appUtils"; import {
daysLeft,
getJobStatusBadge,
getNextBadgeColor,
} from "../../utils/appUtils";
import { useServiceProjectJobs } from "../../hooks/useServiceProject"; import { useServiceProjectJobs } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
@ -23,7 +27,7 @@ const JobList = () => {
{ {
key: "jobTicketUId", key: "jobTicketUId",
label: "Job Id", label: "Job Id",
getValue: (e) => e?.jobTicketUId || "N/A", getValue: (e) => <span>{e?.jobTicketUId || "N/A"}</span>,
align: "text-start", align: "text-start",
}, },
{ {
@ -56,22 +60,9 @@ const JobList = () => {
key: "status", key: "status",
label: "Status", label: "Status",
getValue: (e) => { getValue: (e) => {
const statusName = e?.status?.displayName || "N/A";
const statusColorMap = {
Assigned: "label-primary",
Pending: "label-warning",
Completed: "label-success",
Cancelled: "label-danger",
};
const badgeColor = statusColorMap[statusName] || "label-secondary";
return ( return (
<span <span className={`badge ${getJobStatusBadge(e?.status?.id)}`}>
className={`badge bg-${badgeColor}`} {e?.status?.displayName}
style={{ fontSize: "12px" }}
>
{statusName}
</span> </span>
); );
}, },
@ -85,7 +76,7 @@ const JobList = () => {
const { days, color } = daysLeft(e.startDate, e.dueDate); const { days, color } = daysLeft(e.startDate, e.dueDate);
return ( return (
<span className={`badge bg-${color}`} style={{ fontSize: "12px" }}> <span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"} {days !== null ? `${days} days` : "N/A"}
</span> </span>
); );
@ -96,7 +87,7 @@ const JobList = () => {
]; ];
return ( return (
<div className="dataTables_wrapper dt-bootstrap5 no-footer "> <div className="dataTables_wrapper dt-bootstrap5 no-footer table-responsive">
<table <table
className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive" className="datatables-users table border-top dataTable no-footer dtr-column text-nowrap table-responsive"
aria-describedby="DataTables_Table_0_info" aria-describedby="DataTables_Table_0_info"
@ -123,15 +114,15 @@ const JobList = () => {
<tbody> <tbody>
{Array.isArray(data?.data) && data.data.length > 0 ? ( {Array.isArray(data?.data) && data.data.length > 0 ? (
data.data.map((row, i) => ( data.data.map((row, i) => (
<tr <tr key={i} className="text-start">
key={i}
className="text-start"
>
{jobGrid.map((col) => ( {jobGrid.map((col) => (
<td key={col.key} className={col.className} onClick={() => <td
key={col.key}
className={col.className}
onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id }) setSelectedJob({ showCanvas: true, job: row?.id })
}> }
>
{col.getValue(row)} {col.getValue(row)}
</td> </td>
))} ))}

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import Avatar from "../common/Avatar"; import Avatar from "../common/Avatar";
import { formatUTCToLocalTime } from "../../utils/dateUtils";
const JobStatusLog = ({ data }) => { const JobStatusLog = ({ data }) => {
return ( return (
@ -20,9 +21,9 @@ const JobStatusLog = ({ data }) => {
</span> </span>
</div> </div>
{/* <span className="badge bg-primary"> <span className="text-secondary">
Level {item.nextStatus?.level ?? item.status?.level} {formatUTCToLocalTime(item?.updatedAt,true)}
</span> */} </span>
</div> </div>
<div className="d-flex align-items-start mt-2 mx-0 px-0"> <div className="d-flex align-items-start mt-2 mx-0 px-0">
<Avatar <Avatar

View File

@ -7,10 +7,12 @@ import Avatar from "../common/Avatar";
import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup";
import JobStatusLog from "./JobStatusLog"; import JobStatusLog from "./JobStatusLog";
import JobComments from "./JobComments"; import JobComments from "./JobComments";
import { daysLeft } from "../../utils/appUtils"; import { daysLeft, getJobStatusBadge } from "../../utils/appUtils";
import HoverPopup from "../common/HoverPopup"; import HoverPopup from "../common/HoverPopup";
import ChangeStatus from "./ChangeStatus"; import ChangeStatus from "./ChangeStatus";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { STATUS_JOB_DONE } from "../../utils/constants";
import Tooltip from "../common/Tooltip";
const ManageJobTicket = ({ Job }) => { const ManageJobTicket = ({ Job }) => {
const { projectId } = useParams(); const { projectId } = useParams();
@ -46,12 +48,61 @@ const ManageJobTicket = ({ Job }) => {
<div className="row text-start"> <div className="row text-start">
<div className="col-12"> <div className="col-12">
<h6 className="fs-5 fw-semibold">{data?.title}</h6> <h6 className="fs-5 fw-semibold">{data?.title}</h6>
<div className="d-flex justify-content-between align-items-end flex-wrap mb-2"> <div className="d-flex justify-content-between align-items-start flex-wrap mb-2">
<p className="mb-0"> <p className="mb-0">
<span className="fw-medium me-1">Job Id :</span> <span className="fw-medium me-1">Job Id :</span>
{data?.jobTicketUId || "N/A"} {data?.jobTicketUId || "N/A"}
</p> </p>
<div className="d-flex flex-column align-items-end gap-3 mb-3"> <div className="d-flex flex-column align-items-end gap-3 mb-3">
<div className="d-flex flex-row gap-2">
<span className={`badge ${getJobStatusBadge(data?.status?.id)}`}>
{data?.status?.displayName}
</span>
{STATUS_JOB_DONE !== data?.status?.id && <HoverPopup
id="STATUS_CHANEG"
title="Change Status"
Mode="click"
className=""
content={
<ChangeStatus
statusId={data?.status?.id}
projectId={projectId}
jobId={Job?.job}
popUpId="STATUS_CHANEG"
/>
}
>
<Tooltip text={"Change Status"} placement="left" children={<i className="bx bx-edit bx-sm cursor-pointer"></i>}/>
</HoverPopup>}
</div>
</div>
</div>
<div className="d-flex flex-wrap">
<p>{data?.description || "N/A"}</p>
</div>
<div className="d-flex justify-content-between mb-4">
<div className="d-flex flex-row gap-1 fw-medium">
<i className="bx bx-calendar"></i>{" "}
<span>
Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
</span>
</div>
</div>
<div className="d-flex justify-content-md-between ">
<div className="d-flex flex-row gap-5">
<span className="fw-medium">
<i className="bx bx-calendar"></i> Start Date :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>{" "}
<i className="bx bx-right-arrow-alt"></i>{" "}
<span className="fw-medium">
<i className="bx bx-calendar"></i> Due on :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>
</div>
{data?.dueDate && {data?.dueDate &&
(() => { (() => {
const { days, color } = daysLeft( const { days, color } = daysLeft(
@ -67,61 +118,13 @@ const ManageJobTicket = ({ Job }) => {
</span> </span>
); );
})()} })()}
<div className="d-flex flex-row gap-2">
<span className="badge bg-label-primary">
{data?.status?.name}
</span>
<HoverPopup
id="STATUS_CHANEG"
title="Change Status"
Mode="click"
className=""
content={
<ChangeStatus
statusId={data?.status?.id}
projectId={projectId}
jobId={Job?.job}
popUpId="STATUS_CHANEG"
/>
}
>
<i className="bx bx-edit bx-sm cursor-pointer"></i>
</HoverPopup>
</div>
</div>
</div>
<div className="d-flex flex-wrap">
<p>{data?.description || "N/A"}</p>
</div>
<div className="d-flex justify-content-between mb-4">
<div className="d-flex flex-row gap-1 fw-medium">
<i className="bx bx-calendar"></i>{" "}
<span>
Created Date : {formatUTCToLocalTime(data?.createdAt, true)}
</span>
</div>
</div>
<div>
<div className="d-flex flex-row gap-5">
<span className="fw-medium">
<i className="bx bx-calendar"></i> Start Date :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>{" "}
<i className="bx bx-right-arrow-alt"></i>{" "}
<span className="fw-medium">
<i className="bx bx-calendar"></i> Due on :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>
</div>
</div> </div>
<div className="d-block mt-4 mb-3"> <div className="d-block mt-4 mb-3">
<div className="d-flex align-items-center mb-2"> <div className="row align-items-start align-items-md-start gap-2 mb-1">
<span className="fs-6 fw-medium me-5">Created By</span>{" "} <div className="col-12 col-md-auto">
<small className="fs-6 fw-medium">Created By</small>
</div>
<div className="col d-flex flex-row align-items-center ">
<Avatar <Avatar
size="xs" size="xs"
firstName={data?.createdBy?.firstName} firstName={data?.createdBy?.firstName}
@ -134,11 +137,20 @@ const ManageJobTicket = ({ Job }) => {
</small> </small>
</div> </div>
</div> </div>
<div className="d-flex flex-wrap align-items-start align-items-md-center"> </div>
<small className="fs-6 fw-medium me-3">Assigned By</small>
<div className="row g-3 mt-md-1"> <div className="row align-items-start align-items-md-start gap-2">
<div className="col-12 col-md-auto">
<small className="fs-6 fw-medium">Assigned To</small>
</div>
<div className="col">
<div className="row gap-4">
{data?.assignees?.map((emp) => ( {data?.assignees?.map((emp) => (
<div key={emp.id} className="col-6 col-sm-6 col-md-4 col-lg-4"> <div
key={emp.id}
className="col-6 col-sm-6 col-md-4 col-lg-4"
>
<div className="d-flex align-items-center gap-2"> <div className="d-flex align-items-center gap-2">
<Avatar <Avatar
size="xs" size="xs"
@ -147,10 +159,10 @@ const ManageJobTicket = ({ Job }) => {
/> />
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<span className="fw-semibold"> <span className=" text-truncate">
{emp.firstName} {emp.lastName} {emp.firstName} {emp.lastName}
</span> </span>
<small className="text-secondary"> <small className="text-secondary text-truncate">
{emp.jobRoleName} {emp.jobRoleName}
</small> </small>
</div> </div>
@ -161,6 +173,7 @@ const ManageJobTicket = ({ Job }) => {
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="nav-align-top nav-tabs-shadow p-0 shadow-none"> <div className="nav-align-top nav-tabs-shadow p-0 shadow-none">
<ul className="nav nav-tabs" role="tablist"> <ul className="nav nav-tabs" role="tablist">

View File

@ -123,12 +123,14 @@ const ServiceProjectTeamAllocation = () => {
<div className=" text-start"> <div className=" text-start">
<div className="row"> <div className="row">
<div className="d-flex justify-content-end"> <div className="d-flex justify-content-end">
{!isAddEmployee && <button {!isAddEmployee && (
<button
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
onClick={() => setIsAddEmployee(!isAddEmployee)} onClick={() => setIsAddEmployee(!isAddEmployee)}
> >
<i className="bx bx-plus-circle me-2"></i>Add Employee <i className="bx bx-plus-circle me-2"></i>Add Employee
</button>} </button>
)}
</div> </div>
{isAddEmployee && ( {isAddEmployee && (
@ -166,15 +168,15 @@ const ServiceProjectTeamAllocation = () => {
</> </>
)} )}
</div> </div>
{ isAddEmployee && ( {isAddEmployee && (
<div className="d-flex justify-content-end"> <div className="d-flex justify-content-end my-2">
{" "} {" "}
<button <button
type="button" type="button"
className="btn btn-label-secondary btn-sm me-2" className="btn btn-label-secondary btn-sm me-2"
onClick={()=>setIsAddEmployee(false)} onClick={() => setIsAddEmployee(false)}
aria-label="Close" aria-label="Close"
disabled={isPending } disabled={isPending}
> >
Cancel Cancel
</button> </button>
@ -188,7 +190,7 @@ const ServiceProjectTeamAllocation = () => {
</div> </div>
)} )}
<div className="col-12"> <div className="col-12">
<table className="table text-center "> <table className="table text-center">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -202,29 +204,30 @@ const ServiceProjectTeamAllocation = () => {
Team?.map((emp) => ( Team?.map((emp) => (
<tr key={emp?.id}> <tr key={emp?.id}>
<td className="w-min"> <td className="w-min">
<div className="d-flex align-items-center "> <div className="d-flex align-items-center gap-2">
{" "}
<Avatar <Avatar
size="xs" size="xs"
firstName={emp?.employee?.firstName} firstName={emp?.employee?.firstName}
lastName={emp?.employee?.lastName} lastName={emp?.employee?.lastName}
/> />
<span className="fw-medium">{`${emp?.employee?.firstName} ${emp?.employee?.lastName}`}</span> <span className="fw-medium">
{emp?.employee?.firstName} {emp?.employee?.lastName}
</span>
</div> </div>
</td> </td>
<td>{emp?.teamRole?.name}</td> <td>{emp?.teamRole?.name}</td>
<td className="">
{deletingEmp?.emplyee?.id === emp.id && isPending ? ( <td>
<div className="spinner-border spinner-border-sm "></div> {deletingEmp?.employee?.id === emp.id && isPending ? (
<div className="spinner-border spinner-border-sm"></div>
) : ( ) : (
<span disabled={isPending}>
<i <i
className="bx bx-trash bx-sm text-danger cursor-pointer" className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => onClick={() =>
setSeletingEmp({ employee: emp, isOpen: true }) setSeletingEmp({ employee: emp, isOpen: true })
} }
></i> ></i>
</span>
)} )}
</td> </td>
</tr> </tr>
@ -233,18 +236,12 @@ const ServiceProjectTeamAllocation = () => {
<tr> <tr>
<td <td
colSpan={3} colSpan={3}
className="text-muted border-0 d-flex justify-content-center " className="text-center text-muted py-4 border-0"
> >
{isTeamLoading ? ( {isTeamLoading ? (
<SpinnerLoader /> <SpinnerLoader />
) : ( ) : (
<div className="bg-light-secondary py-3 w-50 m-auto my-2"> <p className="m-0 py-4">No Records Found</p>
<i className="bx bx-box bx-md text-primary"></i>
<p className="fw-medium mt-3">
{" "}
Please Add a Employee{" "}
</p>
</div>
)} )}
</td> </td>
</tr> </tr>

View File

@ -15,14 +15,17 @@ const ServiceProjectTeamList = () => {
{ {
key: "employeName", key: "employeName",
label: "Name", label: "Name",
getValue: (e) => { getValue: (e) => (
return ( <div className="d-flex align-items-center">
<div className="d-flex align-itmes-center"> {" "}
<Avatar /> <Avatar size="xs"
<span>{`${e.employee.firstName} ${e.employee.lastName}`}</span> firstName={e.employee.firstName}
lastName={e.employee.lastName}
/>
<small>{`${e.employee.firstName} ${e.employee.lastName}`}</small>
</div> </div>
); ),
}, align: "text-start",
}, },
{ {
key: "teamRole", key: "teamRole",
@ -30,66 +33,51 @@ const ServiceProjectTeamList = () => {
getValue: (e) => { getValue: (e) => {
return ( return (
<div className="d-flex align-itmes-center"> <div className="d-flex align-itmes-center">
<span>{`${e.teamRole.firstName}`}</span> <span>{`${e.teamRole.name}`}</span>
</div> </div>
); );
}, },
align: "text-start",
}, },
{ {
key: "assignedAt", key: "assignedAt",
label: "assigned Date", label: "assigned Date",
getValue: (e) => { getValue: (e) =>
return ( formatUTCToLocalTime(e.assignedAT)
<div className="d-flex align-itmes-center"> ,
{formatUTCToLocalTime(e.assignedAT)} align: "text-center",
</div>
);
},
}, },
]; ];
return ( return (
<div className="data-table table-responsive "> <div className="table-responsive">
<table className="table w-full"> <table className="table align-middle mb-0">
<thead> <thead>
<tr> <tr>
{servceProjectColmen.map((col) => ( {servceProjectColmen.map((col) => (
<th key={col.key}>{col.label}</th> <th key={col.key} className={col.align}>{col.label}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 ? ( {data?.length > 0 ? (
data.map((emp) => ( data.map((row) => (
<tr key={emp?.id}> <tr key={row.id}>
<td className="w-min"> {servceProjectColmen.map((col) => (
<div className="d-flex align-items-center gap-2 py-1"> <td key={col.key} className={col.align}>{col.getValue(row)}</td>
<Avatar ))}
size="xs"
firstName={emp?.employee?.firstName}
lastName={emp?.employee?.lastName}
/>
<span className="fw-medium">
{`${emp?.employee?.firstName} ${emp?.employee?.lastName}`}
</span>
</div>
</td>
<td>{emp?.teamRole?.name}</td>
<td>{formatUTCToLocalTime(emp.assignedAt)}</td>
</tr> </tr>
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan={3} className="text-muted border-0 text-center py-4"> <td
colSpan={servceProjectColmen.length}
className="text-center py-4 border-0"
>
{isLoading ? ( {isLoading ? (
<SpinnerLoader /> <SpinnerLoader />
) : ( ) : (
<div className="bg-light-secondary py-3 w-50 mx-auto my-2"> <div className="py-8">No Records Available</div>
<i className="bx bx-box bx-md text-primary"></i>
<p className="fw-medium mt-3">Please Add an Employee</p>
</div>
)} )}
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,56 @@
import React from 'react'
const InputFieldSuggesstion = () => {
return (
<div className="position-relative">
<input
className="form-control form-control-sm"
value={value}
onChange={handleInputChange}
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
onFocus={() => {
if (value) setShowSuggestions(true);
}}
disabled={disabled}
/>
{showSuggestions && filteredList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius:"0px"
}}
>
{filteredList.map((org) => (
<li
key={org}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectSuggestion(org)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{org}
</li>
))}
</ul>
)}
{error && <small className="danger-text">{error}</small>}
</div>
)
}
export default InputFieldSuggesstion

View File

@ -181,5 +181,173 @@ const SelectEmployeeServerSide = ({
</div> </div>
); );
}; };
export default SelectEmployeeServerSide; export default SelectEmployeeServerSide;
export const SelectProjectField = ()=>{
const [searchText, setSearchText] = useState("");
const debounce = useDebounce(searchText, 300);
const { data, isLoading } = useEmployeesName(
projectId,
debounce,
isAllEmployee
);
const options = data?.data ?? [];
const [open, setOpen] = useState(false);
const dropdownRef = useRef(null);
const getDisplayName = (emp) => {
if (!emp) return "";
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);
}
/** -----------------------------
* SELECTED OPTION (MULTIPLE)
* ----------------------------- */
let selectedList = [];
if (isMultiple && Array.isArray(value)) {
if (isFullObject) selectedList = value;
else {
selectedList = options.filter((opt) => value.includes(opt[valueKey]));
}
}
/** Main button label */
const displayText = !isMultiple
? getDisplayName(selectedSingle) || placeholder
: selectedList.length > 0
? selectedList.map((e) => getDisplayName(e)).join(", ")
: placeholder;
/** -----------------------------
* HANDLE OUTSIDE CLICK
* ----------------------------- */
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
/** -----------------------------
* HANDLE SELECT
* ----------------------------- */
const handleSelect = (option) => {
if (!isMultiple) {
// SINGLE SELECT
if (isFullObject) onChange(option);
else onChange(option[valueKey]);
} else {
// MULTIPLE SELECT
let updated = [];
const exists = selectedList.some((e) => e[valueKey] === option[valueKey]);
if (exists) {
// remove
updated = selectedList.filter((e) => e[valueKey] !== option[valueKey]);
} else {
// add
updated = [...selectedList, option];
}
if (isFullObject) onChange(updated);
else onChange(updated.map((x) => x[valueKey]));
}
};
return (
<div className="mb-3 position-relative" ref={dropdownRef}>
{label && (
<Label className="form-label" required={required}>
{label}
</Label>
)}
{/* MAIN BUTTON */}
<button
type="button"
className={`select2-icons form-select d-flex align-items-center justify-content-between ${
open ? "show" : ""
}`}
onClick={() => setOpen((prev) => !prev)}
>
<span className={`text-truncate ${!displayText ? "text-muted" : ""}`}>
{displayText}
</span>
</button>
{/* DROPDOWN */}
{open && (
<ul
className="dropdown-menu w-100 shadow-sm show animate__fadeIn h-64 overflow-auto rounded"
style={{
position: "absolute",
top: "100%",
left: 0,
zIndex: 1050,
marginTop: "4px",
borderRadius: "0.375rem",
overflow: "hidden",
}}
>
<div className="p-1">
<input
type="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="form-control form-control-sm"
placeholder="Search..."
/>
</div>
{isLoading && (
<li className="dropdown-item text-muted text-center">Loading...</li>
)}
{!isLoading && options.length === 0 && (
<li className="dropdown-item text-muted text-center">No results found</li>
)}
{!isLoading &&
options.map((option) => {
const isActive = isMultiple
? selectedList.some((x) => x[valueKey] === option[valueKey])
: selectedSingle &&
selectedSingle[valueKey] === option[valueKey];
return (
<li key={option[valueKey]}>
<button
type="button"
className={`dropdown-item ${isActive ? "active" : ""}`}
onClick={() => handleSelect(option)}
>
{getDisplayName(option)}
</button>
</li>
);
})}
</ul>
)}
</div>
);
};

View File

@ -66,7 +66,7 @@ const OffcanvasComponent = ({
></button> ></button>
</div> </div>
<div className="offcanvas-body">{children}</div> <div className="offcanvas-body ">{children}</div>
</div> </div>
</div> </div>
); );

View File

@ -34,8 +34,24 @@ export const getColorNameFromHex = (hex) => {
} }
} }
return null; // return null;
}; };
export const getJobStatusBadge = (statusId) => {
if (!statusId) return "bg-label-secondary";
const map = {
"32d76a02-8f44-4aa0-9b66-c3716c45a918": "bg-label-primary", // New
"cfa1886d-055f-4ded-84c6-42a2a8a14a66": "bg-label-info", // Assigned
"5a6873a5-fed7-4745-a52f-8f61bf3bd72d": "bg-label-warning", // In Progress
"aab71020-2fb8-44d9-9430-c9a7e9bf33b0": "bg-label-label-dark", // Review
"ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7": "bg-label-success", // Done
"3ddeefb5-ae3c-4e10-a922-35e0a452bb69": "bg-label-secondary", // Closed
"75a0c8b8-9c6a-41af-80bf-b35bab722eb2": "bg-label-danger", // On Hold
};
return map[statusId] || "bg-label-secondary";
};
export const useDebounce = (value, delay = 500) => { export const useDebounce = (value, delay = 500) => {
const [debouncedValue, setDebouncedValue] = useState(value); const [debouncedValue, setDebouncedValue] = useState(value);

View File

@ -205,3 +205,9 @@ export const PAYEE_RECURRING_EXPENSE = [
label: "Paused", label: "Paused",
}, },
]; ];
//#region Service Project and Jobs
export const STATUS_JOB_DONE = "ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"
//#endregion