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/PaymentRequest/ActionPaymentRequest.jsx b/src/components/PaymentRequest/ActionPaymentRequest.jsx index 5f3cc1d9..52222b55 100644 --- a/src/components/PaymentRequest/ActionPaymentRequest.jsx +++ b/src/components/PaymentRequest/ActionPaymentRequest.jsx @@ -40,7 +40,6 @@ const ActionPaymentRequest = ({ requestId }) => { error: PaymentModeError, } = usePaymentMode(); - console.log("Kartik", data) const IsReview = useHasUserPermission(REVIEW_EXPENSE); const [imageLoaded, setImageLoaded] = useState({}); diff --git a/src/components/PaymentRequest/PaymentRequestList.jsx b/src/components/PaymentRequest/PaymentRequestList.jsx index e533634f..0c9edfd5 100644 --- a/src/components/PaymentRequest/PaymentRequestList.jsx +++ b/src/components/PaymentRequest/PaymentRequestList.jsx @@ -85,7 +85,7 @@ const PaymentRequestList = ({ filters, filterData, removeFilterChip, clearFilter key: "paymentRequestUID", label: "Request ID", align: "text-start mx-2", - getValue: (e) => e.paymentRequestUID || "N/A", + getValue: (e) =>
{e.paymentRequestUID || "N/A"} {e.isAdvancePayment && Adv}
, }, { key: "title", diff --git a/src/components/PaymentRequest/ViewPaymentRequest.jsx b/src/components/PaymentRequest/ViewPaymentRequest.jsx index d8e936fe..7bf122db 100644 --- a/src/components/PaymentRequest/ViewPaymentRequest.jsx +++ b/src/components/PaymentRequest/ViewPaymentRequest.jsx @@ -148,7 +148,7 @@ const ViewPaymentRequest = ({ requestId }) => {
-
PR No : {data?.paymentRequestUID}
+
PR No : {data?.paymentRequestUID} {data.isAdvancePayment && Advance}
{ if (!workItem) return; - console.log(workItem) reset({ activityID: String( workItem?.workItem?.activityId || workItem?.activityMaster?.id 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/ServiceProfile.jsx b/src/components/ServiceProject/ServiceProfile.jsx new file mode 100644 index 00000000..30864106 --- /dev/null +++ b/src/components/ServiceProject/ServiceProfile.jsx @@ -0,0 +1,95 @@ +import React from 'react' +import { formatUTCToLocalTime } from '../../utils/dateUtils' + +const ServiceProfile = ({data,setIsOpenModal}) => { + return ( +
+
+
+ {" "} + + Project Profile +
+
+
+
    + +
  • +
    + + Name: +
    + +
    + {data.name} +
    +
  • +
  • +
    + + Nick Name: +
    + {data.shortName} +
  • +
  • +
    + + Assign Date: +
    + + {data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"} + +
  • + +
  • +
    + + Status: +
    + {data?.status.status} +
  • +
  • +
    + + Contact: +
    + {data.contactName} +
  • +
  • + {/* Label section with icon */} +
    + + Address: +
    + + {/* Content section that wraps nicely */} +
    + {data.address} +
    +
  • + + + +
  • + + + + + +
  • +
+ +
+
+ ) +} + +export default ServiceProfile diff --git a/src/components/ServiceProject/ServiceProjectBranch/BranchDetails.jsx b/src/components/ServiceProject/ServiceProjectBranch/BranchDetails.jsx new file mode 100644 index 00000000..2aefa4ad --- /dev/null +++ b/src/components/ServiceProject/ServiceProjectBranch/BranchDetails.jsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { useBranchDetails } from "../../../hooks/useServiceProject"; +import { SpinnerLoader } from "../../common/Loader"; +import Error from "../../common/Error"; +import { BranchDetailsSkeleton } from "../ServiceProjectSeketon"; + +const BranchDetails = ({ branch }) => { + const [copied, setCopied] = useState(false); + + const { data, isLoading, isError, error } = useBranchDetails(branch); + + 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/ServiceProjectBranch/ManageBranch.jsx b/src/components/ServiceProject/ServiceProjectBranch/ManageBranch.jsx new file mode 100644 index 00000000..720fa08f --- /dev/null +++ b/src/components/ServiceProject/ServiceProjectBranch/ManageBranch.jsx @@ -0,0 +1,359 @@ +import React, { useEffect } from "react"; +import { useProjectName } from "../../../hooks/useProjects"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Label from "../../common/Label"; +import { + useBranchDetails, + useCreateBranch, + useServiceProjects, + useUpdateBranch, +} from "../../../hooks/useServiceProject"; +import { useAppForm } from "../../../hooks/appHooks/useAppForm"; +import { useParams } from "react-router-dom"; +import { BranchSchema, defaultBranches } from "../ServiceProjectSchema"; + +const ManageBranch = ({ closeModal, BranchToEdit = null }) => { + const { + data, + isLoading, + isError, + error: requestError, + } = useBranchDetails(BranchToEdit); + + const [contacts, setContacts] = React.useState([ + { + contactPerson: "", + designation: "", + contactEmails: [""], + contactNumbers: [""] + } + ]); + + const { projectId } = useParams(); + const schema = BranchSchema(); + const { + register, + control, + watch, + handleSubmit, + setValue, + reset, + formState: { errors }, + } = useAppForm({ + resolver: zodResolver(schema), + defaultValues: { + ...defaultBranches, + projectId: projectId || "", + }, + }); + + const handleClose = () => { + reset(); + closeModal(); + }; + + useEffect(() => { + if (BranchToEdit && data) { + reset({ + branchName: data.branchName || "", + projectId: data.project?.id || projectId || "", + address: data.address || "", + branchType: data.branchType || "", + googleMapUrl: data.googleMapUrl || "", + }); + + if (data.contactInformation) { + try { + setContacts(JSON.parse(data.contactInformation)); + } catch { + setContacts([]); + } + } + } + }, [data, reset]); + + + const { mutate: CreateServiceBranch, isPending: createPending } = + useCreateBranch(() => { + handleClose(); + }); + const { mutate: ServiceBranchUpdate, isPending } = useUpdateBranch(() => + handleClose() + ); + + const onSubmit = (formdata) => { + let payload = { + ...data, + ...formdata, + projectId, + contactInformation: JSON.stringify(contacts), // ← important + }; + + if (BranchToEdit) { + ServiceBranchUpdate({ id: data.id, payload }); + } else { + CreateServiceBranch(payload); + } + }; + + + + return ( +
+
+ {BranchToEdit ? "Update Branch" : "Create Branch"} +
+
+
+
+ + + {errors.branchName && ( + {errors.branchName.message} + )} +
+
+ + + {errors.branchType && ( + {errors.branchType.message} + )} +
+
+ +
+ + +
+ + + {errors.googleMapUrl && ( + + {errors.googleMapUrl.message} + + )} +
+
+ +
+
+ + + {contacts.map((item, index) => ( +
+ + {/* Contact Person + Designation */} +
+
+ { + const list = [...contacts]; + list[index].contactPerson = e.target.value; + setContacts(list); + }} + /> +
+ +
+ { + const list = [...contacts]; + list[index].designation = e.target.value; + setContacts(list); + }} + /> +
+ + {/* Remove entire contact */} +
+ + setContacts(contacts.filter((_, i) => i !== index)) + } + > +
+
+ + {/* Numbers Section */} + + + {item.contactNumbers.map((num, numIndex) => ( +
+ + { + const value = e.target.value.replace(/\D/g, ""); // remove non-digit characters + const list = [...contacts]; + list[index].contactNumbers[numIndex] = value; + setContacts(list); + }} + /> + + {/* Show PLUS only on last row */} + {numIndex === item.contactNumbers.length - 1 ? ( + { + const list = [...contacts]; + list[index].contactNumbers.push(""); + setContacts(list); + }} + > + ) : ( + { + const list = [...contacts]; + list[index].contactNumbers.splice(numIndex, 1); + setContacts(list); + }} + > + )} + +
+ ))} + +
+ + {/* Emails Section */} + + + {item.contactEmails.map((email, emailIndex) => ( +
+ + { + const list = [...contacts]; + list[index].contactEmails[emailIndex] = e.target.value; + setContacts(list); + }} + /> + + {/* Show PLUS only on the last row */} + {emailIndex === item.contactEmails.length - 1 ? ( + { + const list = [...contacts]; + list[index].contactEmails.push(""); + setContacts(list); + }} + > + ) : ( + { + const list = [...contacts]; + list[index].contactEmails.splice(emailIndex, 1); + setContacts(list); + }} + > + )} + +
+ ))} + + +
+ ))} + + + + +
+
+ + +
+ +
+ + + + {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/ServiceProjectProfile.jsx b/src/components/ServiceProject/ServiceProjectProfile.jsx index dfda4880..6ef359ff 100644 --- a/src/components/ServiceProject/ServiceProjectProfile.jsx +++ b/src/components/ServiceProject/ServiceProjectProfile.jsx @@ -4,18 +4,27 @@ import { useServiceProject } from "../../hooks/useServiceProject"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; import ManageServiceProject from "./ManageServiceProject"; import GlobalModel from "../common/GlobalModel"; +import { SpinnerLoader } from "../common/Loader"; +import ServiceBranch from "./ServiceProjectBranch/ServiceBranch"; +import ServiceProfile from "./ServiceProfile"; const ServiceProjectProfile = () => { const { projectId } = useParams(); const [IsOpenModal, setIsOpenModal] = useState(false); const { data, isLoading, isError, error } = useServiceProject(projectId); - if (isLoading) { - return
Loadng.
; - } + if (isLoading) + return ( +
+ +
+ ); return ( <> {IsOpenModal && ( - setIsOpenModal(false)}> + setIsOpenModal(false)} + > setIsOpenModal(false)} @@ -24,98 +33,13 @@ const ServiceProjectProfile = () => { )}
-
-
-
-
- {" "} - - Project Profile -
-
-
-
    - -
  • -
    - - Name: -
    - - {/* Content section that wraps nicely */} -
    - {data.name} -
    -
  • -
  • -
    - - Nick Name: -
    - {data.shortName} -
  • -
  • -
    - - Assign Date: -
    - - {data.assignedDate ? formatUTCToLocalTime(data.assignedDate) : "NA"} - -
  • - -
  • -
    - - Status: -
    - {data?.status.status} -
  • -
  • -
    - - Contact: -
    - {data.contactName} -
  • -
  • - {/* Label section with icon */} -
    - - Address: -
    - - {/* Content section that wraps nicely */} -
    - {data.address} -
    -
  • - - - -
  • {/* Added mt-4 for some top margin */} - - {/* Added mt-4 for some top margin */} - - - - -
  • -
- -
-
+
+
- +
+ +
); diff --git a/src/components/ServiceProject/ServiceProjectSchema.jsx b/src/components/ServiceProject/ServiceProjectSchema.jsx index 5a5727b7..589f1497 100644 --- a/src/components/ServiceProject/ServiceProjectSchema.jsx +++ b/src/components/ServiceProject/ServiceProjectSchema.jsx @@ -50,6 +50,10 @@ export const defaultProjectValues = { //#endregion + + + + //#region JobSchema export const TagSchema = z.object({ @@ -70,6 +74,8 @@ export const jobSchema = z.object({ tags: z.array(TagSchema).optional().default([]), statusId: z.string().optional().nullable(), + + projectBranchId: z.string().optional().nullable(), }); const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB @@ -109,6 +115,52 @@ export const defaultJobValue = { startDate: null, dueDate: null, tags: [], + branchId: null, }; //#endregion + + + + + +//#region Branch + +export const BranchSchema = () => + z.object({ + projectId: z + .string() + .trim() + .min(1, { message: "Project is required" }), + + branchName: z + .string() + .trim() + .min(1, { message: "Branch Name is required" }), + + contactInformation: z.string().optional(), + address: z + .string() + .trim() + .min(1, { message: "Address is required" }), + + branchType: z + .string() + .trim() + .min(1, { message: "Branch Type is required" }), + + googleMapUrl: z + .string() + }); + +export const defaultBranches = { + branchName: "", + projectId: "", + contactInformation: "", + address: "", + branchType: "", + googleMapUrl: "", +}; + + +//#endregion \ No newline at end of file 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/Tenant/SubScriptionHistory.jsx b/src/components/Tenant/SubScriptionHistory.jsx index a587a84e..3972f3d8 100644 --- a/src/components/Tenant/SubScriptionHistory.jsx +++ b/src/components/Tenant/SubScriptionHistory.jsx @@ -104,8 +104,7 @@ const SubScriptionHistory = ({ tenantId }) => {
); }; + +export const SelectFieldSearch = ({ + label = "Select", + placeholder = "Select ", + required = false, + value = null, + onChange, + valueKey = "id", + labelKey = "name", + disabled = false, + isFullObject = false, + isMultiple = false, + hookParams, + useFetchHook, +}) => { + const [searchText, setSearchText] = useState(""); + const debounce = useDebounce(searchText, 300); + + const { data, isLoading } = useFetchHook(...hookParams, debounce); + const options = data?.data ?? []; + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + const getDisplayName = (entity) => { + if (!entity) return ""; + return `${entity[labelKey] || ""}`.trim(); + }; + + /** ----------------------------- + * SELECTED OPTION (SINGLE) + * ----------------------------- */ + 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); + }, []); + + // MERGED OPTIONS TO ENSURE SELECTED VALUE APPEARS EVEN IF NOT IN SEARCH RESULT + const [mergedOptions, setMergedOptions] = useState([]); + + useEffect(() => { + let finalList = [...options]; + + if (!isMultiple && value && !isFullObject) { + // already selected option inside options? + const exists = options.some((o) => o[valueKey] === value); + + // if selected item not found, try to get from props (value) as fallback + if (!exists && typeof value === "object") { + finalList.unshift(value); + } + } + + if (isMultiple && Array.isArray(value)) { + value.forEach((val) => { + const id = isFullObject ? val[valueKey] : val; + const exists = options.some((o) => o[valueKey] === id); + + if (!exists && typeof val === "object") { + finalList.unshift(val); + } + }); + } + + setMergedOptions(finalList); + }, [options, value]); + + /** ----------------------------- + * HANDLE SELECT + * ----------------------------- */ + 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 ( +
+ {label && ( + + )} + + {/* MAIN BUTTON */} + + + {/* DROPDOWN */} + {open && ( +
    +
    + setSearchText(e.target.value)} + className="form-control form-control-sm" + placeholder="Search..." + disabled={disabled} + /> +
    + + {isLoading && ( +
  • Loading...
  • + )} + + {!isLoading && options.length === 0 && ( +
  • + No results found +
  • + )} + + {!isLoading && + options.map((option) => { + const isActive = isMultiple + ? selectedList.some((x) => x[valueKey] === option[valueKey]) + : selectedSingle && + selectedSingle[valueKey] === option[valueKey]; + + return ( +
  • + +
  • + ); + })} +
+ )} +
+ ); +}; diff --git a/src/components/common/GlobalModal/CommentEditor.jsx b/src/components/common/GlobalModal/CommentEditor.jsx index 68d4e353..5d4dc626 100644 --- a/src/components/common/GlobalModal/CommentEditor.jsx +++ b/src/components/common/GlobalModal/CommentEditor.jsx @@ -28,7 +28,6 @@ const CommentEditor = () => { const [value, setValue] = useState(""); const handleSubmit = () => { - console.log("Comment:", value); // Submit or handle content }; 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/hooks/useProjects.js b/src/hooks/useProjects.js index 25caf14b..75467399 100644 --- a/src/hooks/useProjects.js +++ b/src/hooks/useProjects.js @@ -411,7 +411,6 @@ export const useUpdateProject = (onSuccessCallback) => { }, onError: (error) => { - console.log(error); showToast(error?.message || "Error while updating project", "error"); }, }); diff --git a/src/hooks/useServiceProject.jsx b/src/hooks/useServiceProject.jsx index 1e480118..93817800 100644 --- a/src/hooks/useServiceProject.jsx +++ b/src/hooks/useServiceProject.jsx @@ -59,8 +59,8 @@ export const useCreateServiceProject = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to delete task", + error.message || + "Failed to delete task", "error" ); }, @@ -84,8 +84,8 @@ export const useUpdateServiceProject = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to update project", + error.message || + "Failed to update project", "error" ); }, @@ -110,8 +110,8 @@ export const useActiveInActiveServiceProject = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to update project", + error.message || + "Failed to update project", "error" ); }, @@ -138,8 +138,8 @@ export const useAllocationServiceProjectTeam = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to update project", + error.message || + "Failed to update project", "error" ); }, @@ -181,7 +181,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => { ); return resp.data; }, - enabled:!!jobId, + enabled: !!jobId, initialPageParam: pageNumber, @@ -223,8 +223,8 @@ export const useAddCommentJob = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to update project", + error.message || + "Failed to update project", "error" ); }, @@ -247,8 +247,8 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to update project", + error.message || + "Failed to update project", "error" ); }, @@ -273,8 +273,8 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => { onError: (error) => { showToast( error?.response?.data?.message || - error.message || - "Failed to update project", + error.message || + "Failed to update project", "error" ); }, @@ -282,3 +282,113 @@ export const useUpdateServiceProjectJob = (onSuccessCallback) => { }; //#endregion + +//#region Branch +export const useBranches = ( + projectId, + isActive, + pageSize, + pageNumber, + searchString +) => { + return useQuery({ + queryKey: [ + "branches", + projectId, + isActive, + pageSize, + pageNumber, + searchString, + ], + queryFn: async () => { + const resp = await ServiceProjectRepository.GetBranchList( + projectId, + isActive, + pageSize, + pageNumber, + searchString + ); + return resp.data; + }, + enabled: !!projectId, + }); +}; + + +export const useBranchDetails = (id) => { + return useQuery({ + queryKey: ["branch", id], + queryFn: async () => { + const resp = await ServiceProjectRepository.GetBranchDetail(id); + return resp.data; + }, + enabled: !!id + }) +} + +export const useCreateBranch = (onSuccessCallBack) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (payload) => { + await ServiceProjectRepository.CreateBranch(payload); + }, + + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["branches"] }); + showToast("Branches Created Successfully", "success"); + if (onSuccessCallBack) onSuccessCallBack(); + }, + onError: (error) => { + showToast( + error.message || "Something went wrong please try again !", + "error" + ); + }, + }); +}; +export const useUpdateBranch = (onSuccessCallBack) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, payload }) => + await ServiceProjectRepository.UpdateBranch(id, payload), + + onSuccess: (_, variables) => { + // remove old single-branch cache + queryClient.removeQueries({ queryKey: ["branch", variables.id] }); + + // refresh list + queryClient.invalidateQueries({ queryKey: ["branches"] }); + + showToast("Branch updated successfully", "success"); + onSuccessCallBack?.(); + }, + + onError: () => { + showToast("Something went wrong. Please try again later.", "error"); + }, + }); +}; + + +export const useDeleteBranch = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, isActive}) => + await ServiceProjectRepository.DeleteBranch(id, isActive), + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["branches"] }); + showToast("Branch deleted successfully", "success"); + }, + + onError: (error) => { + showToast( + error?.response?.data?.message || + error.message || + "Failed to delete branch", + "error" + ); + }, + }); +}; diff --git a/src/repositories/ServiceProjectRepository.jsx b/src/repositories/ServiceProjectRepository.jsx index 94ef3c8e..23fe8b38 100644 --- a/src/repositories/ServiceProjectRepository.jsx +++ b/src/repositories/ServiceProjectRepository.jsx @@ -1,6 +1,8 @@ +import { isAction } from "@reduxjs/toolkit"; import { api } from "../utils/axiosClient"; export const ServiceProjectRepository = { + //#region Service Project CreateServiceProject: (data) => api.post("/api/ServiceProject/create", data), GetServiceProjects: (pageSize, pageNumber) => api.get( @@ -17,6 +19,8 @@ export const ServiceProjectRepository = { api.get( `/api/ServiceProject/get/allocation/list?projectId=${projectId}&isActive=${isActive} ` ), + //#endregion + //#region Job CreateJob: (data) => api.post(`/api/ServiceProject/job/create`, data), @@ -35,4 +39,22 @@ export const ServiceProjectRepository = { api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, { "Content-Type": "application/json-patch+json", }), + //#endregion + + //#region Project Branch + CreateBranch: (data) => api.post(`/api/ServiceProject/branch/create`, data), + UpdateBranch: (id, data) => + api.put(`/api/ServiceProject/branch/edit/${id}`, data), + + GetBranchList: (projectId, isActive, pageSize, pageNumber, searchString) => { + return api.get( + `/api/ServiceProject/branch/list/${projectId}?isActive=${isActive}&pageSize=${pageSize}&pageNumber=${pageNumber}&searchString=${searchString}` + ); + }, + + GetBranchDetail: (id) => api.get(`/api/ServiceProject/branch/details/${id}`), + DeleteBranch: (id, isActive = false) => + api.delete(`/api/ServiceProject/branch/delete/${id}?isActive=${isActive}`), + }; + diff --git a/src/router/AppRoutes.jsx b/src/router/AppRoutes.jsx index c5d37aa3..dc556a6e 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"; import AdvancePaymentPage1 from "../pages/AdvancePayment/AdvancePaymentPage1"; const router = createBrowserRouter( [