added offcanavs for create and update

This commit is contained in:
pramod.mahajan 2025-11-14 16:39:46 +05:30
parent dab3c5fe52
commit 7190a3c190
8 changed files with 202 additions and 144 deletions

View File

@ -94,7 +94,6 @@ const JobComments = ({ data }) => {
const newFiles = files.filter((_, i) => i !== index);
setValue("attachments", newFiles, { shouldValidate: true });
};
console.log(watch("attachments"))
return (
<div className="row">
<div className="sticky-section bg-white p-3">

View File

@ -9,9 +9,8 @@ import { useParams } from "react-router-dom";
import ProjectPage from "../../pages/project/ProjectPage";
import { useServiceProjectJobContext } from "./Jobs";
const JobList = ({ filterByProject }) => {
const {setSelectedJob} = useServiceProjectJobContext()
const { setSelectedJob, setManageJob } = useServiceProjectJobContext();
const { id } = useParams();
const { data, isLoading, isError, error } = useServiceProjectJobs(
ITEMS_PER_PAGE,
@ -112,12 +111,22 @@ const JobList = ({ filterByProject }) => {
</button>
<div className="dropdown-menu dropdown-menu-end">
{/* View always visible */}
<button className="dropdown-item py-1" onClick={()=>setSelectedJob({showCanvas:true,job:row?.id})}>
<button
className="dropdown-item py-1"
onClick={() =>
setSelectedJob({ showCanvas: true, job: row?.id })
}
>
<i className="bx bx-detail bx-sm"></i> View
</button>
<>
<button className="dropdown-item py-1">
<button
className="dropdown-item py-1"
onClick={() =>
setManageJob({ isOpen: true, jobId: row?.id })
}
>
<i className="bx bx-edit bx-sm"></i> Edit
</button>
</>

View File

@ -20,15 +20,21 @@ export const useServiceProjectJobContext = () => {
return context;
};
const Jobs = () => {
const [manageJob, setManageJob] = useState({ isOpen: false, jobId: null });
const [showCanvas, setShowCanvas] = useState(false);
const [selectedProject, setSelectedProject] = useState(null);
const [selectJob,setSelectedJob] = useState({showCanvas:false,job:null})
const [selectJob, setSelectedJob] = useState({
showCanvas: false,
job: null,
});
const navigate = useNavigate();
const { data } = useServiceProjects(ITEMS_PER_PAGE, 1);
const contextProvider = {
setSelectedJob
}
setSelectedJob,
setSelectedProject,
setManageJob,
};
return (
<>
<JonContext.Provider value={contextProvider}>
@ -37,10 +43,19 @@ const Jobs = () => {
title="Job"
placement="end"
show={selectJob.showCanvas}
onClose={() => setSelectedJob({showCanvas:false,job:null})}
onClose={() => setSelectedJob({ showCanvas: false, job: null })}
>
<ManageJobTicket Job={selectJob} />
</OffcanvasComponent>
<OffcanvasComponent
id="customCanvas1"
title={`${manageJob.jobId ? "Update a Job" : "Create a new Job"} `}
placement="end"
show={manageJob.isOpen}
onClose={() => setManageJob({ isOpen: false, jobId: null })}
>
<ManageJob Job={manageJob.jobId} />
</OffcanvasComponent>
<div className="card page-min-h my-2 px-4">
<div className="row">
<div className="col-12 py-2 d-flex justify-content-between ">
@ -64,7 +79,7 @@ const Jobs = () => {
<div className="px-2">
<button
className="btn btn-sm btn-primary"
onClick={() => navigate("/service/job")}
onClick={() => setManageJob({ isOpen: true, jobId: null })}
>
<i className="bx bx-plus-circle bx-md me-2"></i>New Job
</button>
@ -74,7 +89,6 @@ const Jobs = () => {
<JobList filterByProject={selectedProject} />
</div>
</div>
</JonContext.Provider>
</>
);

View File

@ -1,10 +1,12 @@
import React from "react";
import React, { useEffect } from "react";
import Breadcrumb from "../common/Breadcrumb";
import Label from "../common/Label";
import { zodResolver } from "@hookform/resolvers/zod";
import { defaultJobValue, jobSchema } from "./ServiceProjectSchema";
import {
useCreateServiceProjectJob,
useJobTags,
useServiceProjectJobDetails,
useServiceProjects,
} from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants";
@ -19,7 +21,7 @@ import {
useAppForm,
} from "../../hooks/appHooks/useAppForm";
const ManageJob = () => {
const ManageJob = ({ Job }) => {
const methods = useAppForm({
resolver: zodResolver(jobSchema),
defaultValues: defaultJobValue,
@ -40,10 +42,23 @@ const ManageJob = () => {
isError: isProjectError,
error,
} = useServiceProjects(ITEMS_PER_PAGE, 1);
const {
data: JobTags,
isLoading: isTagLoading,
isError: isTagError,
error: tagError,
} = useJobTags();
const {
data: JobData,
isLoading: isJobLoading,
isError: isJobError,
error: jobError,
} = useServiceProjectJobDetails(Job);
const { mutate: CreateJob, isPending } = useCreateServiceProjectJob(() => {
reset();
});
const onSubmit = (formData) => {
formData.assignees = formData.assignees.map((emp) => ({
employeeId: emp,
@ -52,104 +67,107 @@ const ManageJob = () => {
formData.startDate = localToUtc(formData.startDate);
formData.dueDate = localToUtc(formData.dueDate);
// CreateJob(formData);
console.log(formData);
CreateJob(formData)
};
console.log(errors);
useEffect(() => {
if (!JobData || !Job) return;
const assignedEmployees = (JobData.assignees || []).map((e) => e.id);
reset({
title: JobData.title ?? "",
description: JobData.description ?? "",
projectId: JobData.project.id ?? "",
assignees: assignedEmployees,
startDate: JobData.startDate ?? null,
dueDate: JobData.dueDate ?? null,
tags: [],
});
}, [JobData]);
return (
<div className="container-fluid">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Sevice Projects", link: "/projects" },
{ label: "" || "Jobs", link: null },
]}
/>
<div className="card m-auto page-min-h">
<div className="col-md-4 col-12 p-3"></div>
<div className="col-md-8 p-3">
<AppFormProvider {...methods}>
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div>
<p className="fs-5 fw-medium">Create Job</p>
</div>
<div className="col-12 col-md-6 mb-2 ">
<Label required>Title</Label>
<input
type="text"
{...register("title")}
className="form-control form-control"
<div className="container">
<AppFormProvider {...methods}>
<form className="row text-start" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-6 mb-2 ">
<Label required>Title</Label>
<input
type="text"
{...register("title")}
className="form-control form-control"
/>
</div>
<div className="col-12 col-md-6 mb-2">
<AppFormController
name="projectId"
control={control}
render={({ field }) => (
<SelectField
label="Project"
options={data?.data}
placeholder="Choose a Project"
required
labelKeyKey="name"
valueKeyKey="id"
value={field.value}
onChange={field.onChange}
isLoading={isProjectLoading}
/>
</div>
<div className="col-12 col-md-6 mb-2">
<AppFormController
name="projectId"
control={control}
render={({ field }) => (
<SelectField
label="Project"
options={data?.data}
placeholder="Choose a Project"
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
name="startDate"
control={control}
placeholder="DD-MM-YYYY"
className="w-full form-control-md"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>End Date</Label>
<DatePicker
control={control}
minDate={watch("startDate")}
name="dueDate"
className="w-full"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>End Date</Label>
<PmsEmployeeInputTag
control={control}
name="assignees"
placeholder="Enter employee"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<TagInput name="tags" label="Tag" required />
</div>
<div className="col-12">
<Label required>Description</Label>
<textarea
{...register("description")}
className="form-control form-control-sm"
rows={3}
/>
</div>
<div className="d-flex flex-row-reverse my-2">
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending}
>
{isPending ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</AppFormProvider>
</div>
</div>
)}
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>Start Date</Label>
<DatePicker
name="startDate"
control={control}
placeholder="DD-MM-YYYY"
className="w-full form-control-md"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>End Date</Label>
<DatePicker
control={control}
minDate={watch("startDate")}
name="dueDate"
className="w-full"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<Label required>End Date</Label>
<PmsEmployeeInputTag
control={control}
name="assignees"
placeholder="Enter employee"
/>
</div>
<div className="col-12 col-md-6 mb-2 mb-md-4">
<TagInput
options={JobTags?.data}
name="tags"
label="Tag"
required
/>
</div>
<div className="col-12">
<Label required>Description</Label>
<textarea
{...register("description")}
className="form-control form-control-sm"
rows={3}
/>
</div>
<div className="d-flex flex-row-reverse my-4">
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending}
>
{isPending ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</AppFormProvider>
</div>
);
};

View File

@ -1,13 +1,13 @@
import React from 'react'
import React from "react";
const ServiceProjectNav = ({ onPillClick, activePill }) => {
const ProjectTab = [
const ProjectTab = [
{ key: "profile", icon: "bx bx-user", label: "Profile" },
{
{
key: "jobs",
icon: "bx bx-briefcase-alt",
label: "Jobs",
link:"/service/job",
link: "/service/job",
},
{ key: "teams", icon: "bx bx-group", label: "Teams" },
@ -16,19 +16,17 @@ const ServiceProjectNav = ({ onPillClick, activePill }) => {
icon: "bx bxs-contact",
label: "Directory",
},
];
return (
<div className="table-responsive">
<div className="table-responsive">
<div className="nav-align-top">
<ul className="nav nav-tabs">
{ProjectTab?.filter((tab) => !tab.hidden)?.map((tab) => (
<li key={tab.key} className="nav-item cursor-pointer">
<a
className={`nav-link ${activePill === tab.key ? "active cursor-pointer" : ""
} fs-6`}
className={`nav-link ${
activePill === tab.key ? "active cursor-pointer" : ""
} fs-6`}
onClick={(e) => {
e.preventDefault();
onPillClick(tab.key);
@ -42,7 +40,7 @@ const ServiceProjectNav = ({ onPillClick, activePill }) => {
</ul>
</div>
</div>
)
}
);
};
export default ServiceProjectNav
export default ServiceProjectNav;

View File

@ -1,5 +1,6 @@
import { z } from "zod";
//#region Service Project
export const projectSchema = z.object({
name: z.string().min(1, "Name is required"),
shortName: z.string().min(1, "Short name is required"),
@ -31,12 +32,15 @@ export const defaultProjectValues = {
contactEmail: "",
};
//#endregion
//#region JobSchema
export const TagSchema = z.object({
name: z.string().min(1, "Tag name is required"),
isActive: z.boolean().default(true),
});
export const jobSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
@ -57,27 +61,27 @@ const ALLOWED_TYPES = [
"image/jpg",
"image/jpeg",
];
export const JobCommentSchema = z.object({
comment: z.string().min(1, { message: "Please leave comment" }),
attachments: z
.array(
z.object({
fileName: z.string().min(1),
base64Data: z.string().nullable(),
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
}),
documentId: z.string().optional(),
fileSize: z.number().max(MAX_FILE_SIZE),
description: z.string().optional(),
isActive: z.boolean().default(true),
})
)
.optional()
.default([]),
.array(
z.object({
fileName: z.string().min(1),
base64Data: z.string().nullable(),
contentType: z.string().refine((val) => ALLOWED_TYPES.includes(val), {
message: "Only PDF, PNG, JPG, or JPEG files are allowed",
}),
documentId: z.string().optional(),
fileSize: z.number().max(MAX_FILE_SIZE),
description: z.string().optional(),
isActive: z.boolean().default(true),
})
)
.optional()
.default([]),
});
export const defaultJobValue = {
title: "",
description: "",

View File

@ -1,4 +1,9 @@
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { ServiceProjectRepository } from "../repositories/ServiceProjectRepository";
import showToast from "../services/toastService";
@ -121,12 +126,16 @@ export const useServiceProjectJobs = (
},
});
};
export const useJobComments = (jobId, pageSize, pageNumber ) => {
export const useJobComments = (jobId, pageSize, pageNumber) => {
return useInfiniteQuery({
queryKey: ["jobComments", jobId, pageSize],
queryFn: async ({ pageParam = pageNumber }) => {
const resp = await ServiceProjectRepository.GetJobComment(jobId, pageSize, pageParam);
const resp = await ServiceProjectRepository.GetJobComment(
jobId,
pageSize,
pageParam
);
return resp.data;
},
@ -139,6 +148,12 @@ export const useJobComments = (jobId, pageSize, pageNumber ) => {
},
});
};
export const useJobTags = () => {
return useQuery({
queryKey: ["Job_Tags"],
queryFn: async () => await ServiceProjectRepository.GetJobTags(),
});
};
export const useServiceProjectJobDetails = (job) => {
return useQuery({

View File

@ -21,8 +21,9 @@ export const ServiceProjectRepository = {
),
GetJobDetails: (id) => api.get(`/api/ServiceProject/job/details/${id}`),
AddComment: (data) => api.post("/api/ServiceProject/job/add/comment", data),
GetJobComment: (jobTicketId,pageSize,pageNumber) =>
GetJobComment: (jobTicketId, pageSize, pageNumber) =>
api.get(
`/api/ServiceProject/job/comment/list?jobTicketId=${jobTicketId}&pageSize=${pageSize}&pageNumber=${pageNumber}`
),
GetJobTags: () => api.get(`/api/ServiceProject/job/tag/list`),
};