added changes status fun

This commit is contained in:
pramod.mahajan 2025-11-17 17:06:05 +05:30
commit d95ba4c800
17 changed files with 349 additions and 139 deletions

View File

@ -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 }) => {
<Label htmlFor={"organization"} required>
Organization
</Label>
<InputSuggestions
organizationList={organizationList}
value={watch("organization") || ""}
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
error={errors.organization?.message}
/>
<InputSuggestions
organizationList={organizationList}
value={watch("organization") || ""}
onChange={(val) => setValue("organization", val, { shouldValidate: true })}
error={errors.organization?.message}
/>
</div>
</div>
@ -408,6 +408,7 @@ const ManageContact = ({ contactId, closeModal }) => {
label="Tags"
options={contactTags}
isRequired={true}
placeholder="Enter Tag"
/>
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
@ -482,7 +483,7 @@ const ManageContact = ({ contactId, closeModal }) => {
<button className="btn btn-sm btn-primary" type="submit" disabled={isPending}>
{isPending ? "Please Wait..." : "Submit"}
</button>
</div>
</form>
</FormProvider>

View File

@ -104,7 +104,6 @@ const hasChanges =
permission: payloadPermissions,
};
console.log("Final payload:", payload);
updatePermission(payload);
};

View File

@ -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

View File

@ -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 (
<AppFormProvider {...methods}>
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-2">
<AppFormController
name="statusId"
control={control}
render={({ field }) => (
<SelectField
label="Select Status"
options={data ?? []}
placeholder="Choose a Status"
required
labelKeyKey="name"
valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={isLoading}
className="m-0"
/>
)}
/>
{errors.statusId && (
<small className="danger-text">{errors.statusId.message}</small>
)}
</div>
<div className="d-flex flex-row justify-content-end gap-3">
<button
type="button"
onClick={handleClose}
className="btn btn-label-secondary btn-sm"
>
Cancel
</button>
<button type="submit" className="btn btn-primary btn-sm">
{isPending ? "Please wait" : "Save"}
</button>
</div>
</form>
</AppFormProvider>
);
};
export default ChangeStatus;

View File

@ -2,7 +2,6 @@ import React from "react";
import Avatar from "../common/Avatar";
const JobStatusLog = ({ data }) => {
console.log(data);
return (
<div className="card shadow-none p-0">
<div className="card-body p-0">

View File

@ -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 (
<div className="container">
@ -101,27 +108,10 @@ const ManageJob = ({ Job }) => {
type="text"
{...register("title")}
className="form-control form-control"
placeholder="Enter Title"
/>
</div>
<div className="col-12 col-md-6 mb-2">
<AppFormController
name="projectId"
control={control}
render={({ field }) => (
<SelectField
label="Status"
options={data?.data}
placeholder="Choose a Status"
required
labelKeyKey="name"
valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={isProjectLoading}
/>
)}
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>Start Date</Label>
<DatePicker
@ -155,6 +145,7 @@ const ManageJob = ({ Job }) => {
options={JobTags?.data}
name="tags"
label="Tag"
placeholder="Enter Tag"
required
/>
</div>

View File

@ -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 <SpinnerLoader />;
if (isError)
return (
@ -43,19 +46,36 @@ const ManageJobTicket = ({ Job }) => {
<div className="row text-start">
<div className="col-12">
<div className="d-flex justify-content-between align-items-center mb-3">
<span className="badge bg-label-primary">{data?.status?.name}</span>
{data?.dueDate && (() => {
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
return (
<span style={{ fontSize: "12px" }}>
<span className="fw-medium me-1">Days Left :</span>
<span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"}
<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"
content={
<ChangeStatus
statusId={data?.status?.id}
projectId={projectId}
jobId={Job?.job}
popUpId={"STATUS_CHANEG"}
/>
}
>
<i className="bx bx-edit bx-sm"></i>
</HoverPopup>
</div>
{data?.dueDate &&
(() => {
const { days, color } = daysLeft(data?.startDate, data?.dueDate);
return (
<span style={{ fontSize: "12px" }}>
<span className="fw-medium me-1">Days Left :</span>
<span className={`badge bg-${color}`}>
{days !== null ? `${days} days` : "N/A"}
</span>
</span>
</span>
);
})()}
);
})()}
</div>
<h6 className="fs-5 fw-semibold">{data?.title}</h6>
@ -66,30 +86,31 @@ const ManageJobTicket = ({ Job }) => {
</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>
<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)}
<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)}
<i className="bx bx-calendar"></i> Due on :{" "}
{formatUTCToLocalTime(data?.startDate)}
</span>
</div>
</div>
<div className="d-block mt-4 mb-3">
<div className="d-flex align-items-center mb-2">
<span className="fs-6 fw-medium me-5">
Created By
</span>{" "}
<span className="fs-6 fw-medium me-5">Created By</span>{" "}
<Avatar
size="xs"
firstName={data?.createdBy?.firstName}
@ -97,20 +118,26 @@ const ManageJobTicket = ({ Job }) => {
/>{" "}
<div className="d-flex flex-row align-items-center">
<p className="m-0">{`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`}</p>
<small className="text-secondary ms-1">({data?.createdBy?.jobRoleName})</small>
<small className="text-secondary ms-1">
({data?.createdBy?.jobRoleName})
</small>
</div>
</div>
<div className="d-flex align-items-center">
<small className="fs-6 fw-medium me-3">
Assigned By
</small>
<small className="fs-6 fw-medium me-3">Assigned By</small>
<div className="d-flex flex-row gap-3">
{data?.assignees?.map((emp) => (
<div className="d-flex flex-row ">
<Avatar size="xs" firstName={emp.firstName} lastName={emp.lastName} />
<Avatar
size="xs"
firstName={emp.firstName}
lastName={emp.lastName}
/>
<div className="d-flex flex-row align-items-center">
<p className="m-0">{`${emp.firstName} ${emp.lastName}`}</p>
<small className="text-secondary ms-1">({emp.jobRoleName})</small>
<small className="text-secondary ms-1">
({emp.jobRoleName})
</small>
</div>
</div>
))}

View File

@ -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 (
<div
className="d-inline-block position-relative text-capitalize"
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setVisible(false)}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
<div className="d-inline-block position-relative">
{/* Trigger element */}
<div
ref={triggerRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
{children}
</div>
{/* Popup */}
{visible && (
<div
ref={popupRef}
className="bg-white border rounded shadow-sm p-3 text-start position-absolute top-100 start-500 translate-middle mt-12"
className="bg-white border rounded shadow-sm p-3 position-absolute top-100 start-0 mt-2"
style={{ zIndex: 1000, width: "240px" }}
onClick={(e) => e.stopPropagation()} // prevents closing when clicking inside
>
{title && (
<h6 className="mb-2 fw-semibold text-dark" style={{ fontSize: "0.9rem" }}>
{title}
</h6>
)}
<div className="text-muted" style={{ fontSize: "0.85rem", lineHeight: "1.4" }}>
{content}
</div>
{title && <h6 className="fw-semibold mb-2">{title}</h6>}
<div>{content}</div>
</div>
)}
</div>

View File

@ -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"
}}
/>
</div>

View File

@ -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
})
}

View File

@ -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

View File

@ -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 <ServiceProjectProfile />;
case "teams":
case "teams":
return <ProjectTeam />;
case "jobs":
case "jobs":
return <Jobs />;
default:
return <ComingSoonPage />;
}
};
return (
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Sevice Projects", link: "/projects" },
// { label: projects_Details?.name || "Project", link: null },
{ label: projectdata?.name || "Project", link: null }
]}
/>
<div>

View File

@ -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`),
};

View File

@ -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",
}),
};

View File

@ -24,7 +24,6 @@ const validateToken = async () => {
sessionStorage.getItem("refreshToken");
if (!refreshTokenStored){
console.log("no refrh tokem");
removeSession()
return false
};

View File

@ -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;

View File

@ -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