From 297e0712bc27a576730667f1def04315e049fa18 Mon Sep 17 00:00:00 2001 From: "pramod.mahajan" Date: Fri, 14 Nov 2025 02:51:29 +0530 Subject: [PATCH] integrated job details api and optimized form --- public/assets/css/core-extend.css | 5 + src/components/ServiceProject/JobList.jsx | 10 +- src/components/ServiceProject/Jobs.jsx | 97 +++++++++++------- src/components/ServiceProject/ManageJob.jsx | 84 +++++++--------- .../ServiceProject/ManageJobTicket.jsx | 71 ++++++++++++++ .../ServiceProject/ServiceProjectSchema.jsx | 1 + src/components/common/Forms/SelectField.jsx | 94 ++++++++++++++++++ .../common/Forms/SelectFieldServerSide.jsx | 98 +++++++++++++++++++ src/components/common/OffcanvasComponent.jsx | 67 +++++++++++++ src/hooks/appHooks/useAppForm.js | 6 ++ src/hooks/useServiceProject.jsx | 10 ++ src/repositories/ServiceProjectRepository.jsx | 1 + 12 files changed, 457 insertions(+), 87 deletions(-) create mode 100644 src/components/ServiceProject/ManageJobTicket.jsx create mode 100644 src/components/common/Forms/SelectField.jsx create mode 100644 src/components/common/Forms/SelectFieldServerSide.jsx create mode 100644 src/components/common/OffcanvasComponent.jsx create mode 100644 src/hooks/appHooks/useAppForm.js diff --git a/public/assets/css/core-extend.css b/public/assets/css/core-extend.css index 8c410a8a..ae7c4205 100644 --- a/public/assets/css/core-extend.css +++ b/public/assets/css/core-extend.css @@ -3,6 +3,11 @@ --bs-nav-link-font-size: 0.7375rem; --bg-border-color :#f8f6f6 } +.offcanvas.offcanvas-wide { + width: 700px !important; /* adjust as needed */ + max-width: 90vw; /* responsive fallback */ +} + /* ===========================% Background_Colors %========================================================== */ .bg-light-primary { background-color: color-mix(in srgb, var(--bs-primary) 10.4%, transparent); diff --git a/src/components/ServiceProject/JobList.jsx b/src/components/ServiceProject/JobList.jsx index f3f79b5f..425ffca4 100644 --- a/src/components/ServiceProject/JobList.jsx +++ b/src/components/ServiceProject/JobList.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { getNextBadgeColor } from "../../utils/appUtils"; import { useServiceProjectJobs } from "../../hooks/useServiceProject"; import { ITEMS_PER_PAGE } from "../../utils/constants"; @@ -6,8 +6,12 @@ import EmployeeAvatarGroup from "../common/EmployeeAvatarGroup"; import { formatUTCToLocalTime } from "../../utils/dateUtils"; import { SpinnerLoader } from "../common/Loader"; import { useParams } from "react-router-dom"; +import ProjectPage from "../../pages/project/ProjectPage"; +import { useServiceProjectJobContext } from "./Jobs"; + const JobList = ({ filterByProject }) => { + const {setSelectedJob} = useServiceProjectJobContext() const { id } = useParams(); const { data, isLoading, isError, error } = useServiceProjectJobs( ITEMS_PER_PAGE, @@ -55,7 +59,7 @@ const JobList = ({ filterByProject }) => { label: "Start Date", getValue: (e) => formatUTCToLocalTime(e.startDate), isAlwaysVisible: true, - className: "text-center ", + className: "text-center d-none d-sm-table-cell ", }, { key: "dueDate", @@ -108,7 +112,7 @@ const JobList = ({ filterByProject }) => {
{/* View always visible */} - diff --git a/src/components/ServiceProject/Jobs.jsx b/src/components/ServiceProject/Jobs.jsx index 88bfc902..69e20b61 100644 --- a/src/components/ServiceProject/Jobs.jsx +++ b/src/components/ServiceProject/Jobs.jsx @@ -1,48 +1,79 @@ -import React, { useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useState } from "react"; import JobList from "./JobList"; import { useNavigate } from "react-router-dom"; 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"; +export const JonContext = createContext(); +export const useServiceProjectJobContext = () => { + const context = useContext(JonContext); + if (!context) { + showToast("Something went wrong", "warning"); + window.location = "/dashboard"; + } + return context; +}; const Jobs = () => { + const [showCanvas, setShowCanvas] = useState(false); const [selectedProject, setSelectedProject] = useState(null); + const [selectJob,setSelectedJob] = useState({showCanvas:false,job:null}) const navigate = useNavigate(); - const { data } = useServiceProjects(ITEMS_PER_PAGE, 1); + + const contextProvider = { + setSelectedJob + } return ( -
-
-
-
- {" "} - -
-
- + <> + + setSelectedJob({showCanvas:false,job:null})} + > + + +
+
+
+
+ {" "} + +
+
+ +
+
+ +
- - -
-
+ + ); }; diff --git a/src/components/ServiceProject/ManageJob.jsx b/src/components/ServiceProject/ManageJob.jsx index 55b54239..5232e3ca 100644 --- a/src/components/ServiceProject/ManageJob.jsx +++ b/src/components/ServiceProject/ManageJob.jsx @@ -1,7 +1,6 @@ import React from "react"; import Breadcrumb from "../common/Breadcrumb"; import Label from "../common/Label"; -import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { defaultJobValue, jobSchema } from "./ServiceProjectSchema"; import { @@ -13,9 +12,15 @@ 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"; const ManageJob = () => { - const methods = useForm({ + const methods = useAppForm({ resolver: zodResolver(jobSchema), defaultValues: defaultJobValue, }); @@ -40,21 +45,6 @@ const ManageJob = () => { reset(); }); const onSubmit = (formData) => { - // if (serviceProjectId) { - // let existingServiceIds = projectdata?.services?.map((s) => s.id) || []; - - // const oldAssigneed = projectdata.services.map((service) => ({ - // serviceId: service.id, - // isActive: formData.services.includes(service.id), - // })); - - // const newAassigneed = formData.services - // .filter((s) => !existingServiceIds.includes(s)) - // .map((service) => ({ serviceId: service, isActive: true })); - - // formData.assignees = [...oldServices, ...newServices]; - // } - formData.assignees = formData.assignees.map((emp) => ({ employeeId: emp, isActive: true, @@ -62,9 +52,10 @@ const ManageJob = () => { formData.startDate = localToUtc(formData.startDate); formData.dueDate = localToUtc(formData.dueDate); - CreateJob(formData); + // CreateJob(formData); + console.log(formData); }; - + console.log(errors); return (
{ ]} />
-
+
- +

Create Job

-
+
-
- - + />
@@ -124,7 +106,7 @@ const ManageJob = () => { name="startDate" control={control} placeholder="DD-MM-YYYY" - className="w-full" + className="w-full form-control-md" />
@@ -165,7 +147,7 @@ const ManageJob = () => {
- +
diff --git a/src/components/ServiceProject/ManageJobTicket.jsx b/src/components/ServiceProject/ManageJobTicket.jsx new file mode 100644 index 00000000..3d422a83 --- /dev/null +++ b/src/components/ServiceProject/ManageJobTicket.jsx @@ -0,0 +1,71 @@ +import React 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"; + +const ManageJobTicket = ({ Job }) => { + const { data, isLoading, isError, error } = useServiceProjectJobDetails( + Job?.job + ); + if (isLoading) return ; + if (isError) + return ( +
+ +
+ ); + return ( +
+
+
{data?.title}
+
+
+ {" "} + {formatUTCToLocalTime(data?.createdAt, true)} +
+ {data?.status?.name} +
+
+

{data?.description}

+
+
+ + Start Date : {formatUTCToLocalTime(data?.startDate)} + {" "} + {" "} + + Due To : {formatUTCToLocalTime(data?.startDate)} + +
+
+
+ + Created By + {" "} + {" "} + {`${data?.createdBy?.firstName} ${data?.createdBy?.lastName}`} +
+
+ + Created By + +
+
+
+
+
+
+ +
+
+
+ ); +}; + +export default ManageJobTicket; diff --git a/src/components/ServiceProject/ServiceProjectSchema.jsx b/src/components/ServiceProject/ServiceProjectSchema.jsx index 9f660f14..349e8070 100644 --- a/src/components/ServiceProject/ServiceProjectSchema.jsx +++ b/src/components/ServiceProject/ServiceProjectSchema.jsx @@ -31,6 +31,7 @@ export const defaultProjectValues = { //#region JobSchema + export const TagSchema = z.object({ name: z.string().min(1, "Tag name is required"), isActive: z.boolean().default(true), diff --git a/src/components/common/Forms/SelectField.jsx b/src/components/common/Forms/SelectField.jsx new file mode 100644 index 00000000..a08eaf8c --- /dev/null +++ b/src/components/common/Forms/SelectField.jsx @@ -0,0 +1,94 @@ +import React, { useEffect, useRef, useState } from "react"; +import Label from "../Label"; + +const SelectField = ({ + label = "Select", + options = [], + placeholder = "Select Option", + required = false, + value, + onChange, + valueKey = "id", + labelKey = "name", + isLoading = false, +}) => { + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const selectedOption = options.find((opt) => opt[valueKey] === value); + + const displayText = selectedOption ? selectedOption[labelKey] : placeholder; + + const handleSelect = (option) => { + onChange(option[valueKey]); + setOpen(false); + }; + + const toggleDropdown = () => setOpen((prev) => !prev); + + return ( +
+ {label && ( + + )} + + + + {open && !isLoading && ( +
    + {options.map((option, i) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +}; + +export default SelectField; diff --git a/src/components/common/Forms/SelectFieldServerSide.jsx b/src/components/common/Forms/SelectFieldServerSide.jsx new file mode 100644 index 00000000..c0746775 --- /dev/null +++ b/src/components/common/Forms/SelectFieldServerSide.jsx @@ -0,0 +1,98 @@ +import React, { useEffect, useRef, useState } from "react"; +import Label from "../Label"; +import { useDebounce } from "../../../utils/appUtils"; + +const SelectFieldServerSide = ({ + label = "Select", + options = [], + placeholder = "Select Option", + required = false, + value, + onChange, + valueKey = "id", + labelKey = "name", + isLoading = false, +}) => { + const [searchText,setSeachText] = useState("") + const debounce = useDebounce(searchText,300); +// const {} = use + const [open, setOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const selectedOption = options.find((opt) => opt[valueKey] === value); + + const displayText = selectedOption ? selectedOption[labelKey] : placeholder; + + const handleSelect = (option) => { + onChange(option[valueKey]); + setOpen(false); + }; + + const toggleDropdown = () => setOpen((prev) => !prev); + + return ( +
+ {label && ( + + )} + + + + {open && !isLoading && ( +
    + {options.map((option, i) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +}; + +export default SelectFieldServerSide; diff --git a/src/components/common/OffcanvasComponent.jsx b/src/components/common/OffcanvasComponent.jsx new file mode 100644 index 00000000..970785ae --- /dev/null +++ b/src/components/common/OffcanvasComponent.jsx @@ -0,0 +1,67 @@ +import React, { useEffect, useRef } from "react"; + +const OffcanvasComponent = ({ + id = "globalOffcanvas", + title = "Offcanvas Title", + placement = "start", // start | end | top | bottom + children, + show = false, + onClose = () => {}, +}) => { + const offcanvasRef = useRef(null); + const bsInstance = useRef(null); + + useEffect(() => { + if (!offcanvasRef.current) return; + + // initialize once + bsInstance.current = bootstrap.Offcanvas.getOrCreateInstance(offcanvasRef.current); + + const el = offcanvasRef.current; + const handleHide = () => onClose(); + + el.addEventListener("hidden.bs.offcanvas", handleHide); + + return () => { + el.removeEventListener("hidden.bs.offcanvas", handleHide); + }; + }, [onClose]); + + // react to `show` changes + useEffect(() => { + if (!bsInstance.current) return; + + if (show) bsInstance.current.show(); + else bsInstance.current.hide(); + }, [show]); + + return ( +
+
+
+
+
{title}
+
+ + +
+ +
{children}
+
+
+ ); +}; + +export default OffcanvasComponent; diff --git a/src/hooks/appHooks/useAppForm.js b/src/hooks/appHooks/useAppForm.js new file mode 100644 index 00000000..ea751205 --- /dev/null +++ b/src/hooks/appHooks/useAppForm.js @@ -0,0 +1,6 @@ +import { useForm, Controller,FormProvider } from "react-hook-form"; + +export const useAppForm = (config) => useForm(config); +export const AppFormProvider = FormProvider; +export const AppFormController = Controller; + diff --git a/src/hooks/useServiceProject.jsx b/src/hooks/useServiceProject.jsx index 46f88f23..3e13b848 100644 --- a/src/hooks/useServiceProject.jsx +++ b/src/hooks/useServiceProject.jsx @@ -112,6 +112,16 @@ export const useServiceProjectJobs=(pageSize,pageNumber,isActive=true,project)=> } }) } +export const useServiceProjectJobDetails = (job)=>{ + return useQuery({ + queryKey:['service-job',job], + queryFn:async() =>{ + const resp = await ServiceProjectRepository.GetJobDetails(job); + return resp.data; + }, + enabled:!!job + }) +} export const useCreateServiceProjectJob = (onSuccessCallback) => { const queryClient = useQueryClient(); diff --git a/src/repositories/ServiceProjectRepository.jsx b/src/repositories/ServiceProjectRepository.jsx index 22fb4ba0..541d63de 100644 --- a/src/repositories/ServiceProjectRepository.jsx +++ b/src/repositories/ServiceProjectRepository.jsx @@ -19,4 +19,5 @@ export const ServiceProjectRepository = { api.get( `/api/ServiceProject/job/list?pageSize=${pageSize}&pageNumber=${pageNumber}&isActive=${isActive}&projectId=${projectId}` ), + GetJobDetails:(id)=>api.get(`/api/ServiceProject/job/details/${id}`) };