added created, fetch and edit operration

This commit is contained in:
pramod.mahajan 2025-11-12 18:12:37 +05:30
parent 5c1b14bd38
commit 1bba9ab68a
9 changed files with 285 additions and 108 deletions

View File

@ -1,6 +1,10 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import moment from "moment"; import moment from "moment";
import { formatNumber, getDateDifferenceInDays } from "../../utils/dateUtils"; import {
formatNumber,
formatUTCToLocalTime,
getDateDifferenceInDays,
} from "../../utils/dateUtils";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects"; import { useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
import ManageProjectInfo from "./ManageProjectInfo"; import ManageProjectInfo from "./ManageProjectInfo";
@ -22,7 +26,7 @@ const ProjectCard = ({ project, isCore = true }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const ManageProject = useHasUserPermission(MANAGE_PROJECT); const ManageProject = useHasUserPermission(MANAGE_PROJECT);
const { setMangeProject } = useProjectContext(); const { setMangeProject, setManageServiceProject } = useProjectContext();
const getProgress = (planned, completed) => { const getProgress = (planned, completed) => {
return (completed * 100) / planned + "%"; return (completed * 100) / planned + "%";
@ -54,7 +58,7 @@ const ProjectCard = ({ project, isCore = true }) => {
} else { } else {
setManageServiceProject({ setManageServiceProject({
isOpen: true, isOpen: true,
Project: project.id, project: project.id,
}); });
} }
}; };
@ -138,14 +142,21 @@ const ProjectCard = ({ project, isCore = true }) => {
<span className="text-heading fw-medium"> <span className="text-heading fw-medium">
Contact Person:{" "} Contact Person:{" "}
</span> </span>
{project?.contactPerson ? project.contactPerson : "NA"} {isCore
? project?.contactPerson
? project.contactPerson
: "NA"
: project.contactName}
</p> </p>
<p className="mb-1"> <p className="mb-1">
<span className="text-heading fw-medium">Start Date: </span> <span className="text-heading fw-medium">
{project.startDate {isCore ? "Start Date:" : "Assigned Date"}{" "}
? moment(project?.startDate).format("DD-MMM-YYYY") </span>
: "NA"} {formatUTCToLocalTime(
isCore ? project?.startDate : project.assignedDate
)}
</p> </p>
{isCore && (
<p className="mb-1"> <p className="mb-1">
<span className="text-heading fw-medium">Deadline: </span> <span className="text-heading fw-medium">Deadline: </span>
@ -153,6 +164,7 @@ const ProjectCard = ({ project, isCore = true }) => {
? moment(project?.endDate).format("DD-MMM-YYYY") ? moment(project?.endDate).format("DD-MMM-YYYY")
: "NA"} : "NA"}
</p> </p>
)}
<p className="mb-0">{project?.projectAddress}</p> <p className="mb-0">{project?.projectAddress}</p>
</div> </div>
</div> </div>

View File

@ -1,9 +1,12 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { defaultProjectValues } from "./ServiceProjectSchema"; import { defaultProjectValues } from "./ServiceProjectSchema";
import Label from "../common/Label"; import Label from "../common/Label";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useGlobalServices } from "../../hooks/masterHook/useMaster"; import {
useGlobalServices,
useServices,
} from "../../hooks/masterHook/useMaster";
import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants"; import { ITEMS_PER_PAGE, PROJECT_STATUS } from "../../utils/constants";
import DatePicker from "../common/DatePicker"; import DatePicker from "../common/DatePicker";
@ -14,10 +17,18 @@ import {
useOrganizationsList, useOrganizationsList,
} from "../../hooks/useOrganization"; } from "../../hooks/useOrganization";
import { error } from "pdf-lib"; import { error } from "pdf-lib";
import { useCreateServiceProject } from "../../hooks/useServiceProject"; import {
useCreateServiceProject,
useServiceProject,
useUpdateServiceProject,
} from "../../hooks/useServiceProject";
const ManageServiceProject = ({ servieceProjectId }) => { const ManageServiceProject = ({ serviceProjectId, onClose }) => {
console.log(servieceProjectId) const {
data: projectdata,
isLoading: isProjectLoading,
isProjectError,
} = useServiceProject(serviceProjectId);
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const methods = useForm({ const methods = useForm({
resolver: zodResolver(projectSchema), resolver: zodResolver(projectSchema),
@ -27,22 +38,79 @@ const ManageServiceProject = ({ servieceProjectId }) => {
register, register,
handleSubmit, handleSubmit,
control, control,
reset,
formState: { errors }, formState: { errors },
} = methods; } = methods;
const { data, isError, isLoading } = useGlobalServices(); const { data, isError, isLoading } = useServices();
const { data: organization, isLoading: isLoadingOrganization } = const { data: organization, isLoading: isLoadingOrganization } =
useOrganizationsList(ITEMS_PER_PAGE, 1, true); useOrganizationsList(ITEMS_PER_PAGE, 1, true);
const { mutate: CreateServiceProject, isPending } = useCreateServiceProject(); const { mutate: CreateServiceProject, isPending } = useCreateServiceProject(
() => {
reset();
onClose();
}
);
const { mutate: UpdateServiceProject, isPending: isUpdating } =
useUpdateServiceProject(() => {
reset();
onClose();
});
const onSubmit = (formData) => { const onSubmit = (formData) => {
formData.services = formData.services.filter((s) => s.serviceId);
if (serviceProjectId) {
let existingServiceIds = projectdata?.services?.map((s) => s.id) || [];
const oldServices = projectdata.services.map((service) => ({
serviceId: service.id,
isActive: formData.services.includes(service.id),
}));
const newServices = formData.services
.filter((s) => !existingServiceIds.includes(s))
.map((service) => ({ serviceId: service, isActive: true }));
formData.services = [...oldServices, ...newServices];
} else {
formData.services = formData.services.map((service) => ({
serviceId: service,
isActive: true,
}));
}
if (serviceProjectId) {
let payload = { ...formData, id: serviceProjectId };
UpdateServiceProject({ id: serviceProjectId, payload });
} else {
CreateServiceProject(formData); CreateServiceProject(formData);
}
}; };
useEffect(() => {
if (projectdata) {
const activeServiceIds = (projectdata.services || [])
.map((s) => s.serviceId || s.id)
.filter(Boolean);
reset({
name: projectdata.name,
shortName: projectdata.shortName,
clientId: projectdata.client.id,
assignedDate: projectdata.assignedDate,
address: projectdata.address,
statusId: projectdata.status.id,
contactEmail: projectdata.contactEmail,
contactPhone: projectdata.contactPhone,
contactName: projectdata.contactName,
services: activeServiceIds,
});
}
}, [projectdata]);
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form className="px-3 py-2" onSubmit={handleSubmit(onSubmit)}> <form className="px-3 py-2" onSubmit={handleSubmit(onSubmit)}>
<div className=" text-center"> <div className=" text-center">
<h5>Create Project</h5> <h5>{serviceProjectId ? "Update Project":"Create Project"}</h5>
</div> </div>
<div className="row text-start"> <div className="row text-start">
<div className="col-12 mb-2"> <div className="col-12 mb-2">
@ -105,10 +173,13 @@ const ManageServiceProject = ({ servieceProjectId }) => {
options={data?.data} options={data?.data}
isLoading={isLoading} isLoading={isLoading}
name="services" name="services"
valueKey="id"
labelKey="name" labelKey="name"
label="Select Cient" valueKey="id"
label="Select Service"
/> />
{errors?.services && (
<span className="danger-text">{errors.services.message}</span>
)}
</div> </div>
<div className="col-12 mb-2"> <div className="col-12 mb-2">
<Label htmlFor="name" required> <Label htmlFor="name" required>
@ -205,16 +276,21 @@ const ManageServiceProject = ({ servieceProjectId }) => {
<div className="d-flex justify-content-end gap-4"> <div className="d-flex justify-content-end gap-4">
<button <button
className="btn btn-sm btn-outline-secondary" className="btn btn-sm btn-outline-secondary"
disabled={isPending} disabled={isPending || isUpdating}
onClick={onClose}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
disabled={isPending} disabled={isPending || isUpdating}
> >
{isPending ? "Please wait..." : "Submit"} {isPending || isUpdating
? "Please wait..."
: serviceProjectId
? "Update"
: "Submit"}
</button> </button>
</div> </div>
</form> </form>

View File

@ -4,25 +4,7 @@ export const projectSchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
shortName: z.string().min(1, "Short name is required"), shortName: z.string().min(1, "Short name is required"),
clientId: z.string().min(1, { message: "Client is required" }), clientId: z.string().min(1, { message: "Client is required" }),
services: z services:z.array(z.string()).min(1,{message:"At least one service required"}),
.any() // accept anything, then sanitize below
.transform((val) => {
if (!Array.isArray(val)) return [];
return val
.map((item) => {
if (typeof item === "string") {
return { serviceId: item, isActive: true };
}
if (item && typeof item === "object" && "serviceId" in item) {
return { serviceId: item.serviceId, isActive: !!item.isActive };
}
return null;
})
.filter(Boolean);
})
.refine((arr) => arr.length > 0, {
message: "At least one service is required",
}),
address: z.string().min(1, "Address is required"), address: z.string().min(1, "Address is required"),
assignedDate: z.string().min(1, { message: "Date is required" }), assignedDate: z.string().min(1, { message: "Date is required" }),
statusId: z.string().min(1, "Status is required"), statusId: z.string().min(1, "Status is required"),
@ -38,12 +20,7 @@ export const defaultProjectValues = {
name: "", name: "",
shortName: "", shortName: "",
clientId: "", clientId: "",
services: [ services: [],
{
serviceId:null,
isActive: true,
},
],
address: "", address: "",
assignedDate: "", assignedDate: "",
statusId: "", statusId: "",

View File

@ -1,32 +1,39 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ServiceProjectRepository } from "../repositories/ServiceProject"; import { ServiceProjectRepository } from "../repositories/ServiceProject";
import showToast from "../services/toastService"; import showToast from "../services/toastService";
export const useServiceProjects = (pageSize, pageNumber) => { export const useServiceProjects = (pageSize, pageNumber) => {
return useQuery({ return useQuery({
queryKey: ["serviceProjects", pageSize, pageNumber], queryKey: ["serviceProjects", pageSize, pageNumber],
queryFn: async () => { queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProjects(pageSize,pageNumber); const response = await ServiceProjectRepository.GetServiceProjects(
pageSize,
pageNumber
);
return response.data; return response.data;
}, },
}) });
} };
export const useServiceProject = (projectId) => { export const useServiceProject = (projectId) => {
return useQuery({ return useQuery({
queryKey: ["serviceProject", projectId], queryKey: ["serviceProject", projectId],
queryFn: async () => { queryFn: async () => {
const response = await ServiceProjectRepository.GetServiceProject(projectId); const response = await ServiceProjectRepository.GetServiceProject(
projectId
);
return response.data; return response.data;
}, },
}) enabled: !!projectId,
} });
};
export const useCreateServiceProject = (onSuccessCallback) => { export const useCreateServiceProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (playload) => mutationFn: async (playload) =>
await ServiceProjectRepository.CreateServiceProject(playload), await ServiceProjectRepository.CreateServiceProject(playload),
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
if (onSuccessCallback) onSuccessCallback();
showToast("Project created successfully", "success"); showToast("Project created successfully", "success");
}, },
onError: (error) => { onError: (error) => {
@ -39,3 +46,52 @@ export const useCreateServiceProject = (onSuccessCallback) => {
}, },
}); });
}; };
export const useUpdateServiceProject = (onSuccessCallback) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, payload }) => {
return await ServiceProjectRepository.UpdateServiceProject(id, payload);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
queryClient.invalidateQueries({
queryKey: ["serviceProject", variables.id],
});
if (onSuccessCallback) onSuccessCallback();
showToast("Project updated successfully", "success");
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};
export const useActiveInActiveServiceProject = () => {
return useMutation({
mutationFn: async (id, status) => {
return await ServiceProjectRepository.DeleteServiceProject(id, status);
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ["serviceProjects"] });
if (onSuccessCallback) onSuccessCallback();
showToast(
`Project ${updated ? "restored" : "deleted"} successfully`,
"success"
);
},
onError: (error) => {
showToast(
error?.response?.data?.message ||
error.message ||
"Failed to update project",
"error"
);
},
});
};

View File

@ -1,9 +1,12 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useProjectContext } from "../project/ProjectPage"; import { useProjectContext } from "../project/ProjectPage";
import { useServiceProjects } from "../../hooks/useServiceProject"; import { useActiveInActiveServiceProject, useServiceProjects } from "../../hooks/useServiceProject";
import { ITEMS_PER_PAGE } from "../../utils/constants"; import { ITEMS_PER_PAGE } from "../../utils/constants";
import ProjectCard from "../../components/Project/ProjectCard"; import ProjectCard from "../../components/Project/ProjectCard";
import Pagination from "../../components/common/Pagination"; import Pagination from "../../components/common/Pagination";
import GlobalModel from "../../components/common/GlobalModel";
import ManageServiceProject from "../../components/ServiceProject/ManageServiceProject";
import { SpinnerLoader } from "../../components/common/Loader";
const ServiceProjectDisplay = ({ listView }) => { const ServiceProjectDisplay = ({ listView }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -18,8 +21,41 @@ const ServiceProjectDisplay = ({ listView }) => {
setCurrentPage(page); setCurrentPage(page);
} }
}; };
const {mutate:DeleteProject,isPending} = useActiveInActiveServiceProject()
const handleActiveInactive =(projectId)=>{
}
if (isLoading)
return (
<div
className="page-min-h d-flex justify-content-center align-items-center"
style={{ minHeight: "80vh" }}
>
<SpinnerLoader />
</div>
);
if (isError)
return (
<div className="page-min-h d-flex justify-content-center align-items-center">
<p>{error.message}</p>
</div>
);
return ( return (
<div className="row"> <div className="row">
{/* <ConfirmModal
type="delete"
header="Delete Project"
message="Are you sure you want delete?"
onSubmit={handleActiveInactive}
onClose={() => setDeleteContact({ contactId: null, Open: false })}
loading={isPending}
paramData={deleteContact.contactId}
isOpen={deleteContact.Open}
/> */}
{listView ? ( {listView ? (
<p>List</p> <p>List</p>
) : ( ) : (
@ -27,6 +63,9 @@ const ServiceProjectDisplay = ({ listView }) => {
<ProjectCard project={project} isCore={false} /> <ProjectCard project={project} isCore={false} />
)) ))
)} )}
<div className="col-12 d-flex justify-content-start mt-3"> <div className="col-12 d-flex justify-content-start mt-3">
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
@ -34,6 +73,21 @@ const ServiceProjectDisplay = ({ listView }) => {
onPageChange={paginate} onPageChange={paginate}
/> />
</div> </div>
{manageServiceProject?.isOpen && (
<GlobalModel
size="md"
isOpen={manageServiceProject?.isOpen}
closeModal={() =>
setManageServiceProject({ isOpen: false, project: null })
}
>
<ManageServiceProject
serviceProjectId={manageServiceProject?.project}
onClose={()=>setManageServiceProject({ isOpen: false, project: null })}
/>
</GlobalModel>
)}
</div> </div>
); );
}; };

View File

@ -40,7 +40,7 @@ ul {
} }
ul li { ul li {
padding: 10px 10px 10px 0; /* padding: 10px 10px 10px 0; */
} }
li { li {

View File

@ -1,6 +1,10 @@
import React, { createContext, useContext, useEffect, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
import { ITEMS_PER_PAGE, MANAGE_PROJECT, PROJECT_STATUS } from "../../utils/constants"; import {
ITEMS_PER_PAGE,
MANAGE_PROJECT,
PROJECT_STATUS,
} from "../../utils/constants";
import ProjectListView from "../../components/Project/ProjectListView"; import ProjectListView from "../../components/Project/ProjectListView";
import GlobalModel from "../../components/common/GlobalModel"; import GlobalModel from "../../components/common/GlobalModel";
import ManageProjectInfo from "../../components/Project/ManageProjectInfo"; import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
@ -36,7 +40,10 @@ const ProjectPage = () => {
const [projectList, setProjectList] = useState([]); const [projectList, setProjectList] = useState([]);
const [listView, setListView] = useState(false); const [listView, setListView] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [coreProjects, setCoreProjects] = useState(true); const [coreProjects, setCoreProjects] = useState(() => {
const storedValue = sessionStorage.getItem('whichProjectDisplay');
return storedValue === 'true';
});
const HasManageProject = useHasUserPermission(MANAGE_PROJECT); const HasManageProject = useHasUserPermission(MANAGE_PROJECT);
const [selectedStatuses, setSelectedStatuses] = useState( const [selectedStatuses, setSelectedStatuses] = useState(
@ -47,9 +54,16 @@ const ProjectPage = () => {
setMangeProject, setMangeProject,
setManageServiceProject, setManageServiceProject,
manageProject, manageProject,
manageServiceProject manageServiceProject,
}; };
const handleToggleProject = (e) => setCoreProjects(e.target.checked);
const handleToggleProject = (e) => {
const checked = e.target.checked;
setCoreProjects(checked);
sessionStorage.setItem('whichProjectDisplay', String(checked));
};
return ( return (
<ProjectContext.Provider value={contextDispatcher}> <ProjectContext.Provider value={contextDispatcher}>
<div className="container-fluid"> <div className="container-fluid">
@ -80,7 +94,8 @@ const ProjectPage = () => {
<div className="d-flex gap-2 mb-2"> <div className="d-flex gap-2 mb-2">
<button <button
type="button" type="button"
className={`btn btn-sm p-1 ${!listView ? "btn-primary" : "btn-outline-primary" className={`btn btn-sm p-1 ${
!listView ? "btn-primary" : "btn-outline-primary"
}`} }`}
onClick={() => setListView(false)} onClick={() => setListView(false)}
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
@ -91,7 +106,8 @@ const ProjectPage = () => {
</button> </button>
<button <button
type="button" type="button"
className={`btn btn-sm p-1 ${listView ? "btn-primary" : "btn-outline-primary" className={`btn btn-sm p-1 ${
listView ? "btn-primary" : "btn-outline-primary"
}`} }`}
onClick={() => setListView(true)} onClick={() => setListView(true)}
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
@ -146,18 +162,18 @@ const ProjectPage = () => {
</div> </div>
{HasManageProject && ( {HasManageProject && (
<div className="ms-auto"> <div class="btn-group">
<button type="button" className="btn btn-primary">
<i className="bx bx-plus-circle me-2"></i> New Project
</button>
<button <button
className="btn btn-sm btn-primary"
type="button" type="button"
onClick={() => class="btn btn-primary dropdown-toggle dropdown-toggle-split"
setMangeProject({ isOpen: true, Project: null }) data-bs-toggle="dropdown"
} aria-haspopup="true"
aria-expanded="false"
> >
<i className="bx bx-plus-circle me-2"></i> <span class="visually-hidden">Toggle Dropdown</span>
<span className="d-none d-md-inline-block">
Add New Project
</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li <li

View File

@ -132,20 +132,6 @@ const ProjectsDisplay = ({ listView, searchTerm }) => {
</GlobalModel> </GlobalModel>
)} )}
{/* Service Project */}
{manageServiceProject?.isOpen && (
<GlobalModel
size="md"
isOpen={manageServiceProject?.isOpen}
closeModal={() =>
setManageServiceProject({ isOpen: false, Project: null })
}
>
<ManageServiceProject
serviceProjectId={manageServiceProject?.Project}
/>
</GlobalModel>
)}
</div> </div>
); );
}; };

View File

@ -6,5 +6,5 @@ export const ServiceProjectRepository = {
GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`), GetServiceProject: (id) => api.get(`/api/ServiceProject/details/${id}`),
UpdateServiceProject: (id, data) => UpdateServiceProject: (id, data) =>
api.put(`/api/ServiceProject/edit/${id}`, data), api.put(`/api/ServiceProject/edit/${id}`, data),
DeleteServiceProject: (id) => api.delete(`/api/ServiceProject/delete/{id}`), DeleteServiceProject: (id,isActive=false) => api.delete(`/api/ServiceProject/delete/${id}?isActive=${isActive}`,),
}; };