From 3f859a42de7e9ef0783e9d21e0c7d264ba652f31 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 29 Jul 2025 12:05:22 +0530 Subject: [PATCH 1/4] contact-functionality-step-1-compo --- .../process/processStepOne/ContactModal.tsx | 161 ++++++++----- .../process/processStepOne/ProcessStepOne.tsx | 39 ++-- .../processStepOne/RecipientAddCard.tsx | 211 ++++++++++++++---- .../process/processStepOne/RecipientCards.tsx | 10 - 4 files changed, 293 insertions(+), 128 deletions(-) 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 b7dfb30..ae412fe 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([]); @@ -62,24 +65,10 @@ const ProcessStepOne = () => { console.log("selectedRecipients", selectedRecipients) useEffect(() => { -<<<<<<< HEAD - // Fetching Transaction Details - START - (async () => { - try { - const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`); - setTransactionDetails(res.data) - } catch (e) { - console.log("Error - transactions details") - } - })() - // Fetching Transaction Details - END - -======= (async () => { const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`) console.log(res.data); })() ->>>>>>> origin/main selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId })); selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId })); @@ -177,7 +166,7 @@ const ProcessStepOne = () => { language: "en", name: `${recipient.firstName} ${recipient.lastName}`, roleName: recipient.roleName, - allowSenderToEdit: recipient.allowSenderToEdit , + allowSenderToEdit: recipient.allowSenderToEdit, allowSenderToDelete: recipient.allowSenderToDelete, })); @@ -348,9 +337,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 8711c50..2bfa22f 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 ( +
setOpenContactModal(true)} + onFocus={() => handleFocus("firstName")} onBlur={handleContactModalBlur} value={recipient.firstName} placeholder="First Name" @@ -301,7 +414,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase , setOpenContactModal(true)} + onFocus={() => handleFocus("lastName")} onBlur={handleContactModalBlur} value={recipient.lastName} placeholder="Last Name" @@ -311,7 +424,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase , setOpenContactModal(true)} + onFocus={() => handleFocus("email")} onBlur={handleContactModalBlur} value={recipient.email} placeholder="Recipient Email" @@ -345,8 +458,8 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
} {/* Contact Modal */} - {contactData.contacts.length > 0 && openContactModal && ( -
+ {openContactModal && contactData.contacts.length > 0 && ( +
)} + + {/* Signing options + Add button in one line */}
@@ -385,11 +502,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/processStepOne/RecipientCards.tsx b/src/components/process/processStepOne/RecipientCards.tsx index 3492139..68116c6 100644 --- a/src/components/process/processStepOne/RecipientCards.tsx +++ b/src/components/process/processStepOne/RecipientCards.tsx @@ -59,15 +59,6 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg const handleRemoveRecipient = (recipient) => { if (!accountId || !transactionId) return; -<<<<<<< HEAD - // dispatch(removeSelectedRecipient(recipient)) - dispatch(removeRecipientAsync([{ - recipientId: recipient.id, - email: recipient.email, - accountId: accountId, - transactionId: transactionId, - }])) -======= if (recipient.uuid) { dispatch(removeSelectedRecipient(recipient)) } else { @@ -78,7 +69,6 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg transactionId: transactionId, })) } ->>>>>>> origin/main if (recipients.length === 2) { let leftRecipient = recipients.filter((obj) => { if ("id" in obj && "id" in recipient) { From 8dca832d119a93fc34d55c81e161ab2291a65882 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 29 Jul 2025 13:27:02 +0530 Subject: [PATCH 2/4] contact-add --- src/components/setting/ContactForm.tsx | 228 +++++++++++++++++++++++++ src/components/setting/Setting.tsx | 33 +++- 2 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/components/setting/ContactForm.tsx diff --git a/src/components/setting/ContactForm.tsx b/src/components/setting/ContactForm.tsx new file mode 100644 index 0000000..ba9b964 --- /dev/null +++ b/src/components/setting/ContactForm.tsx @@ -0,0 +1,228 @@ +// 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" + /> + {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} + /> + {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..ff104e2 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]); // Adjust endpoint if different + 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 ( <> {/* */} From 0361ae7c046984c71a3be23e4078de0b324505e7 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 29 Jul 2025 13:33:03 +0530 Subject: [PATCH 3/4] contact-add-placeholders --- src/components/setting/ContactForm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/setting/ContactForm.tsx b/src/components/setting/ContactForm.tsx index ba9b964..920b650 100644 --- a/src/components/setting/ContactForm.tsx +++ b/src/components/setting/ContactForm.tsx @@ -159,6 +159,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => { onChange={(e) => 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]}}
@@ -177,6 +178,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => { onChange={(e) => 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 && ( +
+
+
+ + + + + + + + + + + {loading ? ( + + + + ) : contacts.length === 0 ? ( + + + + ) : ( + contacts.map((contact) => ( + + + + + + + )) + )} + +
First NameLast NameEmailActions
Loading...
No contacts found.
{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 8c7b7a6..398e3bd 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; @@ -66,6 +66,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/setting/Setting.tsx b/src/components/setting/Setting.tsx index ff104e2..d53aded 100644 --- a/src/components/setting/Setting.tsx +++ b/src/components/setting/Setting.tsx @@ -392,7 +392,7 @@ const Setting = () => { const handleContactSubmit = async (data: ContactData) => { try { setCreateAndInviteLoader(true); - const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]); // Adjust endpoint if different + const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]); if (res.status === 200) { toast.success('Contact created successfully!', { position: "top-center",