From 1c1a1da8a07d605dfc686f0a4ece8c54f525bca2 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Fri, 16 May 2025 19:07:15 +0530 Subject: [PATCH 1/7] removed backdrop --- src/components/common/GlobalModel.jsx | 37 ++++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/components/common/GlobalModel.jsx b/src/components/common/GlobalModel.jsx index 04d24d5a..bebdd5ca 100644 --- a/src/components/common/GlobalModel.jsx +++ b/src/components/common/GlobalModel.jsx @@ -12,28 +12,29 @@ const GlobalModel = ({ }) => { const modalRef = useRef(null); // Reference to the modal element - useEffect(() => { - const modalElement = modalRef.current; - const modalInstance = new window.bootstrap.Modal(modalElement); +useEffect(() => { + const modalElement = modalRef.current; + const modalInstance = new window.bootstrap.Modal(modalElement, { + backdrop: false // Disable backdrop + }); - // Show modal if isOpen is true - if (isOpen) { - modalInstance.show(); - } else { - modalInstance.hide(); - } + if (isOpen) { + modalInstance.show(); + } else { + modalInstance.hide(); + } - // Handle modal hide event to invoke the closeModal function - const handleHideModal = () => { - closeModal(); // Close the modal via React state - }; + const handleHideModal = () => { + closeModal(); + }; - modalElement.addEventListener('hidden.bs.modal', handleHideModal); + modalElement.addEventListener('hidden.bs.modal', handleHideModal); + + return () => { + modalElement.removeEventListener('hidden.bs.modal', handleHideModal); + }; +}, [isOpen, closeModal]); - return () => { - modalElement.removeEventListener('hidden.bs.modal', handleHideModal); - }; - }, [isOpen, closeModal]); // Dynamically set the modal size classes (modal-sm, modal-lg, modal-xl) const modalSizeClass = size ? `modal-${size}` : ''; // Default is empty if no size is specified -- 2.43.0 From 7cfe04de2e23d92f2d0389271da68255109ee0c8 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Fri, 16 May 2025 19:08:41 +0530 Subject: [PATCH 2/7] modified TagInput, for array an object --- src/components/common/TagInput.jsx | 171 ++++++++++++++++++++++------- 1 file changed, 134 insertions(+), 37 deletions(-) diff --git a/src/components/common/TagInput.jsx b/src/components/common/TagInput.jsx index 58f3d16f..cdc88afb 100644 --- a/src/components/common/TagInput.jsx +++ b/src/components/common/TagInput.jsx @@ -1,32 +1,89 @@ -import React, { useState, useEffect } from "react"; import { useFormContext } from "react-hook-form"; +import React, { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { changeMaster } from "../../slices/localVariablesSlice"; const TagInput = ({ label = "Tags", name = "tags", - placeholder = "Enter ... ", + placeholder = "Start typing to add...", color = "#e9ecef", + options = [], }) => { const [tags, setTags] = useState([]); const [input, setInput] = useState(""); - const { setValue } = useFormContext(); + const [suggestions, setSuggestions] = useState([]); + const { setValue, trigger } = useFormContext(); + const dispatch = useDispatch(); useEffect(() => { - setValue(name, tags); // sync to form when tags change - }, [tags, name, setValue]); + setValue( + name, + tags.map((tag) => ({ + id: tag.id ?? null, + name: tag.name, + })) + ); + }, [ tags, name, setValue ] ); - const addTag = (e) => { - e.preventDefault(); - const trimmed = input.trim(); - if (trimmed !== "" && !tags.includes(trimmed)) { - setTags([...tags, trimmed]); - setInput(""); + useEffect(() => { + if (input.trim() === "") { + setSuggestions([]); + } else { + const filtered = options?.filter( + (opt) => + opt?.name?.toLowerCase()?.includes(input?.toLowerCase()) && + !tags?.some((tag) => tag?.name === opt?.name) + ); + setSuggestions(filtered); + } + }, [input, options, tags]); + + const addTag = async ( tagObj ) => + { + if (!tags.some((tag) => tag.id === tagObj.id)) { + const cleanedTag = { + id: tagObj.id ?? null, + name: tagObj.name, + }; + const newTags = [...tags, cleanedTag]; + setTags(newTags); + setValue(name, newTags, { shouldValidate: true }); // ✅ only id + name + await trigger(name); + setInput(""); + setSuggestions([]); + } +}; + + + const handleInputKeyDown = (e) => { + if (e.key === "Enter" && input.trim() !== "") { + e.preventDefault(); + const existing = options.find( + (opt) => opt.name.toLowerCase() === input.trim().toLowerCase() + ); + const newTag = existing + ? existing + : { + id: null, + name: input.trim(), + description: input.trim(), + }; + addTag(newTag); // Call async function (not awaiting because it's UI input) + } else if (e.key === "Backspace" && input === "") { + setTags((prev) => prev.slice(0, -1)); } }; - const removeTag = (removeIndex) => { - const updated = tags.filter((_, i) => i !== removeIndex); - setTags(updated); + const handleSuggestionClick = (suggestion) => { + addTag(suggestion); + }; + + const removeTag = (indexToRemove) => { + const newTags = tags.filter((_, i) => i !== indexToRemove); + setTags(newTags); + setValue(name, newTags, { shouldValidate: true }); + trigger(name); }; const backgroundColor = color || "#f8f9fa"; @@ -34,32 +91,72 @@ const TagInput = ({ return ( <> - -
- {tags.map((tag, i) => ( - + {label} + + +
+
+ {tags.map((tag, index) => ( + + {tag.name} + removeTag(index)} + style={{ cursor: "pointer" }} + /> + + ))} + setInput(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={placeholder} style={{ - color: iconColor, - backgroundColor, - padding: "2px 3px", - borderRadius: "2px" + border: "none", + outline: "none", + flex: 1, + minWidth: "120px", + }} + onFocus={() => dispatch(changeMaster("Contact Tag"))} + /> +
+ + {suggestions.length > 0 && ( +
    - {tag} - removeTag(i)}> - - ))} - setInput(e.target.value)} - onKeyDown={(e) => (e.key === "Enter" ? addTag(e) : null)} - placeholder={placeholder} - style={{ outline: "none", minWidth: "120px" }} - /> + {suggestions.map((sugg, i) => ( +
  • handleSuggestionClick(sugg)} + style={{ cursor: "pointer", fontSize: "0.875rem" }} + > + {sugg.name} +
  • + ))} +
+ )}
); -- 2.43.0 From 085f45210edf55e2793b06cff99be453dfce5deb Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Fri, 16 May 2025 19:09:24 +0530 Subject: [PATCH 3/7] changed coulmn --- src/components/Directory/ListViewDirectory.jsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/Directory/ListViewDirectory.jsx b/src/components/Directory/ListViewDirectory.jsx index 5011d229..d9188fd2 100644 --- a/src/components/Directory/ListViewDirectory.jsx +++ b/src/components/Directory/ListViewDirectory.jsx @@ -26,10 +26,10 @@ const getPhoneIcon = (type) => { const ListViewDirectory = ({ contact }) => { return ( - {`${contact.name}`} + {`${contact.name}`} {/* Emails */} - +
{contact.contactEmails?.map((email, index) => ( @@ -52,20 +52,18 @@ const ListViewDirectory = ({ contact }) => {
- {/* Organization */} + {contact.organization} - {/* Tags */} +
- {contact.tags?.map((tag, index) => ( - {tag} - ))} + {contact?.contactCategory?.name }
{/* Actions */} - + -- 2.43.0 From a1ee9dac38157b1b31f408c735116872b15ed8b5 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Fri, 16 May 2025 19:13:56 +0530 Subject: [PATCH 4/7] added useBuckets hook and set up cached data if have --- src/hooks/useDirectory.js | 43 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDirectory.js b/src/hooks/useDirectory.js index f1e31c14..707200df 100644 --- a/src/hooks/useDirectory.js +++ b/src/hooks/useDirectory.js @@ -1,4 +1,4 @@ -import {useState} from "react" +import {useEffect, useState} from "react" import {DirectoryRepository} from "../repositories/DirectoryRepository"; import {cacheData, getCachedData} from "../slices/apiDataManager"; @@ -20,12 +20,16 @@ export const useDirectory = () => { const response = await DirectoryRepository.GetContacts(); setContacts( response.data ) - cacheData("contacts",response.data) + cacheData( "contacts", response.data ) + setLoading(false) } catch ( error ) { setError( error ); setLoading(false) } + } else + { + setContacts(cache_contacts) } } @@ -35,4 +39,39 @@ export const useDirectory = () => fetch() }, [] ) return {contacts,loading,error} +} + +export const useBuckets = () => +{ + const [ buckets, setbuckets ] = useState(); + const [ loading, setLoading ] = useState(); + const [ Error, setError ] = useState( '' ) + + const fetch = async() => + { const cache_buckets = getCachedData("buckets") + if ( !cache_buckets ) + { + setLoading(true) + try + { + const resp = await DirectoryRepository.GetBucktes(); + setbuckets( resp.data ); + cacheData( "bucktes", resp.data ) + setLoading(false) + } catch ( error ) + { + const msg = error.response.data.message || error.message || "Something wrong"; + setError(msg) + } + } else + { + setbuckets(cache_buckets) + } + } + + useEffect( () => + { + fetch() + }, [] ) + return {buckets,loading,Error} } \ No newline at end of file -- 2.43.0 From c7b5a0ba7d2d5369351806f7400b365146531ed9 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Fri, 16 May 2025 19:14:34 +0530 Subject: [PATCH 5/7] created new contact form --- src/components/Directory/ManageDirectory.jsx | 482 ++++++++++++++----- 1 file changed, 362 insertions(+), 120 deletions(-) diff --git a/src/components/Directory/ManageDirectory.jsx b/src/components/Directory/ManageDirectory.jsx index 5eae91af..2198c023 100644 --- a/src/components/Directory/ManageDirectory.jsx +++ b/src/components/Directory/ManageDirectory.jsx @@ -1,40 +1,82 @@ -import React, { useEffect } from "react"; -import { useForm, useFieldArray, FormProvider } from "react-hook-form"; +import React, { useEffect, useState } from "react"; +import { + useForm, + useFieldArray, + FormProvider, + useFormContext, +} from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import TagInput from "../common/TagInput"; import { z } from "zod"; +import IconButton from "../common/IconButton"; +import useMaster from "../../hooks/masterHook/useMaster"; +import { useDispatch, useSelector } from "react-redux"; +import { changeMaster } from "../../slices/localVariablesSlice"; +import { useBuckets } from "../../hooks/useDirectory"; +import {useProjects} from "../../hooks/useProjects"; -export const directorySchema = z.object({ - firstName: z.string().min(1, "First Name is required"), - lastName: z.string().min(1, "Last Name is required"), +export const ContactSchema = z.object({ + Name: z.string().min(1, "Name is required"), organization: z.string().min(1, "Organization name is required"), - type: z.string().min(1, "Type is required"), + ContactCategoryId: z.string().optional(), address: z.string().optional(), - description: z.string().min(1, { message: "Description is required" }), - email: z - .array(z.string().email("Invalid email")) - .nonempty("At least one email required"), - phone: z - .array(z.string().regex(/^\d{10}$/, "Phone must be 10 digits")) - .nonempty("At least one phone number is required"), - tags: z.array(z.string()).optional(), + description: z.string().min( 1, {message: "Description is required"} ), + ProjectId :z.string().optional(), + ContactEmails: z + .array( + z.object({ + label: z.string(), + emailAddress: z.string().email("Invalid email"), + }) + ) + .optional() + .default([]), + ContactPhones: z + .array( + z.object({ + label: z.string(), + phoneNumber: z.string().regex(/^\d{10}$/, "Phone must be 10 digits"), + }) + ) + .optional() + .default([]), + + tags: z + .array( + z.object({ + id: z.string().nullable(), + name: z.string(), + }) + ) + .optional(), + BucketIds: z.array(z.string()).optional(), }); +const ManageDirectory = ({submitContact,onCLosed}) => { + const selectedMaster = useSelector( + (store) => store.localVariables.selectedMaster + ); + const [categoryData, setCategoryData] = useState([]); + const [TagsData, setTagsData] = useState([]); + const { data, loading } = useMaster(); + const {buckets, loading: bucketsLoaging} = useBuckets(); + const {projects, loading: projectLoading} = useProjects(); + const [IsSubmitting,setSubmitting] = useState(false) + const dispatch = useDispatch(); - -const ManageDirectory = () => { const methods = useForm({ - resolver: zodResolver(directorySchema), + resolver: zodResolver(ContactSchema), defaultValues: { - firstName: "", - lastName: "", + Name: "", organization: "", - type: "", + ContactCategoryId: null, address: "", description: "", - email: [""], - phone: [""], + ProjectId:null, + ContactEmails: [], + ContactPhones: [], tags: [], + BucketIds: [], }, }); @@ -44,6 +86,9 @@ const ManageDirectory = () => { control, getValues, trigger, + setValue, + watch, + reset, formState: { errors }, } = methods; @@ -51,160 +96,357 @@ const ManageDirectory = () => { fields: emailFields, append: appendEmail, remove: removeEmail, - } = useFieldArray({ control, name: "email" }); + } = useFieldArray({ control, name: "ContactEmails" }); const { fields: phoneFields, append: appendPhone, remove: removePhone, - } = useFieldArray({ control, name: "phone" }); + } = useFieldArray({ control, name: "ContactPhones" }); useEffect(() => { - if (emailFields.length === 0) appendEmail(""); - if (phoneFields.length === 0) appendPhone(""); + if (emailFields.length === 0) appendEmail(""); + if (phoneFields.length === 0) appendPhone(""); }, [emailFields.length, phoneFields.length]); - const onSubmit = (data) => { - // console.log("Submitted:\n" + JSON.stringify(data, null, 2)); - }; + const handleAddEmail = async () => { - const emails = getValues("email"); + const emails = getValues("ContactEmails"); const lastIndex = emails.length - 1; - const valid = await trigger(`email.${lastIndex}`); - if (valid) appendEmail(""); + const valid = await trigger(`ContactEmails.${lastIndex}.emailAddress`); + if (valid) { + appendEmail({ label: "Work", emailAddress: "" }); + } }; const handleAddPhone = async () => { - const phones = getValues("phone"); + const phones = getValues("ContactPhones"); const lastIndex = phones.length - 1; - const valid = await trigger(`phone.${lastIndex}`); - if (valid) appendPhone(""); + const valid = await trigger(`ContactPhones.${lastIndex}.phoneNumber`); + if (valid) { + appendPhone({ label: "Office", phoneNumber: "" }); + } }; + useEffect(() => { + if (selectedMaster === "Contact Category") { + setCategoryData(data); + } else { + setTagsData(data); + } + }, [selectedMaster, data]); + + const watchBucketIds = watch("BucketIds"); + + const toggleBucketId = (id) => { + const updated = watchBucketIds?.includes(id) + ? watchBucketIds.filter((val) => val !== id) + : [...watchBucketIds, id]; + + setValue("BucketIds", updated, { shouldValidate: true }); + }; + const handleCheckboxChange = (id) => { + const updated = watchBucketIds.includes(id) + ? watchBucketIds.filter((i) => i !== id) + : [...watchBucketIds, id]; + + setValue("BucketIds", updated, { shouldValidate: true }); + }; + + + + + const onSubmit = ( data ) => + { + setSubmitting(true) + submitContact( data, reset, setSubmitting ) + + }; return (
+
+ {" "} +
Create New Contact
+
- - - {errors.firstName && {errors.firstName.message}} + + + {errors.Name && ( + {errors.Name.message} + )}
- - - {errors.lastName && {errors.lastName.message}} + + + {errors.organization && ( + + {errors.organization.message} + + )}
- -
- - - {errors.organization && {errors.organization.message}} -
- -
+
- - {emailFields.map((field, index) => (<> -
- - {index === emailFields.length - 1 ? ( - + ) : ( + + )} +
+ {errors.ContactEmails?.[index]?.emailAddress && ( + + {errors.ContactEmails[index].emailAddress.message} + + )} +
- {errors.email?.[index] && ( - - {errors.email[index]?.message} - - )} - ))} - -
- - {phoneFields.map((field, index) => (<> -
- - {index === phoneFields.length - 1 ? ( - + ) : ( + + )} +
+ {errors.ContactPhones?.[index]?.phoneNumber && ( + + {errors.ContactPhones[index].phoneNumber.message} + + )} +
- {errors.phone?.[ index ] && {errors.phone[ index ]?.message}} - ))} -
- - - {errors.type && {errors.type.message}} + + + {errors.ContactCategoryId && ( + {errors.ContactCategoryId.message} + )}
- + +
+
+
+
+ + +
+ {bucketsLoaging &&

Loading...

} + {buckets?.map((item) => ( +
+
+ handleCheckboxChange(item.id)} + /> + +
+
+ ))} +
+ + {errors.BucketIds && ( + {errors.BucketIds.message} + )} +
+ +
+ + + {errors.category && ( + {errors.category.message} + )}
-