@@ -61,7 +63,6 @@ const ManageJobTicket = ({ Job }) => {
{STATUS_JOB_CLOSED !== data?.status?.id && (
{
);
})()}
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 (
+
+ );
+};
+
+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(
[
{