Added new field in contact create and update form

This commit is contained in:
ashutosh.nehete 2025-07-02 15:06:01 +05:30 committed by Kartik Sharma
parent 587029a766
commit d8f892e9f4
7 changed files with 273 additions and 63 deletions

View File

@ -123,6 +123,16 @@ const CardViewDirectory = ({
}}
>
<hr className="my-0" />
{contact.designation && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i class="fa-solid fa-id-badge ms-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.designation}
</li>
</ul>
)}
{contact.contactEmails[0] && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">

View File

@ -6,6 +6,7 @@ export const ContactSchema = z
contactCategoryId: z.string().nullable().optional(),
address: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
designation: z.string().min(1, {message:"Designation is requried"}),
projectIds: z.array(z.string()).nullable().optional(), // min(1, "Project is required")
contactEmails: z
.array(

View File

@ -14,7 +14,11 @@ import useMaster, {
} from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import { useBuckets, useOrganization } from "../../hooks/useDirectory";
import {
useBuckets,
useDesignation,
useOrganization,
} from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple";
import { ContactSchema } from "./DirectorySchema";
@ -33,8 +37,11 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { organizationList, loading: orgLoading } = useOrganization();
const { designationList, loading: designloading } = useDesignation();
const { contactTags, loading: Tagloading } = useContactTags();
const [IsSubmitting, setSubmitting] = useState(false);
const [showSuggestions,setShowSuggestions] = useState(false);
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
const dispatch = useDispatch();
const methods = useForm({
@ -45,6 +52,7 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
@ -106,6 +114,25 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
const watchBucketIds = watch("bucketIds");
// handle logic when input of desgination is changed
const handleDesignationChange = (e) => {
const val = e.target.value;
const matches = designationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
setFilteredDesignationList(matches);
setShowSuggestions(true);
setTimeout(() => setShowSuggestions(false), 5000);
};
// handle logic when designation is selected
const handleSelectDesignation = (val) => {
setShowSuggestions(false);
setValue("designation", val);
};
const toggleBucketId = (id) => {
const updated = watchBucketIds?.includes(id)
? watchBucketIds.filter((val) => val !== id)
@ -168,6 +195,55 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
/>
</div>
</div>
<div className="row mt-1">
<div className="col-md-6 text-start">
<label className="form-label">Designation</label>
<input
className="form-control form-control-sm"
{...register("designation")}
onChange={handleDesignationChange}
/>
{showSuggestions && filteredDesignationList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute bg-white border w-50 zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius: "0px",
}}
>
{filteredDesignationList.map((designation) => (
<li
key={designation}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectDesignation(designation)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{designation}
</li>
))}
</ul>
)}
{errors.designation && (
<small className="danger-text">
{errors.designation.message}
</small>
)}
</div>
</div>
<div className="row mt-1">
<div className="col-md-6">
{emailFields.map((field, index) => (
@ -381,13 +457,12 @@ const ManageDirectory = ({ submitContact, onCLosed }) => {
</div>
</li>
))}
</ul>
{errors.bucketIds && (
<small className="danger-text mt-0">
{errors.bucketIds.message}
</small>
)}
{errors.bucketIds && (
<small className="danger-text mt-0">
{errors.bucketIds.message}
</small>
)}
</div>
</div>

View File

@ -47,7 +47,8 @@ const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
<div className="d-flex flex-column text-start ms-1">
<span className="m-0 fw-semibold">{contact?.name}</span>
<small className="text-secondary small-text">
{contactProfile?.tags?.map((tag) => tag.name).join(" | ")}
{/* {contactProfile?.tags?.map((tag) => tag.name).join(" | ")} */}
{contactProfile?.designation}
</small>
</div>
</div>

View File

@ -14,7 +14,11 @@ import useMaster, {
} from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import { useBuckets, useOrganization } from "../../hooks/useDirectory";
import {
useBuckets,
useDesignation,
useOrganization,
} from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple";
import { ContactSchema } from "./DirectorySchema";
@ -32,10 +36,13 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { contactTags, loading: Tagloading } = useContactTags();
const [ IsSubmitting, setSubmitting ] = useState( false );
const [IsSubmitting, setSubmitting] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const dispatch = useDispatch();
const {organizationList} = useOrganization()
const { organizationList } = useOrganization();
const { designationList } = useDesignation();
const [showSuggestions, setShowSuggestions] = useState(false);
const [filteredDesignationList, setFilteredDesignationList] = useState([]);
const methods = useForm({
resolver: zodResolver(ContactSchema),
@ -45,6 +52,7 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
contactCategoryId: null,
address: "",
description: "",
designation: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
@ -95,6 +103,24 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
}
};
// handle logic when input of desgination is changed
const handleDesignationChange = (e) => {
const val = e.target.value;
const matches = designationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
setFilteredDesignationList(matches);
setShowSuggestions(true);
setTimeout(() => setShowSuggestions(false), 5000);
};
// handle logic when designation is selected
const handleSelectDesignation = (val) => {
setShowSuggestions(false);
setValue("designation", val);
};
const watchBucketIds = watch("bucketIds");
const toggleBucketId = (id) => {
@ -113,33 +139,28 @@ const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
};
const onSubmit = async (data) => {
const cleaned = {
...data,
contactEmails: (data.contactEmails || [])
.filter((e) => e.emailAddress?.trim() !== "")
.map((email, index) => {
const existingEmail = existingContact.contactEmails?.[index];
return existingEmail
? { ...email, id: existingEmail.id }
: email;
}),
contactPhones: (data.contactPhones || [])
.filter((p) => p.phoneNumber?.trim() !== "")
.map((phone, index) => {
const existingPhone = existingContact.contactPhones?.[index];
return existingPhone
? { ...phone, id: existingPhone.id }
: phone;
}),
};
const cleaned = {
...data,
contactEmails: (data.contactEmails || [])
.filter((e) => e.emailAddress?.trim() !== "")
.map((email, index) => {
const existingEmail = existingContact.contactEmails?.[index];
return existingEmail ? { ...email, id: existingEmail.id } : email;
}),
contactPhones: (data.contactPhones || [])
.filter((p) => p.phoneNumber?.trim() !== "")
.map((phone, index) => {
const existingPhone = existingContact.contactPhones?.[index];
return existingPhone ? { ...phone, id: existingPhone.id } : phone;
}),
};
setSubmitting(true);
await submitContact({ ...cleaned, id: existingContact.id });
setSubmitting(true);
await submitContact({ ...cleaned, id: existingContact.id });
setSubmitting(false);
};
const orgValue = watch("organization")
const orgValue = watch("organization");
const handleClosed = () => {
onCLosed();
};
@ -149,7 +170,7 @@ await submitContact({ ...cleaned, id: existingContact.id });
typeof existingContact === "object" &&
!Array.isArray(existingContact);
if (!isInitialized &&isValidContact && TagsData) {
if (!isInitialized && isValidContact && TagsData) {
reset({
name: existingContact.name || "",
organization: existingContact.organization || "",
@ -158,24 +179,30 @@ await submitContact({ ...cleaned, id: existingContact.id });
contactCategoryId: existingContact.contactCategory?.id || null,
address: existingContact.address || "",
description: existingContact.description || "",
designation: existingContact.designation || "",
projectIds: existingContact.projectIds || null,
tags: existingContact.tags || [],
bucketIds: existingContact.bucketIds || [],
} );
if (!existingContact.contactPhones || existingContact.contactPhones.length === 0) {
appendPhone({ label: "Office", phoneNumber: "" });
});
if (
!existingContact.contactPhones ||
existingContact.contactPhones.length === 0
) {
appendPhone({ label: "Office", phoneNumber: "" });
}
if (
!existingContact.contactEmails ||
existingContact.contactEmails.length === 0
) {
appendEmail({ label: "Work", emailAddress: "" });
}
setIsInitialized(true);
}
if (!existingContact.contactEmails || existingContact.contactEmails.length === 0) {
appendEmail({ label: "Work", emailAddress: "" });
}
setIsInitialized(true)
}
// return()=> reset()
}, [ existingContact, buckets, projects ] );
}, [existingContact, buckets, projects]);
return (
<FormProvider {...methods}>
@ -195,15 +222,14 @@ await submitContact({ ...cleaned, id: existingContact.id });
)}
</div>
<div className="col-md-6 text-start">
<label className="form-label">Organization</label>
<InputSuggestions
organizationList={organizationList}
value={getValues("organization") || ""}
onChange={(val) => setValue("organization", val)}
error={errors.organization?.message}
/>
organizationList={organizationList}
value={getValues("organization") || ""}
onChange={(val) => setValue("organization", val)}
error={errors.organization?.message}
/>
{errors.organization && (
<small className="danger-text">
{errors.organization.message}
@ -211,6 +237,55 @@ await submitContact({ ...cleaned, id: existingContact.id });
)}
</div>
</div>
<div className="row mt-1">
<div className="col-md-6 text-start">
<label className="form-label">Designation</label>
<input
className="form-control form-control-sm"
{...register("designation")}
onChange={handleDesignationChange}
/>
{showSuggestions && filteredDesignationList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-50 bg-white border zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius: "0px",
}}
>
{filteredDesignationList.map((designation) => (
<li
key={designation}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectDesignation(designation)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{designation}
</li>
))}
</ul>
)}
{errors.designation && (
<small className="danger-text">
{errors.designation.message}
</small>
)}
</div>
</div>
<div className="row mt-1">
<div className="col-md-6">
{emailFields.map((field, index) => (
@ -247,11 +322,13 @@ await submitContact({ ...cleaned, id: existingContact.id });
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary" onClick={handleAddEmail}/>
<i
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={handleAddEmail}
/>
) : (
// <button
// type="button"
@ -259,8 +336,10 @@ await submitContact({ ...cleaned, id: existingContact.id });
// onClick={() => removeEmail(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger" onClick={() => removeEmail(index)}/>
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
onClick={() => removeEmail(index)}
/>
)}
</div>
{errors.contactEmails?.[index]?.emailAddress && (
@ -310,7 +389,10 @@ await submitContact({ ...cleaned, id: existingContact.id });
// onClick={handleAddPhone}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary" onClick={handleAddPhone} />
<i
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={handleAddPhone}
/>
) : (
// <button
// type="button"
@ -318,7 +400,10 @@ await submitContact({ ...cleaned, id: existingContact.id });
// onClick={() => removePhone(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger" onClick={() => removePhone(index)} />
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger"
onClick={() => removePhone(index)}
/>
)}
</div>
{errors.contactPhones?.[index]?.phoneNumber && (
@ -348,7 +433,7 @@ await submitContact({ ...cleaned, id: existingContact.id });
</option>
) : (
<>
<option disabled value="">
<option disabled value="">
Select Category
</option>
{contactCategory?.map((cate) => (
@ -387,7 +472,7 @@ await submitContact({ ...cleaned, id: existingContact.id });
)}
</div>
<div className="row">
<div className="col-md-12 mt-1 text-start">
<div className="col-md-12 mt-1 text-start">
<label className="form-label ">Select Label</label>
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
@ -445,7 +530,11 @@ await submitContact({ ...cleaned, id: existingContact.id });
</div>
<div className="d-flex justify-content-center gap-1 py-2">
<button className="btn btn-sm btn-primary" type="submit" disabled={IsSubmitting}>
<button
className="btn btn-sm btn-primary"
type="submit"
disabled={IsSubmitting}
>
{IsSubmitting ? "Please Wait..." : "Update"}
</button>
<button

View File

@ -177,3 +177,36 @@ export const useOrganization = () => {
return { organizationList, loading, error };
};
export const useDesignation = () => {
const [designationList, setDesignationList] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchOrg = async () => {
const cacheOrg = getCachedData("designation");
if (cacheOrg?.length != 0) {
setLoading(true);
try {
const resp = await DirectoryRepository.GetDesignations();
cacheData("designation", resp.data);
setDesignationList(resp.data);
setLoading(false);
} catch (error) {
const msg =
error?.response?.data?.message ||
error?.message ||
"Something went wrong";
setError(msg);
}
} else {
setDesignationList(cacheOrg);
}
};
useEffect(() => {
fetchOrg();
}, []);
return { designationList, loading, error };
};

View File

@ -2,6 +2,7 @@ import { api } from "../utils/axiosClient";
export const DirectoryRepository = {
GetOrganizations: () => api.get("/api/directory/organization"),
GetDesignations: () => api.get("/api/directory/designations"),
GetContacts: (isActive, projectId) => {
const params = new URLSearchParams();
params.append("active", isActive);