diff --git a/src/components/Directory/ManageContact.jsx b/src/components/Directory/ManageContact.jsx index 56d45e32..f55ed858 100644 --- a/src/components/Directory/ManageContact.jsx +++ b/src/components/Directory/ManageContact.jsx @@ -23,7 +23,7 @@ import Label from "../common/Label"; const ManageContact = ({ contactId, closeModal }) => { // fetch master data const { buckets, loading: bucketsLoaging } = useBuckets(); - const { data:projects, loading: projectLoading } = useProjects(); + const { data: projects, loading: projectLoading } = useProjects(); const { contactCategory, loading: contactCategoryLoading } = useContactCategory(); const { organizationList } = useOrganization(); @@ -205,12 +205,12 @@ const ManageContact = ({ contactId, closeModal }) => { - setValue("organization", val, { shouldValidate: true })} - error={errors.organization?.message} -/> + setValue("organization", val, { shouldValidate: true })} + error={errors.organization?.message} + /> @@ -408,6 +408,7 @@ const ManageContact = ({ contactId, closeModal }) => { label="Tags" options={contactTags} isRequired={true} + placeholder="Enter Tag" /> {errors.tags && ( {errors.tags.message} @@ -482,7 +483,7 @@ const ManageContact = ({ contactId, closeModal }) => { - + diff --git a/src/components/Project/ProjectPermission.jsx b/src/components/Project/ProjectPermission.jsx index fa6bf1d2..a043603e 100644 --- a/src/components/Project/ProjectPermission.jsx +++ b/src/components/Project/ProjectPermission.jsx @@ -104,7 +104,6 @@ const hasChanges = permission: payloadPermissions, }; - console.log("Final payload:", payload); updatePermission(payload); }; diff --git a/src/components/RecurringExpense/ManageRecurringExpense.jsx b/src/components/RecurringExpense/ManageRecurringExpense.jsx index 4750c0cf..49fb3675 100644 --- a/src/components/RecurringExpense/ManageRecurringExpense.jsx +++ b/src/components/RecurringExpense/ManageRecurringExpense.jsx @@ -134,7 +134,6 @@ const ManageRecurringExpense = ({ closeModal, requestToEdit = null }) => { const StrikeDate = watch("strikeDate") const onSubmit = (fromdata) => { - console.log(fromdata); let payload = { ...fromdata, strikeDate: fromdata.strikeDate diff --git a/src/components/ServiceProject/ChangeStatus.jsx b/src/components/ServiceProject/ChangeStatus.jsx new file mode 100644 index 00000000..49b2fc70 --- /dev/null +++ b/src/components/ServiceProject/ChangeStatus.jsx @@ -0,0 +1,100 @@ +import SelectField from "../common/Forms/SelectField"; +import { useJobStatus } from "../../hooks/masterHook/useMaster"; +import { SpinnerLoader } from "../common/Loader"; +import Error from "../common/Error"; +import { z } from "zod"; +import { + AppFormController, + AppFormProvider, + 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"; + +export const ChangeStatusSchema = z.object({ + statusId: z.string().min(1, { message: "Please select status" }), +}); +const ChangeStatus = ({ statusId, projectId, jobId, popUpId }) => { + const { data, isLoading, isError, error } = useJobStatus(statusId, projectId); + const dispatch = useDispatch(); + const methods = useAppForm({ + resolver: zodResolver(ChangeStatusSchema), + defaultValues: { statusId: "" }, + }); + + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = methods; + + const { mutate: UpdateStatus, isPending } = useUpdateServiceProjectJob(() => { + // handleClose(); + }); + const onSubmit = (formData) => { + const payload = + [ + { + op: "replace", + path: "/statusId", + value: formData.statusId, + }, + ]; + + + UpdateStatus({ id: jobId, payload }); + }; + + const handleClose = () => { + dispatch(closePopup(popUpId)); + }; + return ( + +
+
+ ( + + )} + /> + + {errors.statusId && ( + {errors.statusId.message} + )} +
+ +
+ + + +
+
+
+ ); +}; + +export default ChangeStatus; diff --git a/src/components/ServiceProject/JobStatusLog.jsx b/src/components/ServiceProject/JobStatusLog.jsx index 44cd0bfb..6e97f9e9 100644 --- a/src/components/ServiceProject/JobStatusLog.jsx +++ b/src/components/ServiceProject/JobStatusLog.jsx @@ -2,7 +2,6 @@ import React from "react"; import Avatar from "../common/Avatar"; const JobStatusLog = ({ data }) => { - console.log(data); return (
diff --git a/src/components/ServiceProject/ManageJob.jsx b/src/components/ServiceProject/ManageJob.jsx index fad970c6..d9c7f54b 100644 --- a/src/components/ServiceProject/ManageJob.jsx +++ b/src/components/ServiceProject/ManageJob.jsx @@ -20,10 +20,11 @@ import { AppFormProvider, useAppForm, } from "../../hooks/appHooks/useAppForm"; -import { useServiceProjectJobContext } from "./Jobs"; +import { useParams } from "react-router-dom"; const ManageJob = ({ Job }) => { - const { manageJob, setManageJob } = useServiceProjectJobContext(); + const { projectId } = useParams(); + const methods = useAppForm({ resolver: zodResolver(jobSchema), defaultValues: defaultJobValue, @@ -69,27 +70,33 @@ const ManageJob = ({ Job }) => { formData.startDate = localToUtc(formData.startDate); formData.dueDate = localToUtc(formData.dueDate); + formData.projectId = projectId; CreateJob(formData); }; useEffect(() => { - if (manageJob.jobId && JobData) { - console.log("freshed data"); - const assignedEmployees = (JobData.assignees || []).map((e) => e.id); + if (!JobData && !Job) { reset({ - title: JobData.title ?? "", - description: JobData.description ?? "", - projectId: JobData.project.id ?? "", - assignees: assignedEmployees, - startDate: JobData.startDate ?? null, - dueDate: JobData.dueDate ?? null, - tags: JobData.tags ?? [], + ...defaultJobValue, + projectId: projectId, }); + return; } - if (!manageJob.jobId) { - reset(defaultJobValue); - } - }, [JobData, manageJob]); + + if (!JobData || !Job) return; + + const assignedEmployees = (JobData.assignees || []).map((e) => e.id); + + reset({ + title: JobData.title ?? "", + description: JobData.description ?? "", + projectId: JobData.project?.id ?? projectId, + assignees: assignedEmployees, + startDate: JobData.startDate ?? null, + dueDate: JobData.dueDate ?? null, + tags: JobData.tags ?? [], + }); + }, [JobData, Job, projectId]); return (
@@ -101,27 +108,10 @@ const ManageJob = ({ Job }) => { type="text" {...register("title")} className="form-control form-control" + placeholder="Enter Title" />
-
- ( - - )} - /> -
+
{ options={JobTags?.data} name="tags" label="Tag" + placeholder="Enter Tag" required />
diff --git a/src/components/ServiceProject/ManageJobTicket.jsx b/src/components/ServiceProject/ManageJobTicket.jsx index 5f29b125..b3ce01c7 100644 --- a/src/components/ServiceProject/ManageJobTicket.jsx +++ b/src/components/ServiceProject/ManageJobTicket.jsx @@ -8,8 +8,12 @@ import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; import JobStatusLog from "./JobStatusLog"; import JobComments from "./JobComments"; import { daysLeft } from "../../utils/appUtils"; +import HoverPopup from "../common/HoverPopup"; +import ChangeStatus from "./ChangeStatus"; +import { useParams } from "react-router-dom"; const ManageJobTicket = ({ Job }) => { + const { projectId } = useParams(); const { data, isLoading, isError, error } = useServiceProjectJobDetails( Job?.job ); @@ -31,7 +35,6 @@ const ManageJobTicket = ({ Job }) => { }, ]; - if (isLoading) return ; if (isError) return ( @@ -43,19 +46,36 @@ const ManageJobTicket = ({ Job }) => {
- {data?.status?.name} - {data?.dueDate && (() => { - const { days, color } = daysLeft(data?.startDate, data?.dueDate); - return ( - - Days Left : - - {days !== null ? `${days} days` : "N/A"} +
+ {data?.status?.name} + + } + > + + +
+ {data?.dueDate && + (() => { + const { days, color } = daysLeft(data?.startDate, data?.dueDate); + return ( + + Days Left : + + {days !== null ? `${days} days` : "N/A"} + -
- ); - })()} - + ); + })()}
{data?.title}
@@ -66,30 +86,31 @@ const ManageJobTicket = ({ Job }) => {

-
{" "} - Created Date : {formatUTCToLocalTime(data?.createdAt, true)} + + Created Date : {formatUTCToLocalTime(data?.createdAt, true)} +
- Start Date : {formatUTCToLocalTime(data?.startDate)} + Start Date :{" "} + {formatUTCToLocalTime(data?.startDate)} {" "} {" "} - Due on : {formatUTCToLocalTime(data?.startDate)} + Due on :{" "} + {formatUTCToLocalTime(data?.startDate)}
- - Created By - {" "} + Created By{" "} { />{" "}

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

- ({data?.createdBy?.jobRoleName}) + + ({data?.createdBy?.jobRoleName}) +
- - Assigned By - + Assigned By
{data?.assignees?.map((emp) => (
- +

{`${emp.firstName} ${emp.lastName}`}

- ({emp.jobRoleName}) + + ({emp.jobRoleName}) +
))} diff --git a/src/components/common/HoverPopup.jsx b/src/components/common/HoverPopup.jsx index 156a4e02..62e216bd 100644 --- a/src/components/common/HoverPopup.jsx +++ b/src/components/common/HoverPopup.jsx @@ -1,54 +1,74 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { closePopup, openPopup, togglePopup } from "../../slices/localVariablesSlice"; + +const HoverPopup = ({ id, title, content, children, Mode = "hover" }) => { + const dispatch = useDispatch(); + const visible = useSelector((s) => s.localVariables.popups[id] || false); -const HoverPopup = ({ title, content, children }) => { - const [visible, setVisible] = useState(false); const triggerRef = useRef(null); const popupRef = useRef(null); - // Toggle popup on hover or click - const handleMouseEnter = () => setVisible(true); - const handleClick = () => setVisible((prev) => !prev); + // Hover mode + const handleMouseEnter = () => { + if (Mode === "hover") dispatch(openPopup(id)); + }; - // Hide popup on outside click + const handleMouseLeave = () => { + if (Mode === "hover") dispatch(closePopup(id)); + }; + + // Click mode + const handleClick = (e) => { + if (Mode === "click") { + e.stopPropagation(); + dispatch(togglePopup(id)); + } + }; + + // Outside click handling useEffect(() => { - const handleDocumentClick = (e) => { + if (Mode !== "click" || !visible) return; + + const handleOutside = (e) => { if ( !popupRef.current?.contains(e.target) && !triggerRef.current?.contains(e.target) ) { - setVisible(false); + dispatch(closePopup(id)); } }; - if (visible) document.addEventListener("click", handleDocumentClick); - return () => document.removeEventListener("click", handleDocumentClick); - }, [visible]); + document.addEventListener("click", handleOutside); + return () => document.removeEventListener("click", handleOutside); + }, [visible, Mode, id]); return ( -
setVisible(false)} - onClick={handleClick} - style={{ cursor: "pointer" }} - > - {children} +
+ {/* Trigger element */} +
+ {children} +
+ {/* Popup */} {visible && (
e.stopPropagation()} // prevents closing when clicking inside > - {title && ( -
- {title} -
- )} -
- {content} -
+ {title &&
{title}
} + +
{content}
+ +
)}
diff --git a/src/components/common/TagInput.jsx b/src/components/common/TagInput.jsx index 3241d548..d0a015ae 100644 --- a/src/components/common/TagInput.jsx +++ b/src/components/common/TagInput.jsx @@ -2,7 +2,7 @@ import { useFormContext, useWatch } from "react-hook-form"; import React, { useEffect, useState } from "react"; import Label from "./Label"; -const TagInput = ({ label, name, placeholder, color = "#e9ecef", required=false, options = [] }) => { +const TagInput = ({ label, name, placeholder, color = "#e9ecef", required = false, options = [] }) => { const { setValue, watch } = useFormContext(); const tags = watch(name) || []; const [input, setInput] = useState(""); @@ -33,29 +33,29 @@ const TagInput = ({ label, name, placeholder, color = "#e9ecef", required=false, } }; -const handleChange = (e) => { - const val = e.target.value; - setInput(val); + const handleChange = (e) => { + const val = e.target.value; + setInput(val); - if (val) { - setSuggestions( - options - .filter((opt) => { - const label = typeof opt === "string" ? opt : opt.name; - return ( - label.toLowerCase().includes(val.toLowerCase()) && - !tags.some((t) => t.name === label) - ); - }) - .map((opt) => ({ - name: typeof opt === "string" ? opt : opt.name, - isActive: true, - })) - ); - } else { - setSuggestions([]); - } -}; + if (val) { + setSuggestions( + options + .filter((opt) => { + const label = typeof opt === "string" ? opt : opt.name; + return ( + label.toLowerCase().includes(val.toLowerCase()) && + !tags.some((t) => t.name === label) + ); + }) + .map((opt) => ({ + name: typeof opt === "string" ? opt : opt.name, + isActive: true, + })) + ); + } else { + setSuggestions([]); + } + }; const handleSuggestionClick = (sugg) => { handleAdd(sugg); @@ -105,6 +105,9 @@ const handleChange = (e) => { outline: "none", flex: 1, minWidth: "120px", + backgroundColor: "white", + color: "black" + }} />
diff --git a/src/hooks/masterHook/useMaster.js b/src/hooks/masterHook/useMaster.js index 978bf30e..ca5de298 100644 --- a/src/hooks/masterHook/useMaster.js +++ b/src/hooks/masterHook/useMaster.js @@ -296,13 +296,14 @@ export const useOrganizationType = () => { }); }; -export const useJobStatus=()=>{ +export const useJobStatus=(statusId,projectId)=>{ return useQuery({ - queryKey:["Job_Staus"], + queryKey:["Job_Staus",statusId,projectId], queryFn:async()=>{ - const resp = await MasterRespository.getJobStatus(); + const resp = await MasterRespository.getJobStatus(statusId,projectId); return resp.data; - } + }, + enabled:!!statusId && !!projectId }) } diff --git a/src/hooks/useServiceProject.jsx b/src/hooks/useServiceProject.jsx index 67679964..e92f9e02 100644 --- a/src/hooks/useServiceProject.jsx +++ b/src/hooks/useServiceProject.jsx @@ -190,7 +190,7 @@ export const useJobComments = (jobId, pageSize, pageNumber) => { return allPages.length + pageNumber; }, }); -}; +}; export const useJobTags = () => { return useQuery({ queryKey: ["Job_Tags"], @@ -253,4 +253,35 @@ export const useCreateServiceProjectJob = (onSuccessCallback) => { }, }); }; + +export const useUpdateServiceProjectJob = (onSuccessCallback) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ id, payload }) => { + + // Call the repository patch + const resp = await ServiceProjectRepository.UpdateJob( + id, + payload + ); + + return resp; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["serviceProjectJobs"] }); + queryClient.invalidateQueries({ queryKey: ["service-job"] }); + if (onSuccessCallback) onSuccessCallback(); + showToast("Job Updated successfully", "success"); + }, + onError: (error) => { + showToast( + error?.response?.data?.message || + error.message || + "Failed to update project", + "error" + ); + }, + }); +}; + //#endregion diff --git a/src/pages/ServiceProject/ServiceProjectDetail.jsx b/src/pages/ServiceProject/ServiceProjectDetail.jsx index ac5eecc5..f8de3964 100644 --- a/src/pages/ServiceProject/ServiceProjectDetail.jsx +++ b/src/pages/ServiceProject/ServiceProjectDetail.jsx @@ -5,8 +5,18 @@ import { ComingSoonPage } from "../Misc/ComingSoonPage"; import ServiceProjectProfile from "../../components/ServiceProject/ServiceProjectProfile"; import Jobs from "../../components/ServiceProject/Jobs"; import ProjectTeam from "../../components/ServiceProject/ServiceProjectTeam/ProjectTeam"; +import { useSelectedProject } from "../../slices/apiDataManager"; +import { useParams } from "react-router-dom"; +import { useServiceProject } from "../../hooks/useServiceProject"; const ServiceProjectDetail = () => { + const { projectId } = useParams(); + const { + data: projectdata, + isLoading: isProjectLoading, + isProjectError, + } = useServiceProject(projectId); + const [activePill, setActivePill] = useState( sessionStorage.getItem("servicePrjectTab") || "profile" ); @@ -18,21 +28,22 @@ const ServiceProjectDetail = () => { switch (activePill) { case "profile": return ; - case "teams": + case "teams": return ; - case "jobs": + case "jobs": return ; default: return ; } }; + return (
diff --git a/src/repositories/MastersRepository.jsx b/src/repositories/MastersRepository.jsx index 72cb91d9..72b3e2a0 100644 --- a/src/repositories/MastersRepository.jsx +++ b/src/repositories/MastersRepository.jsx @@ -150,8 +150,11 @@ export const MasterRespository = { getCurrencies: () => api.get(`/api/Master/currencies/list`), getRecurringStatus: () => api.get(`/api/Master/recurring-status/list`), + // Service Job JobTickets Status + getJobStatus: (statusId,projectId) => + api.get( + `/api/Master/job-status/list?statusId=${statusId}&projectId=${projectId}` + ), - getJobStatus: () => api.get("/api/Master/job-status/list"), - - getTeamRole:()=> api.get(`/api/Master/team-roles/list`), + getTeamRole: () => api.get(`/api/Master/team-roles/list`), }; diff --git a/src/repositories/ServiceProjectRepository.jsx b/src/repositories/ServiceProjectRepository.jsx index 97a445ec..94ef3c8e 100644 --- a/src/repositories/ServiceProjectRepository.jsx +++ b/src/repositories/ServiceProjectRepository.jsx @@ -31,4 +31,8 @@ export const ServiceProjectRepository = { `/api/ServiceProject/job/comment/list?jobTicketId=${jobTicketId}&pageSize=${pageSize}&pageNumber=${pageNumber}` ), GetJobTags: () => api.get(`/api/ServiceProject/job/tag/list`), + UpdateJob: (id, patchData) => + api.patch(`/api/ServiceProject/job/edit/${id}`, patchData, { + "Content-Type": "application/json-patch+json", + }), }; diff --git a/src/router/ProtectedRoute.jsx b/src/router/ProtectedRoute.jsx index 9179d43d..2a741df6 100644 --- a/src/router/ProtectedRoute.jsx +++ b/src/router/ProtectedRoute.jsx @@ -24,7 +24,6 @@ const validateToken = async () => { sessionStorage.getItem("refreshToken"); if (!refreshTokenStored){ - console.log("no refrh tokem"); removeSession() return false }; diff --git a/src/slices/localVariablesSlice.jsx b/src/slices/localVariablesSlice.jsx index e05326ad..2e2951ba 100644 --- a/src/slices/localVariablesSlice.jsx +++ b/src/slices/localVariablesSlice.jsx @@ -11,6 +11,9 @@ const localVariablesSlice = createSlice({ SelectedOrg: "", }, + // PopUp + popups: {}, + // Modal for all simple pass Name modals: { @@ -127,6 +130,19 @@ const localVariablesSlice = createSlice({ state.selfTenant.paymentDetailId = action.payload.paymentDetailId ?? state.selfTenant.paymentDetailId; }, + + openPopup: (state, action) => { + const id = action.payload; + state.popups[id] = true; + }, + closePopup: (state, action) => { + const id = action.payload; + state.popups[id] = false; + }, + togglePopup: (state, action) => { + const id = action.payload; + state.popups[id] = !state.popups[id]; + } }, }); @@ -145,6 +161,6 @@ export const { openModal, closeModal, toggleModal, - setSelfTenant, + setSelfTenant,openPopup, closePopup, togglePopup } = localVariablesSlice.actions; export default localVariablesSlice.reducer; diff --git a/src/utils/axiosClient.jsx b/src/utils/axiosClient.jsx index 0f2f14a2..40981633 100644 --- a/src/utils/axiosClient.jsx +++ b/src/utils/axiosClient.jsx @@ -178,6 +178,12 @@ export const api = { headers: { ...customHeaders }, authRequired: true, }), + patch: (url, data = {}, customHeaders = {}) => + apiRequest("patch", url, data, { + headers: { ...customHeaders }, + authRequired: true, + }), + }; // Redirect helper