diff --git a/src/App.tsx b/src/App.tsx
index f0e6439..91e1ca5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -21,6 +21,7 @@ import TransactionView from "./components/transactions/TransactionView";
import Templates from "./components/templates/Templates";
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
import JoyrideController from "./utlity/JoyRideTour";
+import Contacts from "./components/contacts/Contacts";
const App = () => {
@@ -48,6 +49,8 @@ const App = () => {
{/* } /> */}
} />
{/* */}
+ } />
+ {/* */}
} />
diff --git a/src/components/contacts/Contacts.tsx b/src/components/contacts/Contacts.tsx
new file mode 100644
index 0000000..da45268
--- /dev/null
+++ b/src/components/contacts/Contacts.tsx
@@ -0,0 +1,174 @@
+import { useEffect, useState, useRef } from 'react';
+import { FaEllipsisV } from 'react-icons/fa';
+import api from '../../config/api';
+import ContactFormModal from './ContactsFormModal';
+import { Bounce, toast } from 'react-toastify';
+import { IoMdAdd } from 'react-icons/io';
+
+const Contacts = () => {
+ const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
+ const [contacts, setContacts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [openMenuId, setOpenMenuId] = useState(null);
+ const blurTimeoutRef = useRef(null);
+ const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
+ const [showModal, setShowModal] = useState(false)
+
+ const fetchContacts = async () => {
+ try {
+ const res = await api.get(`/api/accounts/${activeAccountId}/contacts`);
+ setContacts(res.data.content || []);
+ } catch (error) {
+ console.error('Failed to fetch contacts:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (activeAccountId) {
+ fetchContacts();
+ }
+ }, [activeAccountId]);
+
+ const handleEdit = (contactId) => {
+ console.log('Edit contact:', contactId);
+ setOpenMenuId(null);
+ };
+
+ const handleDelete = async(contactId) => {
+ console.log('Delete contact:', contactId);
+ setOpenMenuId(null);
+ try {
+ let res = await api.delete(`/api/accounts/{accountId}/contacts/{contactId}`)
+ if (res?.data) {
+
+ }
+ } catch (error) {
+ console.log(error)
+ }
+ };
+
+ const toggleMenu = (contactId) => {
+ // Cancel any pending blur timeout (prevents blinking)
+ clearTimeout(blurTimeoutRef.current);
+ setOpenMenuId((prevId) => (prevId === contactId ? null : contactId));
+ };
+
+ const handleBlur = () => {
+ blurTimeoutRef.current = setTimeout(() => {
+ setOpenMenuId(null);
+ }, 100);
+ };
+
+ const handleContactSubmit = async (data: ContactData) => {
+ try {
+ setCreateAndInviteLoader(true);
+ const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]);
+ if (res.status === 200) {
+ toast.success('Contact created successfully!', {
+ position: "top-center",
+ autoClose: 1500,
+ transition: Bounce,
+ });
+ setShowModal(false)
+ fetchContacts();
+ }
+ // return res;
+ } catch (err) {
+ console.error("Error creating contact", err);
+ toast.error("Failed to create contact", {
+ position: "top-center",
+ autoClose: 1500,
+ transition: Bounce,
+ });
+ throw err;
+ } finally {
+ setCreateAndInviteLoader(false);
+ }
+ };
+
+ return (
+ <>
+
+
setShowModal((prev) => !prev)}
+ className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
+
+
+
+
+
+
+
+
+ | First Name |
+ Last Name |
+ Email |
+ Actions |
+
+
+
+ {loading ? (
+
+ | Loading... |
+
+ ) : contacts.length === 0 ? (
+
+ | No contacts found. |
+
+ ) : (
+ contacts.map((contact) => (
+
+ | {contact.firstName} |
+ {contact.lastName} |
+ {contact.email} |
+
+
+
+
+ {openMenuId === contact.id && (
+
+
+
+
+ )}
+
+ |
+
+ ))
+ )}
+
+
+ {showModal &&
+
+ }
+
+ >
+ );
+};
+
+export default Contacts;
diff --git a/src/components/contacts/ContactsFormModal.tsx b/src/components/contacts/ContactsFormModal.tsx
new file mode 100644
index 0000000..ea6416a
--- /dev/null
+++ b/src/components/contacts/ContactsFormModal.tsx
@@ -0,0 +1,245 @@
+// ContactForm.tsx
+import React, { useState } from 'react';
+import { FaSpinner } from 'react-icons/fa';
+
+export interface ContactData {
+ contactId?: number;
+ email: string;
+ firstName: string;
+ lastName: string;
+ title: string;
+ company: string;
+ website: string;
+ phones: string[];
+}
+
+interface Props {
+ onSubmit: (data: ContactData) => void;
+ loading: boolean;
+ setShowModal: () => React.Dispatch>
+}
+
+const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
+ const [formData, setFormData] = useState({
+ email: '',
+ firstName: '',
+ lastName: '',
+ title: '',
+ company: '',
+ website: '',
+ phones: [''],
+ });
+
+ const [errors, setErrors] = useState<{ [key: string]: string }>({});
+
+ const handleChange = (field: keyof ContactData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ if (errors[field]) {
+ setErrors((prev) => ({ ...prev, [field]: '' }));
+ }
+ };
+
+ const handlePhoneChange = (index: number, value: string) => {
+ const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
+ const updatedPhones = [...formData.phones];
+ updatedPhones[index] = numericValue;
+ setFormData((prev) => ({ ...prev, phones: updatedPhones }));
+
+ const errorKey = `phones_${index}`;
+ if (errors[errorKey]) {
+ setErrors((prev) => ({ ...prev, [errorKey]: '' }));
+ }
+ };
+
+
+ const addPhoneField = () => {
+ setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
+ };
+
+ const removePhoneField = (index: number) => {
+ const updatedPhones = formData.phones.filter((_, i) => i !== index);
+ setFormData((prev) => ({ ...prev, phones: updatedPhones }));
+ };
+
+ const validate = () => {
+ const newErrors: { [key: string]: string } = {};
+
+ const nameRegex = /^[a-zA-Z\s\-']+$/;
+ const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
+ const phoneRegex = /^\d+$/;
+
+ // Email
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Invalid email address';
+ }
+
+ // // First Name
+ // if (!formData.firstName.trim()) {
+ // newErrors.firstName = 'First name is required';
+ // } else if (!nameRegex.test(formData.firstName)) {
+ // newErrors.firstName = 'First name contains invalid characters';
+ // }
+
+ // Last Name
+ if (!formData.lastName.trim()) {
+ newErrors.lastName = 'Last name is required';
+ } else if (!nameRegex.test(formData.lastName)) {
+ newErrors.lastName = 'Last name contains invalid characters';
+ }
+
+ // // Title
+ // if (!formData.title.trim()) {
+ // newErrors.title = 'Title is required';
+ // }
+
+
+ // Website (optional but validate if present)
+ if (formData.website.trim() && !urlRegex.test(formData.website)) {
+ newErrors.website = 'Invalid website URL';
+ }
+
+ // Phones
+ // formData.phones.forEach((phone, index) => {
+ // if (!phone.trim()) {
+ // newErrors[`phones_${index}`] = 'Phone number is required';
+ // } else if (!phoneRegex.test(phone)) {
+ // newErrors[`phones_${index}`] = 'Phone number must contain only digits';
+ // }
+ // });
+
+ return newErrors;
+ };
+
+
+ const handleSubmit = async () => {
+ const validationErrors = validate();
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ return;
+ }
+
+ try {
+ await onSubmit(formData);
+
+ setFormData({
+ email: '',
+ firstName: '',
+ lastName: '',
+ title: '',
+ company: '',
+ website: '',
+ phones: [''],
+ });
+ setErrors({});
+ } catch (err) {
+ console.error('Submit failed:', err);
+ }
+ };
+
+
+ return (
+
+
+
+
Add Contact
+
+
+ {[
+ { label: 'First Name', field: 'firstName' },
+ { label: 'Last Name', field: 'lastName' },
+ { label: 'Email', field: 'email' },
+ { label: 'Title', field: 'title' },
+ { label: 'Company', field: 'company' },
+ { label: 'Website', field: 'website' },
+ ].map(({ label, field }) => (
+
+
+ handleChange(field as keyof ContactData, e.target.value)}
+ required={true}
+ className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
+ placeholder={`Enter ${label}`}
+ />
+ {errors[field] && {errors[field]}}
+
+ ))}
+
+
+
+
+
+ {formData.phones.map((phone, index) => (
+
+
+ handlePhoneChange(index, e.target.value)}
+ className="flex-1 px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
+ maxLength={15}
+ placeholder='Enter Phone Numbers'
+ />
+ {formData.phones.length > 1 && (
+
+ )}
+
+ {errors[`phones_${index}`] && (
+
{errors[`phones_${index}`]}
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ContactFormModal;
diff --git a/src/components/dashboard/SideBar.tsx b/src/components/dashboard/SideBar.tsx
index c3084e7..486c3b1 100644
--- a/src/components/dashboard/SideBar.tsx
+++ b/src/components/dashboard/SideBar.tsx
@@ -10,7 +10,7 @@ import { IoSettingsOutline } from "react-icons/io5";
import axios from 'axios';
import { MdManageAccounts } from 'react-icons/md';
-import { GrTransaction } from 'react-icons/gr';
+import { GrContactInfo, GrTransaction } from 'react-icons/gr';
type SideBarProps = {
showSideBar: boolean;
@@ -68,6 +68,7 @@ const SideBar: React.FC = ({ showSideBar, setShowSideBar }) => {
} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
{/* } label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
+ } label="Contacts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
diff --git a/src/components/process/processStepOne/ContactModal.tsx b/src/components/process/processStepOne/ContactModal.tsx
index 88ba9e6..853bc56 100644
--- a/src/components/process/processStepOne/ContactModal.tsx
+++ b/src/components/process/processStepOne/ContactModal.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react'
-import { ContactTypeObj } from './RecipientAddCard'
+import React, { useState } from 'react';
+import { ContactTypeObj } from './RecipientAddCard';
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
interface ContactDataProps {
@@ -10,66 +10,127 @@ interface ContactDataProps {
openContactModal: boolean;
setOpenContactModal: React.Dispatch>;
setRecipients: React.Dispatch>;
+ topRef?: React.RefObject;
+ setManualFocusTriggered: React.Dispatch>
}
-const ContactModal: React.FC = ({ contactData, inpFirstName, inpLastName, inpEmail, openContactModal, setRecipients, setOpenContactModal }) => {
+// Helper function to highlight matching text
+const highlightText = (text: string, searchTerm: string, shouldHighlight: boolean) => {
+ if (!shouldHighlight || !searchTerm.trim()) {
+ return {text};
+ }
+
+ const searchTermLower = searchTerm.toLowerCase();
+ const textLower = text.toLowerCase();
+ const index = textLower.indexOf(searchTermLower);
+ if (index === -1) {
+ return {text};
+ }
+
+ const beforeMatch = text.slice(0, index);
+ const match = text.slice(index, index + searchTerm.length);
+ const afterMatch = text.slice(index + searchTerm.length);
+
+ return (
+
+ {beforeMatch}
+ {match}
+ {afterMatch}
+
+ );
+};
+
+const ContactModal: React.FC = ({
+ contactData,
+ inpFirstName,
+ inpLastName,
+ inpEmail,
+ openContactModal,
+ setRecipients,
+ setOpenContactModal ,
+ setManualFocusTriggered
+}) => {
const handleContactClick = (indx: number) => {
if (!contactData.contacts.length) return;
- setRecipients((prev) => {
- return {
- firstName: contactData.contacts[indx].firstName,
- lastName: contactData.contacts[indx].lastName,
- email: contactData.contacts[indx].email,
- type: prev.type
- }
- });
+ setRecipients((prev) => ({
+ ...prev, // Preserve other properties like uuid, newOne, etc.
+ firstName: contactData.contacts[indx].firstName,
+ lastName: contactData.contacts[indx].lastName,
+ email: contactData.contacts[indx].email,
+ }));
setOpenContactModal(false);
+ setManualFocusTriggered(false)
+
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent, indx: number) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ handleContactClick(indx);
+ }
+ };
+
+ if (!openContactModal || !contactData.contacts.length) {
+ return null;
}
return (
- <>
- {
- contactData.contacts.map((recipient, indx) => {
- return handleContactClick(indx)}
- style={{display: `${openContactModal ? "flex" : "none"}`}}
- className='flex items-center px-3 py-2 cursor-pointer hover:bg-slate-100 transiton-all w-full'>
-
-
-
-
- {recipient.firstName}
-
-
- {recipient.lastName}
-
-
-
- {recipient.email}
+
+ {contactData.contacts.map((contact, indx) => (
+
handleContactClick(indx)}
+ onKeyDown={(e) => handleKeyDown(e, indx)}
+ className="flex items-center px-3 py-2 cursor-pointer hover:bg-gray-50 transition-colors duration-150 focus:bg-gray-100 focus:outline-none rounded-md"
+ role="button"
+ aria-label={`Select contact ${contact.firstName} ${contact.lastName}`}
+ >
+ {/* Avatar */}
+
+ {(contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')}
+
+
+ {/* Contact Info */}
+
+
+
+ {highlightText(
+ contact.firstName,
+ inpFirstName,
+ contactData.type === "firstName"
+ )}
+
+
+ {highlightText(
+ contact.lastName,
+ inpLastName,
+ contactData.type === "lastName"
+ )}
+
+ {highlightText(
+ contact.email,
+ inpEmail,
+ contactData.type === "email"
+ )}
+
- })
- }
- >
- )
-}
+
+ ))}
+
+ {/* No results message */}
+ {contactData.contacts.length === 0 && (
+
+ No contacts found
+
+ )}
+
+ );
+};
-// return
-// {val.firstName}
-//
-export default ContactModal
\ No newline at end of file
+export default ContactModal;
\ No newline at end of file
diff --git a/src/components/process/processStepOne/ProcessStepOne.tsx b/src/components/process/processStepOne/ProcessStepOne.tsx
index 49a733e..3150468 100644
--- a/src/components/process/processStepOne/ProcessStepOne.tsx
+++ b/src/components/process/processStepOne/ProcessStepOne.tsx
@@ -6,7 +6,7 @@ import SelectedDocuments from "./SelectedDocuments";
import api from "../../../config/api";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../store/store";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { setImages } from "../../../store/slices/selectedDocsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
@@ -39,10 +39,10 @@ const ProcessStepOne = () => {
dispatch(toggleToTemplates());
}
- const [editCase , setEditCase] = useState
("")
- const [editRecipient , setEditRecipient] = useState({})
+ const [editCase, setEditCase] = useState("")
+ const [editRecipient, setEditRecipient] = useState({})
+
-
// if (!accountId || !transactionId) {
// window.location.href = "/";
@@ -55,6 +55,9 @@ const ProcessStepOne = () => {
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const userData = useSelector((state: RootState) => state.user)
+ // ProcessStepOne.tsx
+ const topRef = useRef(null);
+
// const userData = useSelector((state: RootState) => state.user)
const [transactionDetails, setTransactionDetails] = useState([]);
@@ -80,6 +83,10 @@ const ProcessStepOne = () => {
// console.log(res.data);
// })()
// >>>>>>> origin/main
+ (async () => {
+ const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`)
+ console.log(res.data);
+ })()
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
@@ -177,7 +184,7 @@ const ProcessStepOne = () => {
language: "en",
name: `${recipient.firstName} ${recipient.lastName}`,
roleName: recipient.roleName,
- allowSenderToEdit: recipient.allowSenderToEdit ,
+ allowSenderToEdit: recipient.allowSenderToEdit,
allowSenderToDelete: recipient.allowSenderToDelete,
}));
@@ -350,9 +357,17 @@ const ProcessStepOne = () => {
{
signType !== SignTypeEnum.selfSign &&
-
+
{/* {signType !== "self-sign" && } */}
-
+
+
+
+
+
+
}
diff --git a/src/components/process/processStepOne/RecipientAddCard.tsx b/src/components/process/processStepOne/RecipientAddCard.tsx
index 3157d3c..aad1488 100644
--- a/src/components/process/processStepOne/RecipientAddCard.tsx
+++ b/src/components/process/processStepOne/RecipientAddCard.tsx
@@ -24,7 +24,11 @@ export interface ContactTypeObj {
}
-const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,setEditRecipient, setEditCase }) => {
+const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase, setEditRecipient, setEditCase, topRef }) => {
+ const [activeAccountId, setActiveAccountId] = useState(
+ JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
+ );
+
const [contactData, setContactData] = useState({ type: "", contacts: [] });
const [openContactModal, setOpenContactModal] = useState(false);
@@ -32,11 +36,31 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
const dispatch = useDispatch();
const formRef = useRef(null);
- const [recipient, setRecipients] = useState({ uuid:null, newOne:false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false });
+ const [recipient, setRecipients] = useState({ uuid: null, newOne: false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false });
const [showAdvanceSettings, setShowAdvanceSettings] = useState(false)
+
+ const [cachedContacts, setCachedContacts] = useState([]);
+ const [filteredContacts, setFilteredContacts] = useState([]);
+ const [activeField, setActiveField] = useState<"firstName" | "lastName" | "email" | null>(null);
+ const [hasFetchedInitial, setHasFetchedInitial] = useState(false);
+ const [lastQuery, setLastQuery] = useState(null);
+ const containerRef = useRef(null);
+ const [manualFocusTriggered, setManualFocusTriggered] = useState(false);
+
+
console.log(recipient)
+ useEffect(() => {
+ if (openContactModal) {
+ if (topRef?.current) {
+ topRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
+ } else {
+ window.scrollTo({ top: 0, behavior: "smooth" });
+ }
+ }
+ }, [openContactModal]);
+
useEffect(() => {
if (editCase === "Edit" && editRecipient) {
setRecipients({
@@ -51,56 +75,55 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
allowSenderToEdit: editRecipient.allowSenderToEdit ?? false,
allowSenderToDelete: editRecipient.allowSenderToDelete ?? false,
uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''),
- newOne:editRecipient.newOne ?? false
+ newOne: editRecipient.newOne ?? false
});
}
}, [editCase, editRecipient]);
const handleRecipientAdd = (e: React.FormEvent) => {
- e.preventDefault();
+ e.preventDefault();
- console.log("recipientrecipient",recipient)
+ console.log("recipientrecipient", recipient)
- if (!recipient.lastName || !recipient.email || !recipient.type) return;
+ if (!recipient.lastName || !recipient.email || !recipient.type) return;
- // ✅ Email format validation
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!emailRegex.test(recipient.email)) {
- console.log("Invalid email format");
- return;
- }
+ // ✅ Email format validation
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(recipient.email)) {
+ console.log("Invalid email format");
+ return;
+ }
- if (editCase === 'Edit') {
- dispatch(updateRecipient(recipient));
- } else {
- dispatch(addRecipient({
- ...recipient,
- uuid:uuidv4(),
- newOne:true
- }));
- }
+ if (editCase === 'Edit') {
+ dispatch(updateRecipient(recipient));
+ } else {
+ dispatch(addRecipient({
+ ...recipient,
+ uuid: uuidv4(),
+ newOne: true
+ }));
+ }
- // ✅ Reset state and form
- setRecipients({
- firstName: "",
- lastName: "",
- email: "",
- hostName: "",
- hostEmail: "",
- roleName: "",
- type: recipientType.SIGNER,
- allowSenderToEdit: false,
- allowSenderToDelete: false,
- uuid:"",
- newOne:false
- });
+ setRecipients({
+ firstName: "",
+ lastName: "",
+ email: "",
+ hostName: "",
+ hostEmail: "",
+ roleName: "",
+ type: recipientType.SIGNER,
+ allowSenderToEdit: false,
+ allowSenderToDelete: false,
+ uuid: "",
+ newOne: false
+ });
- // setRecipients(null),
- setEditCase("")
-
- formRef.current?.reset();
-};
+ // setRecipients(null),
+ setEditCase("")
+setManualFocusTriggered(false)
+ formRef.current?.reset();
+ };
@@ -155,7 +178,97 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
}, 350)
}
+
+ const handleFocus = async (field: "firstName" | "lastName" | "email") => {
+ setManualFocusTriggered(true);
+ setOpenContactModal(true);
+ setActiveField(field);
+ setLastQuery(null); // Reset to allow fresh input fetch if needed
+
+ if (!hasFetchedInitial) {
+ try {
+ const res = await api.get(`/api/accounts/${activeAccountId}/contacts?pageSize=10`);
+ const contacts = res.data?.content || [];
+ setCachedContacts(contacts);
+ setFilteredContacts(contacts);
+ setContactData({ type: field, contacts });
+ setHasFetchedInitial(true);
+ } catch (err) {
+ console.error("Initial contact fetch failed:", err);
+ }
+ } else {
+ setFilteredContacts(cachedContacts);
+ setContactData({ type: field, contacts: cachedContacts });
+ }
+ };
+
+
+
+
+
+ useDebouncedEffect(() => {
+ console.log("useDebouncedEffect")
+ if (!activeField || !manualFocusTriggered) return;
+
+ const inputValue = recipient[activeField].trim().toLowerCase();
+
+ if (inputValue.length === 0) {
+ setFilteredContacts(cachedContacts);
+ setContactData({ type: activeField, contacts: cachedContacts });
+ setOpenContactModal(true); // Keep showing modal with all options
+ return;
+ }
+
+ // Local match
+ const localMatches = cachedContacts.filter(contact =>
+ contact[activeField]?.toLowerCase().includes(inputValue)
+ );
+
+ if (localMatches.length > 0) {
+ setFilteredContacts(localMatches);
+ setContactData({ type: activeField, contacts: localMatches });
+ setOpenContactModal(true);
+ return;
+ }
+
+ if (lastQuery !== inputValue) {
+ setLastQuery(inputValue);
+ api
+ .get(`/api/accounts/${activeAccountId}/contacts?pageSize=5&${activeField}=${inputValue}`)
+ .then((res) => {
+ const fetched = res.data?.content || [];
+ setFilteredContacts(fetched);
+ setContactData({ type: activeField, contacts: fetched });
+ setOpenContactModal(true);
+ })
+ .catch(console.error);
+ }
+ }, [recipient.firstName, recipient.lastName, recipient.email], 300);
+
+useEffect(() => {
+ const handleClickOutside = (e: MouseEvent | FocusEvent) => {
+ if (
+ containerRef.current &&
+ !containerRef.current.contains(e.target as Node)
+ ) {
+ setOpenContactModal(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("focusin", handleClickOutside); // Also handles tabbing out
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("focusin", handleClickOutside);
+ };
+}, []);
+
+
+
+
return (
+
}
{/* Contact Modal */}
- {contactData.contacts.length > 0 && openContactModal && (
-
+ {openContactModal && contactData.contacts.length > 0 && (
+
)}
+
+
{/* Signing options + Add button in one line */}
@@ -386,11 +503,11 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
type="submit"
className="inline-flex items-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-sm font-semibold px-5 py-2 rounded-md transition"
>
- {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
+ {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
-
+
);
};
diff --git a/src/components/process/processStepTwo/rightSideBar/DirectionalControls.tsx b/src/components/process/processStepTwo/rightSideBar/DirectionalControls.tsx
index fe79bb8..25c08a5 100644
--- a/src/components/process/processStepTwo/rightSideBar/DirectionalControls.tsx
+++ b/src/components/process/processStepTwo/rightSideBar/DirectionalControls.tsx
@@ -149,6 +149,79 @@ const DirectionalControls: React.FC
= ({
+ {/* Middle Row: Left - Center (Inputs) - Right */}
+
+ {/* Left Button */}
+
+
+ Left
+
+
+ {/* Center Panel */}
+
+
+
+ {/* Right Button */}
+
+
+ Right
+
+
+
+ {/* Bottom Button */}
+
+
+
+ Bottom
+
+
);
};
diff --git a/src/components/setting/ContactForm.tsx b/src/components/setting/ContactForm.tsx
new file mode 100644
index 0000000..920b650
--- /dev/null
+++ b/src/components/setting/ContactForm.tsx
@@ -0,0 +1,230 @@
+// ContactForm.tsx
+import React, { useState } from 'react';
+import { FaSpinner } from 'react-icons/fa';
+
+export interface ContactData {
+ contactId?: number;
+ email: string;
+ firstName: string;
+ lastName: string;
+ title: string;
+ company: string;
+ website: string;
+ phones: string[];
+}
+
+interface Props {
+ onSubmit: (data: ContactData) => void;
+ loading: boolean;
+}
+
+const ContactForm = ({ onSubmit, loading }: Props) => {
+ const [formData, setFormData] = useState({
+ email: '',
+ firstName: '',
+ lastName: '',
+ title: '',
+ company: '',
+ website: '',
+ phones: [''],
+ });
+
+ const [errors, setErrors] = useState<{ [key: string]: string }>({});
+
+ const handleChange = (field: keyof ContactData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ if (errors[field]) {
+ setErrors((prev) => ({ ...prev, [field]: '' }));
+ }
+ };
+
+ const handlePhoneChange = (index: number, value: string) => {
+ const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
+ const updatedPhones = [...formData.phones];
+ updatedPhones[index] = numericValue;
+ setFormData((prev) => ({ ...prev, phones: updatedPhones }));
+
+ const errorKey = `phones_${index}`;
+ if (errors[errorKey]) {
+ setErrors((prev) => ({ ...prev, [errorKey]: '' }));
+ }
+ };
+
+
+ const addPhoneField = () => {
+ setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
+ };
+
+ const removePhoneField = (index: number) => {
+ const updatedPhones = formData.phones.filter((_, i) => i !== index);
+ setFormData((prev) => ({ ...prev, phones: updatedPhones }));
+ };
+
+ const validate = () => {
+ const newErrors: { [key: string]: string } = {};
+
+ const nameRegex = /^[a-zA-Z\s\-']+$/;
+ const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
+ const phoneRegex = /^\d+$/;
+
+ // Email
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
+ newErrors.email = 'Invalid email address';
+ }
+
+ // // First Name
+ // if (!formData.firstName.trim()) {
+ // newErrors.firstName = 'First name is required';
+ // } else if (!nameRegex.test(formData.firstName)) {
+ // newErrors.firstName = 'First name contains invalid characters';
+ // }
+
+ // Last Name
+ if (!formData.lastName.trim()) {
+ newErrors.lastName = 'Last name is required';
+ } else if (!nameRegex.test(formData.lastName)) {
+ newErrors.lastName = 'Last name contains invalid characters';
+ }
+
+ // // Title
+ // if (!formData.title.trim()) {
+ // newErrors.title = 'Title is required';
+ // }
+
+
+ // Website (optional but validate if present)
+ if (formData.website.trim() && !urlRegex.test(formData.website)) {
+ newErrors.website = 'Invalid website URL';
+ }
+
+ // Phones
+ // formData.phones.forEach((phone, index) => {
+ // if (!phone.trim()) {
+ // newErrors[`phones_${index}`] = 'Phone number is required';
+ // } else if (!phoneRegex.test(phone)) {
+ // newErrors[`phones_${index}`] = 'Phone number must contain only digits';
+ // }
+ // });
+
+ return newErrors;
+ };
+
+
+ const handleSubmit = async () => {
+ const validationErrors = validate();
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ return;
+ }
+
+ try {
+ await onSubmit(formData);
+
+ setFormData({
+ email: '',
+ firstName: '',
+ lastName: '',
+ title: '',
+ company: '',
+ website: '',
+ phones: [''],
+ });
+ setErrors({});
+ } catch (err) {
+ console.error('Submit failed:', err);
+ }
+ };
+
+
+ return (
+
+
Add Contact
+
+
+ {[
+ { label: 'First Name', field: 'firstName' },
+ { label: 'Last Name', field: 'lastName' },
+ { label: 'Email', field: 'email' },
+ { label: 'Title', field: 'title' },
+ { label: 'Company', field: 'company' },
+ { label: 'Website', field: 'website' },
+ ].map(({ label, field }) => (
+
+
+ handleChange(field as keyof ContactData, e.target.value)}
+ required={true}
+ className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
+ placeholder={`Enter ${label}`}
+ />
+ {errors[field] && {errors[field]}}
+
+ ))}
+
+
+
+
+
+ {formData.phones.map((phone, index) => (
+
+
+ handlePhoneChange(index, e.target.value)}
+ className="flex-1 px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
+ maxLength={15}
+ placeholder='Enter Phone Numbers'
+ />
+ {formData.phones.length > 1 && (
+
+ )}
+
+ {errors[`phones_${index}`] && (
+
{errors[`phones_${index}`]}
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ContactForm;
diff --git a/src/components/setting/Setting.tsx b/src/components/setting/Setting.tsx
index 623cc53..d53aded 100644
--- a/src/components/setting/Setting.tsx
+++ b/src/components/setting/Setting.tsx
@@ -9,6 +9,7 @@ import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
+import ContactForm , {ContactData} from "./ContactForm";
// Define the shape of state object
interface Account {
@@ -57,7 +58,7 @@ const Setting = () => {
const [activeTab, setActiveTab] = useState("userProfile");
const [activeSubSection, setActiveSubSection] = useState("");
- const [user, setUser] = useState({
+ const [user, setUser] = useState({
id: '',
name: '',
email: '',
@@ -212,6 +213,13 @@ const Setting = () => {
accountOptionsLoading={accountOptionsLoading}
/>
);
+ case "contacts":
+ return (
+
+ );
default:
return Coming Soon...
;
}
@@ -236,6 +244,7 @@ const Setting = () => {
const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" },
{ id: "create and invite", label: "Create and invite", type: "settings" },
+ { id: "contacts", label: "Contacts", type: "settings" },
{ id: "language", label: "Language", type: "settings" },
{ id: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
@@ -379,6 +388,28 @@ const Setting = () => {
}
};
+
+ const handleContactSubmit = async (data: ContactData) => {
+ try {
+ setCreateAndInviteLoader(true);
+ const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]);
+ if (res.status === 200) {
+ toast.success('Contact created successfully!', {
+ position: "top-center",
+ autoClose: 1500,
+ transition: Bounce,
+ });
+ }
+ // return res;
+ } catch (err) {
+ console.error("Error creating contact", err);
+ toast.error("Failed to create contact");
+ throw err;
+ } finally {
+ setCreateAndInviteLoader(false);
+ }
+ };
+
return (
<>
{/* */}