From 5ccdf33c3544b61bed3afebeef776a8ff143ad95 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:11:09 +0530 Subject: [PATCH 01/16] added style for Tag list --- src/index.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.css b/src/index.css index dff2b166..8758bf11 100644 --- a/src/index.css +++ b/src/index.css @@ -164,4 +164,7 @@ padding: 1px !important; .accordion-button:not(.collapsed) .toggle-icon { content: "\f146"; /* minus-circle */ +} +.hoverBox:hover{ + background-color: #f1f3f5; } \ No newline at end of file -- 2.43.0 From b33fad1b171d1cd5330126a7f92ebd74d55864c9 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:12:12 +0530 Subject: [PATCH 02/16] created style file SelectMultiple component --- src/components/common/MultiSelectDropdown.css | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/components/common/MultiSelectDropdown.css diff --git a/src/components/common/MultiSelectDropdown.css b/src/components/common/MultiSelectDropdown.css new file mode 100644 index 00000000..eae0fa94 --- /dev/null +++ b/src/components/common/MultiSelectDropdown.css @@ -0,0 +1,158 @@ +/* Container for the multi-select dropdown */ +.multi-select-dropdown-container { + position: relative; +} + + + +/* Header of the dropdown */ +.multi-select-dropdown-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px; + border: 1px solid #ddd; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +.multi-select-dropdown-header .placeholder-style { + color: #6c757d; + font-size: 14px; +} + +.multi-select-dropdown-header .placeholder-style-selected { + /* color: #0d6efd; */ + + font-size: 12px; +} + +/* Arrow icon */ +.multi-select-dropdown-arrow { + width: 14px; + height: 14px; + margin-left: 10px; +} + +/* Dropdown options */ +.multi-select-dropdown-options { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 10; + border: 1px solid #ddd; + border-radius: 3px; + background-color: white; + max-height: 250px; + overflow-y: auto; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); + margin-top: 5px; +} + +/* Search input */ +.multi-select-dropdown-search { + padding: 4px; + border-bottom: 1px solid #f1f3f5; +} + +.multi-select-dropdown-search-input { + width: 100%; + padding: 4px; + border: none; + outline: none; + background-color: #f8f9fa; + border-radius: 6px; + font-size: 14px; +} + +/* Select All checkbox */ +.multi-select-dropdown-select-all { + display: flex; + align-items: center; + padding: 4px; +} + +.multi-select-dropdown-select-all .custom-checkbox { + margin-right: 8px; +} + +.multi-select-dropdown-select-all-label { + font-size: 14px; + color: #333; +} + +/* Options in dropdown */ +.multi-select-dropdown-option { + display: flex; + align-items: center; + padding: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.multi-select-dropdown-option:hover { + background-color: #f1f3f5; +} + +.multi-select-dropdown-option.selected { + background-color: #dbe7ff; + color: #0d6efd; +} + +.multi-select-dropdown-option input[type="checkbox"] { + margin-right: 10px; +} + +/* Custom checkbox */ +.custom-checkbox { + width: 18px; + height: 18px; + border-radius: 4px; + border: 1px solid #ddd; + background-color: white; + cursor: pointer; + position: relative; + margin-right: 10px; +} + +.custom-checkbox:checked { + background-color: #0d6efd; + border-color: #0d6efd; +} + +.custom-checkbox:checked::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 10px; + height: 10px; + border-radius: 2px; +} +.multi-select-dropdown-Not-found{ + text-align: center; + padding: 1px 3px; +} +.multi-select-dropdown-Not-found:hover { + background: #e2dfdf; + + +} + +/* Responsive styles */ +@media (max-width: 767px) { + .multi-select-dropdown-container { + width: 100%; + } + + .multi-select-dropdown-header { + font-size: 12px; + } + + .multi-select-dropdown-options { + width: 100%; + max-height: 200px; + } +} -- 2.43.0 From c2ead3cd0f2e73ae8795378b0068cf6396005073 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:13:38 +0530 Subject: [PATCH 03/16] created new component SelectMultiple items --- src/components/common/SelectMultiple.jsx | 125 +++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/components/common/SelectMultiple.jsx diff --git a/src/components/common/SelectMultiple.jsx b/src/components/common/SelectMultiple.jsx new file mode 100644 index 00000000..e0c47760 --- /dev/null +++ b/src/components/common/SelectMultiple.jsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useFormContext } from 'react-hook-form'; +import './MultiSelectDropdown.css'; + +const SelectMultiple = ({ + name, + options = [], + label = 'Select options', + labelKey = 'name', + valueKey = 'id', + placeholder = 'Please select...', + IsLoading = false +}) => { + const { setValue, watch } = useFormContext(); + const selectedValues = watch(name) || []; + + const [isOpen, setIsOpen] = useState(false); + const [searchText, setSearchText] = useState(''); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleCheckboxChange = (value) => { + const updated = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + + setValue(name, updated, { shouldValidate: true }); + }; + + const filteredOptions = options.filter((item) => + item[labelKey]?.toLowerCase().includes(searchText.toLowerCase()) + ); + + return ( +
+ + +
setIsOpen((prev) => !prev)} +> + 0 + ? 'placeholder-style-selected' + : 'placeholder-style' + } + > +
+ {selectedValues.length > 0 ? ( + selectedValues.map((val) => { + const found = options.find((opt) => opt[valueKey] === val); + return ( + + {found ? found[labelKey] : ''} + + ); + }) + ) : ( + {placeholder} + )} +
+
+ +
+ + + {isOpen && ( +
+
+ setSearchText(e.target.value)} + className="multi-select-dropdown-search-input" + /> +
+ + {filteredOptions.map((item) => { + const labelVal = item[labelKey]; + const valueVal = item[valueKey]; + const isChecked = selectedValues.includes(valueVal); + + return ( +
+ handleCheckboxChange(valueVal)} + /> + +
+ ); + } )} + {!IsLoading && filteredOptions.length === 0 && ( +
+ +
+ )} + {IsLoading && filteredOptions.length === 0 && ( +
+ +
+ )} +
+ )} +
+ ); +}; + +export default SelectMultiple; -- 2.43.0 From 28a4f63d10378e90a22f237de0c38f2200b547fc Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:14:40 +0530 Subject: [PATCH 04/16] modfified TagInput for prevent multiple rendering --- src/components/common/TagInput.jsx | 85 ++++++++++++++---------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/components/common/TagInput.jsx b/src/components/common/TagInput.jsx index cdc88afb..c40a278f 100644 --- a/src/components/common/TagInput.jsx +++ b/src/components/common/TagInput.jsx @@ -1,30 +1,28 @@ -import { useFormContext } from "react-hook-form"; +import { useFormContext, useWatch } 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 = "Start typing to add...", + placeholder = "Start typing to add... like employee, manager", color = "#e9ecef", options = [], }) => { const [tags, setTags] = useState([]); const [input, setInput] = useState(""); const [suggestions, setSuggestions] = useState([]); - const { setValue, trigger } = useFormContext(); - const dispatch = useDispatch(); + const { setValue, trigger, control } = useFormContext(); + const watchedTags = useWatch({ control, name }); - useEffect(() => { - setValue( - name, - tags.map((tag) => ({ - id: tag.id ?? null, - name: tag.name, - })) - ); - }, [ tags, name, setValue ] ); + +useEffect(() => { + if ( + Array.isArray(watchedTags) && + JSON.stringify(tags) !== JSON.stringify(watchedTags) + ) { + setTags(watchedTags); + } +}, [JSON.stringify(watchedTags)]); useEffect(() => { if (input.trim() === "") { @@ -32,29 +30,34 @@ const TagInput = ({ } else { const filtered = options?.filter( (opt) => - opt?.name?.toLowerCase()?.includes(input?.toLowerCase()) && - !tags?.some((tag) => tag?.name === opt?.name) + 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 addTag = async (tagObj) => { + if (!tags.some((tag) => tag.name === tagObj.name)) { + const cleanedTag = { + id: tagObj.id ?? null, + name: tagObj.name, + }; + const newTags = [...tags, cleanedTag]; + setTags(newTags); + setValue(name, newTags, { shouldValidate: true }); + await trigger(name); + setInput(""); + setSuggestions([]); + } + }; + const removeTag = (indexToRemove) => { + const newTags = tags.filter((_, i) => i !== indexToRemove); + setTags(newTags); + setValue(name, newTags, { shouldValidate: true }); + trigger(name); + }; const handleInputKeyDown = (e) => { if (e.key === "Enter" && input.trim() !== "") { @@ -69,7 +72,7 @@ const TagInput = ({ name: input.trim(), description: input.trim(), }; - addTag(newTag); // Call async function (not awaiting because it's UI input) + addTag(newTag); } else if (e.key === "Backspace" && input === "") { setTags((prev) => prev.slice(0, -1)); } @@ -79,13 +82,6 @@ const TagInput = ({ 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"; const iconColor = `var(--bs-${color})`; @@ -120,6 +116,7 @@ const TagInput = ({ /> ))} + dispatch(changeMaster("Contact Tag"))} /> {suggestions.length > 0 && (
    {suggestions.map((sugg, i) => ( @@ -150,7 +147,8 @@ const TagInput = ({ key={i} className="dropdown-item p-1 hoverBox" onClick={() => handleSuggestionClick(sugg)} - style={{ cursor: "pointer", fontSize: "0.875rem" }} + style={{cursor: "pointer", fontSize: "0.875rem"}} + > {sugg.name} @@ -161,5 +159,4 @@ const TagInput = ({ ); }; - export default TagInput; -- 2.43.0 From 4d6171e1ed8459f694cbbf91cc4c658177c30c67 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:15:30 +0530 Subject: [PATCH 05/16] created new file for directory form schema --- src/components/Directory/DirectorySchema.js | 57 +++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/components/Directory/DirectorySchema.js diff --git a/src/components/Directory/DirectorySchema.js b/src/components/Directory/DirectorySchema.js new file mode 100644 index 00000000..45e2904a --- /dev/null +++ b/src/components/Directory/DirectorySchema.js @@ -0,0 +1,57 @@ +import { z } from "zod"; +export const ContactSchema = z + .object({ + name: z.string().min(1, "Name is required"), + organization: z.string().min(1, "Organization name is required"), + contactCategoryId: z.string().nullable().optional(), + address: z.string().optional(), + description: z.string().min(1, { message: "Description is required" }), + projectIds: z.array(z.string()).min(1, "Project is required"), + contactEmails: z + .array( + z.object({ + label: z.string(), + emailAddress: z.string().email("Invalid email").or(z.literal("")), + }) + ) + .optional() + .default([]), + + contactPhones: z + .array( + z.object({ + label: z.string(), + phoneNumber: z + .string() + .min(6, "Invalid Number") + .max(10, "Invalid Number") + .regex(/^[\d\s+()-]+$/, "Invalid phone number format").or(z.literal("")), + }) + ) + .optional() + .default([]), + + tags: z + .array( + z.object({ + id: z.string().nullable(), + name: z.string(), + }) + ) + .min(1, { message: "At least one tag is required" }), + bucketIds: z.array(z.string()).optional(), + }) + + .refine((data) => { + const hasValidEmail = (data.contactEmails || []).some( + (e) => e.emailAddress?.trim() !== "" + ); + const hasValidPhone = (data.contactPhones || []).some( + (p) => p.phoneNumber?.trim() !== "" + ); + + return hasValidEmail || hasValidPhone; +}, { + message: "At least one contact (email or phone) is required", + path: ["contactPhone"], +}); -- 2.43.0 From 79ad15d5729b2a8a7ab70cdf5d595602d3bfd703 Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:16:58 +0530 Subject: [PATCH 06/16] added new props, for triggering for open modal --- src/components/Directory/ListViewDirectory.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Directory/ListViewDirectory.jsx b/src/components/Directory/ListViewDirectory.jsx index d9188fd2..441aee45 100644 --- a/src/components/Directory/ListViewDirectory.jsx +++ b/src/components/Directory/ListViewDirectory.jsx @@ -23,7 +23,7 @@ const getPhoneIcon = (type) => { } }; -const ListViewDirectory = ({ contact }) => { +const ListViewDirectory = ({ contact,setSelectedContact,setIsOpenModal }) => { return ( {`${contact.name}`} @@ -40,7 +40,6 @@ const ListViewDirectory = ({ contact }) => { - {/* Phones */}
    {contact.contactPhones?.map((phone, index) => ( @@ -64,7 +63,11 @@ const ListViewDirectory = ({ contact }) => { {/* Actions */} - + + { + setSelectedContact( contact ) + setIsOpenModal(true) + }}> -- 2.43.0 From 8e06991bd380d364b241b3726e79744f83d6632b Mon Sep 17 00:00:00 2001 From: Pramod Mahajan Date: Sun, 18 May 2025 02:18:13 +0530 Subject: [PATCH 07/16] modified as per required changes. --- src/components/Directory/ManageDirectory.jsx | 277 +++++++++---------- 1 file changed, 128 insertions(+), 149 deletions(-) diff --git a/src/components/Directory/ManageDirectory.jsx b/src/components/Directory/ManageDirectory.jsx index 2198c023..cff228a6 100644 --- a/src/components/Directory/ManageDirectory.jsx +++ b/src/components/Directory/ManageDirectory.jsx @@ -7,76 +7,48 @@ import { } 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 useMaster, { + useContactCategory, + useContactTags, +} 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"; +import { useProjects } from "../../hooks/useProjects"; +import SelectMultiple from "../common/SelectMultiple"; +import {ContactSchema} from "./DirectorySchema"; -export const ContactSchema = z.object({ - Name: z.string().min(1, "Name is required"), - organization: z.string().min(1, "Organization name is required"), - ContactCategoryId: z.string().optional(), - address: 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 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 { buckets, loading: bucketsLoaging } = useBuckets(); + const { projects, loading: projectLoading } = useProjects(); + const { contactCategory, loading: contactCategoryLoading } = + useContactCategory(); + const { contactTags, loading: Tagloading } = useContactTags(); + const [IsSubmitting, setSubmitting] = useState(false); const dispatch = useDispatch(); const methods = useForm({ resolver: zodResolver(ContactSchema), defaultValues: { - Name: "", + name: "", organization: "", - ContactCategoryId: null, + contactCategoryId: null, address: "", description: "", - ProjectId:null, - ContactEmails: [], - ContactPhones: [], + projectIds: [], + contactEmails: [], + contactPhones: [], tags: [], - BucketIds: [], + bucketIds: [], }, }); @@ -96,93 +68,100 @@ const ManageDirectory = ({submitContact,onCLosed}) => { fields: emailFields, append: appendEmail, remove: removeEmail, - } = useFieldArray({ control, name: "ContactEmails" }); + } = useFieldArray({ control, name: "contactEmails" }); const { fields: phoneFields, append: appendPhone, remove: removePhone, - } = useFieldArray({ control, name: "ContactPhones" }); + } = useFieldArray({ control, name: "contactPhones" }); - useEffect(() => { - if (emailFields.length === 0) appendEmail(""); - if (phoneFields.length === 0) appendPhone(""); - }, [emailFields.length, phoneFields.length]); +useEffect(() => { + if (emailFields.length === 0) { + appendEmail({ label: "Work", emailAddress: "" }); // ✅ valid object + } + if (phoneFields.length === 0) { + appendPhone({ label: "Office", phoneNumber: "" }); // ✅ valid object + } +}, [emailFields.length, phoneFields.length]); - const handleAddEmail = async () => { - const emails = getValues("ContactEmails"); + const emails = getValues("contactEmails"); const lastIndex = emails.length - 1; - const valid = await trigger(`ContactEmails.${lastIndex}.emailAddress`); + const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`); if (valid) { appendEmail({ label: "Work", emailAddress: "" }); } }; const handleAddPhone = async () => { - const phones = getValues("ContactPhones"); + const phones = getValues("contactPhones"); const lastIndex = phones.length - 1; - const valid = await trigger(`ContactPhones.${lastIndex}.phoneNumber`); + 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 watchBucketIds = watch("bucketIds"); const toggleBucketId = (id) => { const updated = watchBucketIds?.includes(id) ? watchBucketIds.filter((val) => val !== id) : [...watchBucketIds, id]; - setValue("BucketIds", updated, { shouldValidate: true }); + 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 }); + setValue("bucketIds", updated, { shouldValidate: true }); }; - - - const onSubmit = ( data ) => { - setSubmitting(true) - submitContact( data, reset, setSubmitting ) + const cleaned = { + ...data, + contactEmails: (data.contactEmails || []).filter( + (e) => e.emailAddress?.trim() !== "" + ), + contactPhones: (data.contactPhones || []).filter( + (p) => p.phoneNumber?.trim() !== "" + ), + }; + // Submit cleaned data + console.log(cleaned); + // setSubmitting(true); + // submitContact(data, reset, setSubmitting); + }; + + const handleClosed = () => { + onCLosed(); }; return (
    -
    +
    {" "}
    Create New Contact
    -
    +
    - {errors.Name && ( - {errors.Name.message} + {errors.name && ( + {errors.name.message} )}
    -
    +
    { key={field.id} className="row d-flex align-items-center mb-1" > -
    +
    - {errors.ContactEmails?.[index]?.label && ( + {errors.contactEmails?.[index]?.label && ( - {errors.ContactEmails[index].label.message} + {errors.contactEmails[index].label.message} )}
    -
    +
    {index === emailFields.length - 1 ? ( @@ -247,27 +226,26 @@ const ManageDirectory = ({submitContact,onCLosed}) => { )}
    - {errors.ContactEmails?.[index]?.emailAddress && ( + {errors.contactEmails?.[index]?.emailAddress && ( - {errors.ContactEmails[index].emailAddress.message} + {errors.contactEmails[index].emailAddress.message} )}
    ))}
    -
    {phoneFields.map((field, index) => (
    -
    +
    {index === phoneFields.length - 1 ? ( @@ -308,27 +286,28 @@ const ManageDirectory = ({submitContact,onCLosed}) => { )}
    - {errors.ContactPhones?.[index]?.phoneNumber && ( + {errors.contactPhones?.[index]?.phoneNumber && ( - {errors.ContactPhones[index].phoneNumber.message} + {errors.contactPhones[index].phoneNumber.message} )}
    ))}
    + {errors.contactPhone?.message && ( +
    {errors.contactPhone.message}
    + )}
    -
    +
    - {errors.ContactCategoryId && ( - {errors.ContactCategoryId.message} + {errors.contactCategoryId && ( + + {errors.contactCategoryId.message} + )}
    -
    - +
    + + {errors.projectIds && ( + + {errors.projectIds.message} + + )}
    -
    -
    - -
    + + {errors.tags && ( + + {errors.tags.message} + + )} +
    +
    +
    + + +
      {bucketsLoaging &&

      Loading...

      } {buckets?.map((item) => ( -
      -
      +
    • +
      { {item.name}
      -
    • + ))} -
      +
    {errors.BucketIds && ( {errors.BucketIds.message} )}
    - -
    - - - {errors.category && ( - {errors.category.message} - )} -
    -
    +