+
diff --git a/src/components/Directory/UpdateContact.jsx b/src/components/Directory/UpdateContact.jsx
new file mode 100644
index 00000000..cb2310c4
--- /dev/null
+++ b/src/components/Directory/UpdateContact.jsx
@@ -0,0 +1,455 @@
+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 IconButton from "../common/IconButton";
+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 SelectMultiple from "../common/SelectMultiple";
+import { ContactSchema } from "./DirectorySchema";
+
+const UpdateContact = ({ submitContact, existingContact, 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 { contactCategory, loading: contactCategoryLoading } =
+ useContactCategory();
+ const { contactTags, loading: Tagloading } = useContactTags();
+ const [ IsSubmitting, setSubmitting ] = useState( false );
+ const [isInitialized, setIsInitialized] = useState(false);
+ const dispatch = useDispatch();
+
+ const methods = useForm({
+ resolver: zodResolver(ContactSchema),
+ defaultValues: {
+ name: "",
+ organization: "",
+ contactCategoryId: null,
+ address: "",
+ description: "",
+ projectIds: [],
+ contactEmails: [],
+ contactPhones: [],
+ tags: [],
+ bucketIds: [],
+ },
+ });
+
+ const {
+ register,
+ handleSubmit,
+ control,
+ getValues,
+ trigger,
+ setValue,
+ watch,
+ reset,
+ formState: { errors },
+ } = methods;
+
+ const {
+ fields: emailFields,
+ append: appendEmail,
+ remove: removeEmail,
+ } = useFieldArray({ control, name: "contactEmails" });
+
+ const {
+ fields: phoneFields,
+ append: appendPhone,
+ remove: removePhone,
+ } = useFieldArray({ control, name: "contactPhones" });
+
+ const handleAddEmail = async () => {
+ const emails = getValues("contactEmails");
+ const lastIndex = emails.length - 1;
+ const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
+ if (valid) {
+ appendEmail({ label: "Work", emailAddress: "" });
+ }
+ };
+
+ const handleAddPhone = async () => {
+ const phones = getValues("contactPhones");
+ const lastIndex = phones.length - 1;
+ const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
+ if (valid) {
+ appendPhone({ label: "Office", phoneNumber: "" });
+ }
+ };
+
+ 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 = async (data) => {
+ debugger;
+ const cleaned = {
+ ...data,
+ contactEmails: (data.contactEmails || []).filter(
+ (e) => e.emailAddress?.trim() !== ""
+ ),
+ contactPhones: (data.contactPhones || []).filter(
+ (p) => p.phoneNumber?.trim() !== ""
+ ),
+ };
+
+ setSubmitting(true);
+ await submitContact({ ...cleaned, id: existingContact.id });
+ setSubmitting(false);
+
+ };
+
+ const handleClosed = () => {
+ onCLosed();
+ };
+ useEffect(() => {
+ const isValidContact =
+ existingContact &&
+ typeof existingContact === "object" &&
+ !Array.isArray(existingContact);
+
+ if (!isInitialized &&isValidContact && TagsData) {
+ reset({
+ name: existingContact.name || "",
+ organization: existingContact.organization || "",
+ contactEmails: existingContact.contactEmails || [],
+ contactPhones: existingContact.contactPhones || [],
+ contactCategoryId: existingContact.contactCategory?.id || null,
+ address: existingContact.address || "",
+ description: existingContact.description || "",
+ projectIds: existingContact.projectIds || null,
+ tags: existingContact.tags || [],
+ bucketIds: existingContact.bucketIds || [],
+ } );
+
+ if (!existingContact.contactPhones || existingContact.contactPhones.length === 0) {
+ appendPhone({ label: "Office", phoneNumber: "" });
+ }
+
+ if (!existingContact.contactEmails || existingContact.contactEmails.length === 0) {
+ appendEmail({ label: "Work", emailAddress: "" });
+ }
+ setIsInitialized(true)
+ }
+
+ return()=> reset()
+ }, [ existingContact, buckets, projects ] );
+
+
+ return (
+
+
+
+ );
+};
+
+export default UpdateContact;
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;
+ }
+}
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;
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"))}
/>