diff --git a/src/components/Layout/Sidebar.jsx b/src/components/Layout/Sidebar.jsx index 1f4b63b8..4fa7cd9f 100644 --- a/src/components/Layout/Sidebar.jsx +++ b/src/components/Layout/Sidebar.jsx @@ -25,8 +25,7 @@ const Sidebar = () => { /> */} - @@ -35,12 +34,13 @@ const Sidebar = () => { OnField Work .com - + - + + - +
diff --git a/src/components/ServiceProject/Jobs.jsx b/src/components/ServiceProject/Jobs.jsx index d6339174..3859fb3a 100644 --- a/src/components/ServiceProject/Jobs.jsx +++ b/src/components/ServiceProject/Jobs.jsx @@ -5,8 +5,8 @@ import { useServiceProjects } from "../../hooks/useServiceProject"; import { ITEMS_PER_PAGE } from "../../utils/constants"; import OffcanvasComponent from "../common/OffcanvasComponent"; import showToast from "../../services/toastService"; -import ManageJob from "./ManageJob"; -import ManageJobTicket from "./ManageJobTicket"; +import ManageJob from "./ServiceProjectJob/ManageJob"; +import ManageJobTicket from "./ServiceProjectJob/ManageJobTicket"; import GlobalModel from "../common/GlobalModel"; import PreviewDocument from "../Expenses/PreviewDocument"; diff --git a/src/components/ServiceProject/ServiceProjectBranch/BranchDetails.jsx b/src/components/ServiceProject/ServiceProjectBranch/BranchDetails.jsx new file mode 100644 index 00000000..66bd700c --- /dev/null +++ b/src/components/ServiceProject/ServiceProjectBranch/BranchDetails.jsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { useBranch } from "../../../hooks/useServiceProject"; +import { SpinnerLoader } from "../../common/Loader"; +import Error from "../../common/Error"; +import { BranchDetailsSkeleton } from "../ServiceProjectSeketon"; + +const BranchDetails = ({ branch }) => { + const [copied, setCopied] = useState(false); + + const { data, isLoading, isError, error } = useBranch(branch); + + const googleMapUrl = data?.googleMapUrl || data?.locationLink; + + const handleCopy = async () => { + if (!googleMapUrl) return; + + await navigator.clipboard.writeText(googleMapUrl); + setCopied(true); + + setTimeout(() => setCopied(false), 3000); + }; + if (isLoading) return ; + if (isError) + return ( +
+ +
+ ); + return ( +
+
+ + Branch Details + +
+
+
Name:
+
{data?.branchName}
+
+ +
+
Type:
+
{data?.branchType}
+
+
+
Contact No:
+
{data?.contactInformation}
+
+ +
+
Address:
+
{data?.address}
+
+ + {googleMapUrl && ( +
+
Map:
+ +
+ + Open in Google Maps + + + + + {copied && Copied!} +
+
+ )} +
+ ); +}; + +export default BranchDetails; diff --git a/src/components/ServiceProject/ChangeStatus.jsx b/src/components/ServiceProject/ServiceProjectJob/ChangeStatus.jsx similarity index 83% rename from src/components/ServiceProject/ChangeStatus.jsx rename to src/components/ServiceProject/ServiceProjectJob/ChangeStatus.jsx index 49b2fc70..935c621d 100644 --- a/src/components/ServiceProject/ChangeStatus.jsx +++ b/src/components/ServiceProject/ServiceProjectJob/ChangeStatus.jsx @@ -1,17 +1,16 @@ -import SelectField from "../common/Forms/SelectField"; -import { useJobStatus } from "../../hooks/masterHook/useMaster"; -import { SpinnerLoader } from "../common/Loader"; -import Error from "../common/Error"; +import SelectField from "../../common/Forms/SelectField"; +import Error from "../../common/Error"; import { z } from "zod"; import { AppFormController, AppFormProvider, useAppForm, -} from "../../hooks/appHooks/useAppForm"; +} from "../../../hooks/appHooks/useAppForm"; import { zodResolver } from "@hookform/resolvers/zod"; import { useDispatch, useSelector } from "react-redux"; -import { closePopup } from "../../slices/localVariablesSlice"; -import { useUpdateServiceProjectJob } from "../../hooks/useServiceProject"; +import { closePopup } from "../../../slices/localVariablesSlice"; +import { useUpdateServiceProjectJob } from "../../../hooks/useServiceProject"; +import { useJobStatus } from "../../../hooks/masterHook/useMaster"; export const ChangeStatusSchema = z.object({ statusId: z.string().min(1, { message: "Please select status" }), @@ -53,6 +52,11 @@ const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => { }; return ( +
+ + Change Status + +
{ const { @@ -161,46 +161,48 @@ const JobComments = ({ data }) => { const user = item?.createdBy; return ( -
-
- -
-
- - {user?.firstName} {user?.lastName} - - - {formatUTCToLocalTime(item?.createdAt, true)} - -
-
- {user?.jobRoleName} -
-
-

{item.comment}

-
- {item.attachments?.map((file) => ( -
- -
-

{file.fileName}

- - {formatFileSize(file.fileSize)} - -
+
+ + +
+
+ + {user?.firstName} {user?.lastName} + + + + {formatUTCToLocalTime(item?.createdAt, true)} + + +
+ +
+ {user?.jobRoleName} +
+ +
+

{item.comment}

+ +
+ {item.attachments?.map((file) => ( +
+ +
+

{file.fileName}

+ + {formatFileSize(file.fileSize)} +
- ))} -
+
+ ))}
diff --git a/src/components/ServiceProject/JobStatusLog.jsx b/src/components/ServiceProject/ServiceProjectJob/JobStatusLog.jsx similarity index 94% rename from src/components/ServiceProject/JobStatusLog.jsx rename to src/components/ServiceProject/ServiceProjectJob/JobStatusLog.jsx index 88ff3562..bf13b7e5 100644 --- a/src/components/ServiceProject/JobStatusLog.jsx +++ b/src/components/ServiceProject/ServiceProjectJob/JobStatusLog.jsx @@ -1,6 +1,7 @@ import React from "react"; -import Avatar from "../common/Avatar"; -import { formatUTCToLocalTime } from "../../utils/dateUtils"; +import Avatar from "../../common/Avatar"; +import { formatUTCToLocalTime } from "../../../utils/dateUtils"; + const JobStatusLog = ({ data }) => { return ( diff --git a/src/components/ServiceProject/ManageJob.jsx b/src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx similarity index 88% rename from src/components/ServiceProject/ManageJob.jsx rename to src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx index 6ec96e3b..cc2066e0 100644 --- a/src/components/ServiceProject/ManageJob.jsx +++ b/src/components/ServiceProject/ServiceProjectJob/ManageJob.jsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from "react"; -import Breadcrumb from "../common/Breadcrumb"; -import Label from "../common/Label"; +import Breadcrumb from "../../common/Breadcrumb"; +import Label from "../../common/Label"; import { zodResolver } from "@hookform/resolvers/zod"; -import { defaultJobValue, jobSchema } from "./ServiceProjectSchema"; +import { defaultJobValue, jobSchema } from "../ServiceProjectSchema"; import { useBranches, useCreateServiceProjectJob, @@ -10,23 +10,23 @@ import { useServiceProjectJobDetails, useServiceProjects, useUpdateServiceProjectJob, -} from "../../hooks/useServiceProject"; -import { ITEMS_PER_PAGE } from "../../utils/constants"; -import DatePicker from "../common/DatePicker"; -import PmsEmployeeInputTag from "../common/PmsEmployeeInputTag"; -import TagInput from "../common/TagInput"; -import { localToUtc } from "../../utils/appUtils"; -import SelectField from "../common/Forms/SelectField"; +} from "../../../hooks/useServiceProject"; +import { ITEMS_PER_PAGE } from "../../../utils/constants"; +import DatePicker from "../../common/DatePicker"; +import PmsEmployeeInputTag from "../../common/PmsEmployeeInputTag"; +import TagInput from "../../common/TagInput"; +import { localToUtc } from "../../../utils/appUtils"; +import SelectField from "../../common/Forms/SelectField"; import { AppFormController, AppFormProvider, useAppForm, -} from "../../hooks/appHooks/useAppForm"; +} from "../../../hooks/appHooks/useAppForm"; import { useParams } from "react-router-dom"; import { useDispatch } from "react-redux"; -import { useJobStatus } from "../../hooks/masterHook/useMaster"; -import { useServiceProjectJobContext } from "./Jobs"; -import { SelectFieldSearch } from "../common/Forms/SelectFieldServerSide"; +import { useJobStatus } from "../../../hooks/masterHook/useMaster"; +import { useServiceProjectJobContext } from "../Jobs"; +import { SelectFieldSearch } from "../../common/Forms/SelectFieldServerSide"; const ManageJob = ({ Job }) => { const { setManageJob, setSelectedJob } = useServiceProjectJobContext(); @@ -165,7 +165,7 @@ const ManageJob = ({ Job }) => { dueDate: JobData.dueDate ?? null, tags: JobData.tags ?? [], statusId: JobData.status.id, - branchId : JobData?.projectBranch?.id + projectBranchId : JobData?.projectBranch?.id }); }, [JobData, Job, projectId]); return ( @@ -248,8 +248,8 @@ const ManageJob = ({ Job }) => { setValue("branchId", val)} + value={watch("projectBranchId")} + onChange={(val) => setValue("projectBranchId", val)} valueKey="id" labelKey="branchName" hookParams={[projectId, true, 10, 1]} diff --git a/src/components/ServiceProject/ManageJobTicket.jsx b/src/components/ServiceProject/ServiceProjectJob/ManageJobTicket.jsx similarity index 60% rename from src/components/ServiceProject/ManageJobTicket.jsx rename to src/components/ServiceProject/ServiceProjectJob/ManageJobTicket.jsx index c589509c..12428c77 100644 --- a/src/components/ServiceProject/ManageJobTicket.jsx +++ b/src/components/ServiceProject/ServiceProjectJob/ManageJobTicket.jsx @@ -1,25 +1,27 @@ -import React, { useEffect } from "react"; -import { useServiceProjectJobDetails } from "../../hooks/useServiceProject"; -import { SpinnerLoader } from "../common/Loader"; -import Error from "../common/Error"; -import { formatUTCToLocalTime } from "../../utils/dateUtils"; -import Avatar from "../common/Avatar"; -import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; -import JobStatusLog from "./JobStatusLog"; -import JobComments from "./JobComments"; -import { daysLeft, getJobStatusBadge } from "../../utils/appUtils"; -import HoverPopup from "../common/HoverPopup"; +import React, { useEffect, useRef } from "react"; +import { useServiceProjectJobDetails } from "../../../hooks/useServiceProject"; +import { SpinnerLoader } from "../../common/Loader"; +import Error from "../../common/Error"; +import { formatUTCToLocalTime } from "../../../utils/dateUtils"; +import Avatar from "../../common/Avatar"; +import EmployeeAvatarGroup from "../../common/EmployeeAvatarGroup"; +import { daysLeft, getJobStatusBadge } from "../../../utils/appUtils"; +import HoverPopup from "../../common/HoverPopup"; import ChangeStatus from "./ChangeStatus"; import { useParams } from "react-router-dom"; -import { STATUS_JOB_CLOSED } from "../../utils/constants"; -import Tooltip from "../common/Tooltip"; +import { STATUS_JOB_CLOSED } from "../../../utils/constants"; +import Tooltip from "../../common/Tooltip"; +import BranchDetails from "../ServiceProjectBranch/BranchDetails"; +import { JobDetailsSkeleton } from "../ServiceProjectSeketon"; +import JobComments from "./JobComments"; +import JobStatusLog from "./JobStatusLog"; const ManageJobTicket = ({ Job }) => { const { projectId } = useParams(); const { data, isLoading, isError, error } = useServiceProjectJobDetails( Job?.job ); - + const drawerRef = useRef(); const tabsData = [ { id: "comment", @@ -37,7 +39,7 @@ const ManageJobTicket = ({ Job }) => { }, ]; - if (isLoading) return ; + if (isLoading) return ; if (isError) return (
@@ -45,7 +47,7 @@ const ManageJobTicket = ({ Job }) => {
); return ( -
+
{data?.title}
@@ -61,7 +63,6 @@ const ManageJobTicket = ({ Job }) => { {STATUS_JOB_CLOSED !== data?.status?.id && ( { ); })()}
-
- - {" "} - Branch Name : - -
-
-
-
- Created By -
-
- {" "} -
-

{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}

- - ({data?.createdBy?.jobRoleName}) - -
-
+ {data?.projectBranch && ( +
+ + Branch Name : + + + } + > + + {data?.projectBranch?.branchName} + +
+ )} - {data?.assignees?.length > 0 && ( -
-
- Assigned To -
- -
-
- {data?.assignees?.map((emp) => ( -
-
- - -
- - {emp.firstName} {emp.lastName} - - - {emp.jobRoleName} - -
-
-
- ))} -
-
-
- )} -
+
+ People +
diff --git a/src/components/ServiceProject/ServiceProjectJob/UpdateJobComment.jsx b/src/components/ServiceProject/ServiceProjectJob/UpdateJobComment.jsx new file mode 100644 index 00000000..48ce3e27 --- /dev/null +++ b/src/components/ServiceProject/ServiceProjectJob/UpdateJobComment.jsx @@ -0,0 +1,80 @@ +import React from "react"; +import { useAppForm } from "../../../hooks/appHooks/useAppForm"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { JobCommentSchema } from "../ServiceProjectSchema"; +import Avatar from "../../common/Avatar"; +import Filelist from "../../Expenses/Filelist"; + +const UpdateJobComment = () => { + const { + register, + handleSubmit, + watch, + reset, + setValue, + formState: { errors }, + } = useAppForm({ + resolver: zodResolver(JobCommentSchema), + defaultValues: { comment: "", attachments: [] }, + }); + const onSubmit = () => {}; + return ( +
+ +
+ + +
+ + + {errors?.comment && ( + {errors?.comment?.message} + )} +
+
+ {/*
+ {files?.length > 0 && ( + + )} +
*/} +
+
document.getElementById("attachments").click()} + className="cursor-pointer" + style={{ whiteSpace: "nowrap" }} + > + { + onFileChange(e); + e.target.value = ""; + }} + /> + + Add Attachment +
+ + +
+ +
+ ); +}; + +export default UpdateJobComment; diff --git a/src/components/ServiceProject/ServiceProjectSchema.jsx b/src/components/ServiceProject/ServiceProjectSchema.jsx index 38013cae..dc8a6713 100644 --- a/src/components/ServiceProject/ServiceProjectSchema.jsx +++ b/src/components/ServiceProject/ServiceProjectSchema.jsx @@ -75,7 +75,7 @@ export const jobSchema = z.object({ statusId: z.string().optional().nullable(), - branchId: z.string().optional().nullable(), + projectBranchId: z.string().optional().nullable(), }); const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB diff --git a/src/components/ServiceProject/ServiceProjectSeketon.jsx b/src/components/ServiceProject/ServiceProjectSeketon.jsx new file mode 100644 index 00000000..b4b3a93e --- /dev/null +++ b/src/components/ServiceProject/ServiceProjectSeketon.jsx @@ -0,0 +1,138 @@ +import React from "react"; + +const SkeletonLine = ({ height = 18, width = "100%", className = "" }) => ( +
+); + +export const BranchDetailsSkeleton = () => { + return ( +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ ); +}; + +export const JobDetailsSkeleton = () => { + return ( +
+
+ {/* Title */} + + + {/* Job ID + Status */} +
+ +
+ + +
+
+ + {/* Description */} + + + {/* Created Date */} +
+ +
+ + {/* Start / Due Date */} +
+ + +
+ + {/* Branch Name */} +
+ + +
+ + {/* Created By */} +
+
+ +
+ +
+ {/* Avatar */} + +
+
+ + {/* Assigned To */} +
+
+ +
+
+ + {/* Tabs */} +
+
+ + + +
+ + +
+
+
+ ); +}; diff --git a/src/components/common/HoverPopup.jsx b/src/components/common/HoverPopup.jsx index e1c2dd4d..2093c25a 100644 --- a/src/components/common/HoverPopup.jsx +++ b/src/components/common/HoverPopup.jsx @@ -1,11 +1,11 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { - closePopup, - openPopup, - togglePopup, -} from "../../slices/localVariablesSlice"; +import { closePopup, openPopup, togglePopup } from "../../slices/localVariablesSlice"; +/** + * align: "auto" | "left" | "right" + * boundaryRef: optional ref to the drawer/container element to use as boundary + */ const HoverPopup = ({ id, title, @@ -13,6 +13,8 @@ const HoverPopup = ({ children, className = "", Mode = "hover", + align = "auto", + boundaryRef = null, }) => { const dispatch = useDispatch(); const visible = useSelector((s) => s.localVariables.popups[id] || false); @@ -23,11 +25,9 @@ const HoverPopup = ({ const handleMouseEnter = () => { if (Mode === "hover") dispatch(openPopup(id)); }; - const handleMouseLeave = () => { if (Mode === "hover") dispatch(closePopup(id)); }; - const handleClick = (e) => { if (Mode === "click") { e.stopPropagation(); @@ -35,43 +35,111 @@ const HoverPopup = ({ } }; + // Close on outside click when using click mode useEffect(() => { if (Mode !== "click" || !visible) return; - const handleOutside = (e) => { + const handler = (e) => { if ( - !popupRef.current?.contains(e.target) && - !triggerRef.current?.contains(e.target) + popupRef.current && + !popupRef.current.contains(e.target) && + triggerRef.current && + !triggerRef.current.contains(e.target) ) { dispatch(closePopup(id)); } }; - document.addEventListener("click", handleOutside); - return () => document.removeEventListener("click", handleOutside); - }, [visible, Mode, id]); + document.addEventListener("click", handler); + return () => document.removeEventListener("click", handler); + }, [Mode, visible, dispatch, id]); + + // Positioning effect: respects align prop and stays inside boundary (drawer) useEffect(() => { - if (!visible || !popupRef.current) return; + if (!visible || !popupRef.current || !triggerRef.current) return; - const popup = popupRef.current; - const rect = popup.getBoundingClientRect(); + // run in next frame so DOM/layout settles + requestAnimationFrame(() => { + const popup = popupRef.current; - popup.style.left = "50%"; - popup.style.right = "auto"; - popup.style.transform = "translateX(-50%)"; + // choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement) + const boundaryEl = (boundaryRef && boundaryRef.current) || popup.parentElement; + if (!boundaryEl) return; - if (rect.right > window.innerWidth) { - popup.style.left = "auto"; - popup.style.right = "0"; - popup.style.transform = "none"; - } + const boundaryRect = boundaryEl.getBoundingClientRect(); + const triggerRect = triggerRef.current.getBoundingClientRect(); - if (rect.left < 0) { - popup.style.left = "0"; - popup.style.right = "auto"; - popup.style.transform = "none"; - } - }, [visible]); + // reset styles first + popup.style.left = ""; + popup.style.right = ""; + popup.style.transform = ""; + popup.style.top = ""; + + // default: place below trigger and center horizontally relative to parent + // We'll use absolute positioning with respect to the positioned parent container. + // Ensure popup is positioned using left/right in parent's coordinate system. + // Compute desired left (centered under trigger) + const popupRect = popup.getBoundingClientRect(); + const parentRect = boundaryRect; // alias + + // Convert trigger center to parent coordinates + const triggerCenterX = triggerRect.left + triggerRect.width / 2 - parentRect.left; + + // preferred left so popup center aligns to trigger center: + const preferredLeft = triggerCenterX - popupRect.width / 2; + + // Helpers to set styles in parent's coordinate system: + const setLeft = (leftPx) => { + popup.style.left = `${leftPx}px`; + popup.style.right = "auto"; + popup.style.transform = "none"; + }; + const setRight = (rightPx) => { + popup.style.left = "auto"; + popup.style.right = `${rightPx}px`; + popup.style.transform = "none"; + }; + + // If user forced align: + if (align === "left") { + // align popup's left to parent's left (0) + setLeft(0); + return; + } + if (align === "right") { + // align popup's right to parent's right (0) + setRight(0); + return; + } + + // align === "auto": try preferred centered position, but flip fully if overflow + // clamp preferredLeft to boundaries so it doesn't render partially outside + const leftIfCentered = Math.max(0, Math.min(preferredLeft, parentRect.width - popupRect.width)); + + // if centered fits, use it + if (leftIfCentered === preferredLeft) { + setLeft(leftIfCentered); + return; + } + + // if centering would overflow right -> stick popup fully to left (left=0) + if (preferredLeft > parentRect.width - popupRect.width) { + // place popup so its right aligns to parent's right + // i.e., left = parent width - popup width + setLeft(parentRect.width - popupRect.width); + return; + } + + // if centering would overflow left -> stick popup fully to left=0 + if (preferredLeft < 0) { + setLeft(0); + return; + } + + // fallback center + setLeft(leftIfCentered); + }); + }, [visible, align, boundaryRef]); return (
@@ -88,16 +156,18 @@ const HoverPopup = ({ {visible && (
e.stopPropagation()} > {title &&
{title}
} -
{content}
)} diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index 1c95ec64..3ef8131b 100644 --- a/src/router/AppRoutes.jsx +++ b/src/router/AppRoutes.jsx @@ -60,7 +60,7 @@ import PaymentRequestPage from "../pages/PaymentRequest/PaymentRequestPage"; import RecurringExpensePage from "../pages/RecurringExpense/RecurringExpensePage"; import AdvancePaymentPage from "../pages/AdvancePayment/AdvancePaymentPage"; import ServiceProjectDetail from "../pages/ServiceProject/ServiceProjectDetail"; -import ManageJob from "../components/ServiceProject/ManageJob"; +import ManageJob from "../components/ServiceProject/ServiceProjectJob/ManageJob"; const router = createBrowserRouter( [ {