initially setup service provider form

This commit is contained in:
pramod mahajan 2025-09-18 19:23:24 +05:30
parent 7fa2ca9227
commit 1452e77bc5
8 changed files with 386 additions and 111 deletions

View File

@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import React from "react"; import React, { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { import {
defaultOrganizationValues, defaultOrganizationValues,
@ -14,7 +14,11 @@ import Label from "../common/Label";
import SelectMultiple from "../common/SelectMultiple"; import SelectMultiple from "../common/SelectMultiple";
import { useServices } from "../../hooks/masterHook/useMaster"; import { useServices } from "../../hooks/masterHook/useMaster";
const ManageOrganization = () => { const ManageOrganization = ({
projectOrganizations = ["ee"],
organizationId = null,
}) => {
const [step, setStep] = useState(1); // 1 = Service Provider, 2 = Org Details, 3 = Add New Org
const orgModal = useOrganizationModal(); const orgModal = useOrganizationModal();
const { data: services, isLoading } = useServices(); const { data: services, isLoading } = useServices();
@ -34,123 +38,319 @@ const ManageOrganization = () => {
() => { () => {
reset(defaultOrganizationValues); reset(defaultOrganizationValues);
orgModal.onClose(); orgModal.onClose();
setStep(1); // reset to first step
} }
); );
const onSubmit = (OrgPayload) => { const onSubmit = (OrgPayload) => {
CreateOrganization(OrgPayload); CreateOrganization(OrgPayload);
}; };
const RenderTitle = useMemo(() => {
if (organizationId) {
return "Update Organization";
}
if (step === 1) {
return projectOrganizations && projectOrganizations.length > 0
? "Add Organization"
: "Find Organization";
}
if (step === 2) {
return "Organization Details";
}
if (step === 3) {
return "Create Organization";
}
return "Manage Organization"; // fallback
}, [step, orgModal?.orgData, organizationId]);
const contentBody = ( const contentBody = (
<FormProvider {...method}> <div>
<form className="form" onSubmit={handleSubmit(onSubmit)}> {/* ---------- STEP 1: Service Provider- Form Own Tenant list ---------- */}
<div className="mb-1 text-start"> {step === 1 && (
<Label htmlFor="name" required> <div className="d-block">
Organization Name <div className="text-start mb-1">
</Label> <Label className="text-secondary">Enter Service Provider ID</Label>
<input <input
className="form-control form-control-sm" type="text"
{...register("name")} className="form-control form-control-sm w-auto"
/> placeholder="SPR - ID"
{errors.name && ( aria-describedby="search-label"
<span className="danger-text">{errors.name.message}</span> />
)} </div>
</div>
<div className="mb-1 text-start"> <div className="py-2 text-start">
<Label htmlFor="contactPerson" required> {orgModal.orgData && (<p className="text-secondary">Don't have Service provder id, Select Service Provider <span className="text-primary text-decoration-underline cursor-pointer" onClick={()=>orgModal.orgData ? setStep(2):setStep(3)}>Choose Provider</span></p>
Contact Person )}
</Label> </div>
<input
className="form-control form-control-sm"
{...register("contactPerson")}
/>
{errors.contactPerson && (
<span className="danger-text">{errors.contactPerson.message}</span>
)}
</div>
<div className="mb-1 text-start"> {/* ======== org list ======*/}
<Label htmlFor="contactNumber" required> {/* <div className="list-group mt-3">
Contact Number <div
</Label> className="list-group-item list-group-item-action cursor-pointer"
<input onClick={() => setStep(2)}
className="form-control form-control-sm" >
{...register("contactNumber")} <i className="bx bx-building-house me-2"></i>
/> Sample Organization 1
{errors.contactNumber && ( </div>
<span className="danger-text">{errors.contactNumber.message}</span> </div> */}
)}
</div>
<div className="mb-1 text-start"> <div className="d-flex justify-content-center text-secondary mt-3">
<Label htmlFor="email" required> <button
Email Address type="button"
</Label> className="btn btn-xs btn-secondary"
<input onClick={() => setStep(4)}
className="form-control form-control-sm" >
{...register("email")} <i className="bx bx-plus-circle me-2"></i>
/> Add New Organization
{errors.email && ( </button>
<span className="danger-text">{errors.email.message}</span> </div>
)}
</div> </div>
)}
<div className="mb-1 text-start"> {/* ---------- STEP 1: Service Provider From Own Other Tenant ---------- */}
<SelectMultiple {step === 2 && (
name="serviceIds" <div className="d-block">
label="Services" {/* Optional: dropdown if projectOrganizations exist */}
required={true} {/* Optional: dropdown if projectOrganizations exist */}
valueKey="id" <p className="text-secondary">Select Tags</p>
options={services?.data || []} {/* ======== org list ======*/}
/> <div className="list-group mt-3">
{errors.serviceIds && ( <div
<span className="danger-text">{errors.serviceIds.message}</span> className="list-group-item list-group-item-action cursor-pointer"
)} onClick={() => setStep(3)}
</div> >
<i className="bx bx-building-house me-2"></i>
Sample Organization 1
</div>
</div>
<div className="mb-1 text-start"> <div className="d-flex justify-content-center text-secondary mt-3">
<Label htmlFor="address" required> <button
Address type="button"
</Label> className="btn btn-xs btn-secondary"
<textarea onClick={() => setStep(4)}
className="form-control form-control-sm" >
{...register("address")} <i className="bx bx-plus-circle me-2"></i>
rows={2} Add New Organization
/> </button>
{errors.address && ( </div>
<span className="danger-text">{errors.address.message}</span>
)}
</div> </div>
)}
<div className="d-flex justify-content-end gap-2 my-2"> {/* ---------- STEP 2: Existing Organization Details ---------- */}
<button {step === 3 && (
type="button" <div>
className="btn btn-sm btn-secondary" <p className="text-muted small">
onClick={orgModal.onClose} Show organization details here (from SPR list). User selects
disabled={isPending || isLoading} services and clicks Add.
> </p>
Cancel
</button> <div className="mb-2">
<button <Label>Services Offered</Label>
type="submit" <ul className="list-group">
className="btn btn-sm btn-primary" <li className="list-group-item">
disabled={isPending || isLoading} <input type="checkbox" className="form-check-input me-2" />
> Service 1
{isPending ? "Please Wait..." : "Submit"} </li>
</button> <li className="list-group-item">
<input type="checkbox" className="form-check-input me-2" />
Service 2
</li>
</ul>
</div>
<div className="d-flex justify-content-between mt-3">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => setStep(1)}
>
Back
</button>
<button type="button" className="btn btn-sm btn-primary">
Add
</button>
</div>
</div> </div>
</form> )}
</FormProvider>
{/* ---------- STEP 3: Add New Organization ---------- */}
{step === 4 && (
<FormProvider {...method}>
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="mb-1 text-start">
<Label htmlFor="name" required>
Organization Name
</Label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<span className="danger-text">{errors.name.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactPerson" required>
Contact Person
</Label>
<input
className="form-control form-control-sm"
{...register("contactPerson")}
/>
{errors.contactPerson && (
<span className="danger-text">
{errors.contactPerson.message}
</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="contactNumber" required>
Contact Number
</Label>
<input
className="form-control form-control-sm"
{...register("contactNumber")}
/>
{errors.contactNumber && (
<span className="danger-text">
{errors.contactNumber.message}
</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="email" required>
Email Address
</Label>
<input
className="form-control form-control-sm"
{...register("email")}
/>
{errors.email && (
<span className="danger-text">{errors.email.message}</span>
)}
</div>
<div className="mb-1 text-start">
<SelectMultiple
name="serviceIds"
label="Services"
required
valueKey="id"
options={services?.data || []}
/>
{errors.serviceIds && (
<span className="danger-text">{errors.serviceIds.message}</span>
)}
</div>
<div className="mb-1 text-start">
<Label htmlFor="address" required>
Address
</Label>
<textarea
className="form-control form-control-sm"
{...register("address")}
rows={2}
/>
{errors.address && (
<span className="danger-text">{errors.address.message}</span>
)}
</div>
<div className="d-flex justify-content-between gap-2 my-2">
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => setStep(1)}
>
Back
</button>
<div>
<button
type="button"
className="btn btn-sm btn-secondary me-2"
onClick={orgModal.onClose}
disabled={isPending || isLoading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isPending || isLoading}
>
{isPending ? "Please Wait..." : "Submit"}
</button>
</div>
</div>
</form>
</FormProvider>
)}
</div>
); );
return ( return (
<Modal <Modal
isOpen={orgModal.isOpen} isOpen={orgModal.isOpen}
onClose={orgModal.onClose} onClose={orgModal.onClose}
title="Manage Organization" title={RenderTitle}
body={contentBody} body={contentBody}
/> />
); );
}; };
export default ManageOrganization; export default ManageOrganization;
// <div className="d-flex flex-column gap-2 border-0 bg-none">
// <div className="list-group-item list-group-item-action d-flex align-items-center cursor-pointer border-0">
// <div className="d-flex align-items-center justify-content-center me-3">
// <i className="bx bx-building-house bx-md text-primary"></i>
// </div>
// <div className="w-100">
// <div className="d-flex justify-content-between">
// <div className="user-info text-start">
// <h6 className="mb-1 fw-normal">Icing sweet gummies</h6>
// <small className="text-body-secondary">15 minutes</small>
// <div className="user-status">
// <small>In Meeting</small>
// </div>
// </div>
// <div className="add-btn">
// <button className="btn btn-primary btn-sm">Add</button>
// </div>
// </div>
// </div>
// </div>
// {/* Icon item */}
// <div className="list-group-item list-group-item-action d-flex align-items-center cursor-pointer border-0">
// <div className="d-flex align-items-center justify-content-center me-3">
// <i className="bx bx-building-house bx-md text-primary"></i>
// </div>
// <div className="w-100">
// <div className="d-flex justify-content-between">
// <div className="user-info text-start">
// <h6 className="mb-1 fw-normal">Icing sweet gummies</h6>
// <small className="text-body-secondary">15 minutes</small>
// <div className="user-status">
// <small>In Meeting</small>
// </div>
// </div>
// <div className="add-btn">
// <button className="btn btn-primary btn-sm">Add</button>
// </div>
// </div>
// </div>
// </div>
// </div>

View File

@ -43,6 +43,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
}, },
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) }, { key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam }, { key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
]; ];
return ( return (
<div className="nav-align-top"> <div className="nav-align-top">

View File

@ -0,0 +1,30 @@
import React from "react";
import { useOrganizationModal } from "../../hooks/useOrganization";
import { useSelectedProject } from "../../slices/apiDataManager";
const ProjectOrganizations = () => {
const orgModal = useOrganizationModal();
const selectedProject = useSelectedProject()
return (
<div className="card">
<div className="d-flex justify-content-end px-2">
<button
type="button"
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
onClick={() => orgModal.onOpen(selectedProject)}
>
<i className="bx bx-plus-circle me-2"></i>
Add Organization
</button>
</div>
<div className="card-body">
<p className="text-secondary">
Not found Organization connected with current Project
</p>
</div>
</div>
);
};
export default ProjectOrganizations;

View File

@ -10,36 +10,57 @@ import showToast from "../services/toastService";
export const useOrganizationModal = () => { export const useOrganizationModal = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isOpen = useSelector( const { isOpen, orgData } = useSelector(
(state) => state.localVariables.OrganizationModal.isOpen (state) => state.localVariables.OrganizationModal
); );
return { return {
isOpen, isOpen,
onOpen: () => dispatch(openOrgModal()), orgData,
onOpen: (dat) =>
dispatch(openOrgModal({ isOpen: true, orgData: dat || null })),
onClose: () => dispatch(closeOrgModal()), onClose: () => dispatch(closeOrgModal()),
Togggle: () => dispatch(toggleOrgModal(isOpen)), onToggle: () => dispatch(toggleOrgModal()),
}; };
}; };
export const useOrganizationsList = (pageSize, pageNumber, active, sprid, searchString="") => { export const useOrganizationsList = (
pageSize,
pageNumber,
active,
sprid,
searchString = ""
) => {
return useQuery({ return useQuery({
queryKey: ["organizationList", pageSize, pageNumber, active, sprid, searchString], queryKey: [
queryFn: async() => { "organizationList",
const resp = await OrganizationRepository.getOrganizationList(pageSize, pageNumber, active, sprid, searchString); pageSize,
pageNumber,
active,
sprid,
searchString,
],
queryFn: async () => {
const resp = await OrganizationRepository.getOrganizationList(
pageSize,
pageNumber,
active,
sprid,
searchString
);
return resp.data; return resp.data;
}, },
keepPreviousData: true, keepPreviousData: true,
}); });
}; };
export const useCreateOrganization = (onSuccessCallback) => { export const useCreateOrganization = (onSuccessCallback) => {
const useClient = useQueryClient() const useClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (OrgPayload) => mutationFn: async (OrgPayload) =>
await OrganizationRepository.createOrganization(OrgPayload), await OrganizationRepository.createOrganization(OrgPayload),
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
useClient.invalidateQueries({queryKey:["organizationList"]}) useClient.invalidateQueries({ queryKey: ["organizationList"] });
showToast("Organization created successfully", "success"); showToast("Organization created successfully", "success");
if (onSuccessCallback) onSuccessCallback(); if (onSuccessCallback) onSuccessCallback();
}, },

View File

@ -69,7 +69,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
}; };
if (isError) return <div>{error.message}</div>; if (isError) return <div>{error.message}</div>;
if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />; // if (isLoading) return gridView ? <CardViewContactSkeleton /> : <ListViewContactSkeleton />;
return ( return (
<div className="row mt-5"> <div className="row mt-5">
@ -94,7 +94,7 @@ const ContactsPage = ({ projectId, searchText, onExport }) => {
) : ( ) : (
<div className="col-12"> <div className="col-12">
<ListViewContact <ListViewContact
data={data.data} data={data?.data}
Pagination={ Pagination={
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}

View File

@ -23,6 +23,7 @@ import DirectoryPage from "../Directory/DirectoryPage";
import { useProjectAccess } from "../../hooks/useProjectAccess"; // new import { useProjectAccess } from "../../hooks/useProjectAccess"; // new
import "./ProjectDetails.css"; import "./ProjectDetails.css";
import ProjectOrganizations from "../../components/Project/ProjectOrganizations";
const ProjectDetails = () => { const ProjectDetails = () => {
const projectId = useSelectedProject(); const projectId = useSelectedProject();
@ -96,6 +97,8 @@ const ProjectDetails = () => {
return <ProjectDocuments />; return <ProjectDocuments />;
case "setting": case "setting":
return <ProjectSetting />; return <ProjectSetting />;
case "organization":
return <ProjectOrganizations />;
default: default:
return <ComingSoonPage />; return <ComingSoonPage />;
} }

View File

@ -9,6 +9,8 @@ const OrganizationRepository = {
}searchString=${searchString}` }searchString=${searchString}`
); );
}, },
assignOrganizationToProject:(data)=>api.post(`/api/Organization/assign/project`,data)
}; };
export default OrganizationRepository; export default OrganizationRepository;

View File

@ -13,7 +13,8 @@ const localVariablesSlice = createSlice({
reload:false, reload:false,
OrganizationModal:{ OrganizationModal:{
isOpen:false isOpen:false,
orgData:null,
} }
}, },
@ -37,15 +38,32 @@ const localVariablesSlice = createSlice({
state.defaultDateRange = action.payload; state.defaultDateRange = action.payload;
}, },
openOrgModal: (state) => { openOrgModal: (state, action) => {
state.OrganizationModal.isOpen = true; debugger;
},
if (typeof action.payload === "boolean") {
state.OrganizationModal.isOpen = action.payload;
if (!action.payload) {
state.OrganizationModal.orgData = null;
}
} else if (typeof action.payload === "object") {
const { isOpen, orgData } = action.payload;
state.OrganizationModal.isOpen =
typeof isOpen === "boolean" ? isOpen : state.OrganizationModal.isOpen;
state.OrganizationModal.orgData =
orgData !== undefined ? orgData : state.OrganizationModal.orgData;
}
}
,
closeOrgModal: (state) => { closeOrgModal: (state) => {
state.OrganizationModal.isOpen = false; state.OrganizationModal.isOpen = false;
}, },
toggleOrgModal: (state) => { toggleOrgModal: (state) => {
state.OrganizationModal.isOpen = !state.OrganizationModal.isOpen; state.OrganizationModal.isOpen = !state.OrganizationModal.isOpen;
}, },
addedOrgModal:(state,action)=>{
state.OrganizationModal.orgData = action.payload;
}
}, },
}); });