diff --git a/src/App.tsx b/src/App.tsx
index 5270316..80e4d00 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -20,6 +20,7 @@ import Account from "./components/accounts/Account";
import TransactionView from "./components/transactions/TransactionView";
import Templates from "./components/templates/Templates";
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
+import Contacts from "./components/contacts/Contacts";
const App = () => {
return (
@@ -43,6 +44,7 @@ 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 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",