Compare commits
12 Commits
4a5ede2891
...
1a4ea46c5b
| Author | SHA1 | Date |
|---|---|---|
|
|
1a4ea46c5b | |
|
|
6de3205493 | |
|
|
00dcbb60f7 | |
|
|
e14e59e759 | |
|
|
69796039ba | |
|
|
0361ae7c04 | |
|
|
7726647d40 | |
|
|
8dca832d11 | |
|
|
99f40a3689 | |
|
|
3f859a42de | |
|
|
95d4a5ccff | |
|
|
bdaa3dc263 |
|
|
@ -21,6 +21,7 @@ import TransactionView from "./components/transactions/TransactionView";
|
||||||
import Templates from "./components/templates/Templates";
|
import Templates from "./components/templates/Templates";
|
||||||
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
|
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
|
||||||
import JoyrideController from "./utlity/JoyRideTour";
|
import JoyrideController from "./utlity/JoyRideTour";
|
||||||
|
import Contacts from "./components/contacts/Contacts";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
|
||||||
|
|
@ -49,6 +50,8 @@ const App = () => {
|
||||||
<Route path="/templates" element={<Templates />} />
|
<Route path="/templates" element={<Templates />} />
|
||||||
{/* </Route> */}
|
{/* </Route> */}
|
||||||
|
|
||||||
|
<Route path="/contacts" element={<Contacts />} />
|
||||||
|
|
||||||
<Route path="/s/:page1" element={<Setting />} />
|
<Route path="/s/:page1" element={<Setting />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,10 +213,6 @@ const Account = () => {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleTransactionClick = (id: number) => {
|
|
||||||
navigate(`/accounts/${activeAccountId}/transactions/${id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
|
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
|
||||||
setInviteLoading(true);
|
setInviteLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||||
import api from '../../config/api';
|
import api from '../../config/api';
|
||||||
import { Transaction } from '../transactions/Transactions';
|
import { Transaction } from '../transactions/Transactions';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { FaCheck } from 'react-icons/fa';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,10 +18,12 @@ const TransactionDataTable = () => {
|
||||||
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const fetchTransactions = async () => {
|
const fetchTransactions = async () => {
|
||||||
console.log("fetchTransactions")
|
console.log("fetchTransactions")
|
||||||
try {
|
try {
|
||||||
const response = await api(`/api/accounts/${activeAccountId}/transactions`);
|
const response = await api.get(`/api/accounts/${activeAccountId}/transactions?sortBy=lastModifiedAt desc`);
|
||||||
setTransactions(response.data || []);
|
setTransactions(response.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching transactions:", error);
|
console.error("Error fetching transactions:", error);
|
||||||
|
|
@ -28,6 +32,10 @@ const TransactionDataTable = () => {
|
||||||
|
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
|
||||||
|
const handleTransactionClick = (id: number) => {
|
||||||
|
navigate(`/accounts/${activeAccountId}/transactions/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeAccountId) {
|
if (activeAccountId) {
|
||||||
fetchTransactions();
|
fetchTransactions();
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<div className='flex justify-end my-2 cursor-pointer'>
|
||||||
|
<div
|
||||||
|
onClick={() => setShowModal((prev) => !prev)}
|
||||||
|
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
|
||||||
|
<IoMdAdd className="text-white" />
|
||||||
|
<button
|
||||||
|
className="text-white rounded text-sm hover:opacity-90 cursor-pointer">
|
||||||
|
Add Contact
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto max-h-[79vh] border border-gray-200 rounded shadow-sm no-scrollbar">
|
||||||
|
<table className="min-w-full text-sm bg-white">
|
||||||
|
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left">First Name</th>
|
||||||
|
<th className="px-4 py-3 text-left">Last Name</th>
|
||||||
|
<th className="px-4 py-3 text-left">Email</th>
|
||||||
|
<th className="px-4 py-3 text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="4" className="text-center py-4">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
) : contacts.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="4" className="text-center py-4">No contacts found.</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
contacts.map((contact) => (
|
||||||
|
<tr key={contact.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-3">{contact.firstName}</td>
|
||||||
|
<td className="px-4 py-3">{contact.lastName}</td>
|
||||||
|
<td className="px-4 py-3">{contact.email}</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<div
|
||||||
|
className="relative inline-block"
|
||||||
|
tabIndex={0}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="text-gray-600 hover:text-black p-1 focus:outline-none"
|
||||||
|
onClick={() => toggleMenu(contact.id)}
|
||||||
|
>
|
||||||
|
<FaEllipsisV />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{openMenuId === contact.id && (
|
||||||
|
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(contact.id)}
|
||||||
|
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(contact.id)}
|
||||||
|
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{showModal &&
|
||||||
|
<ContactFormModal onSubmit={handleContactSubmit}
|
||||||
|
loading={createAndInviteLoader} setShowModal={setShowModal}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contacts;
|
||||||
|
|
@ -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<React.SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
|
||||||
|
const [formData, setFormData] = useState<ContactData>({
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/5 backdrop-blur-sm bg-opacity-50">
|
||||||
|
<div className="bg-white rounded-md p-6 max-w-[700px] w-full shadow-lg relative">
|
||||||
|
<div className="ms-2 space-y-2 max-w-[60vw]">
|
||||||
|
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{[
|
||||||
|
{ 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 }) => (
|
||||||
|
<div key={field} className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
|
||||||
|
<input
|
||||||
|
type={field === 'email' ? 'email' : 'text'}
|
||||||
|
value={formData[field as keyof ContactData] as string}
|
||||||
|
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] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.phones.map((phone, index) => (
|
||||||
|
<div key={index} className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={phone}
|
||||||
|
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 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removePhoneField(index)}
|
||||||
|
className="text-xs text-red-500 hover:underline"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errors[`phones_${index}`] && (
|
||||||
|
<span className="text-sm text-red-500">{errors[`phones_${index}`]}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addPhoneField}
|
||||||
|
className="text-sm text-blue-500 hover:underline mt-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
+ Add another phone
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowModal((prev) => !prev)}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer me-3"
|
||||||
|
> Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin inline-block mr-2" />
|
||||||
|
SAVING...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'SAVE CONTACT'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactFormModal;
|
||||||
|
|
@ -10,7 +10,7 @@ import { IoSettingsOutline } from "react-icons/io5";
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { MdManageAccounts } from 'react-icons/md';
|
import { MdManageAccounts } from 'react-icons/md';
|
||||||
import { GrTransaction } from 'react-icons/gr';
|
import { GrContactInfo, GrTransaction } from 'react-icons/gr';
|
||||||
|
|
||||||
type SideBarProps = {
|
type SideBarProps = {
|
||||||
showSideBar: boolean;
|
showSideBar: boolean;
|
||||||
|
|
@ -68,6 +68,7 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
||||||
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
{/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
|
{/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
|
||||||
<NavLink to="transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
<NavLink to="transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
|
<NavLink to="/contacts" icon={<GrContactInfo />} label="Contacts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react';
|
||||||
import { ContactTypeObj } from './RecipientAddCard'
|
import { ContactTypeObj } from './RecipientAddCard';
|
||||||
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
|
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
|
||||||
|
|
||||||
interface ContactDataProps {
|
interface ContactDataProps {
|
||||||
|
|
@ -10,66 +10,127 @@ interface ContactDataProps {
|
||||||
openContactModal: boolean;
|
openContactModal: boolean;
|
||||||
setOpenContactModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpenContactModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setRecipients: React.Dispatch<React.SetStateAction<Recipient>>;
|
setRecipients: React.Dispatch<React.SetStateAction<Recipient>>;
|
||||||
|
topRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
setManualFocusTriggered: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactModal: React.FC<ContactDataProps> = ({ 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 <span>{text}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTermLower = searchTerm.toLowerCase();
|
||||||
|
const textLower = text.toLowerCase();
|
||||||
|
const index = textLower.indexOf(searchTermLower);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return <span>{text}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeMatch = text.slice(0, index);
|
||||||
|
const match = text.slice(index, index + searchTerm.length);
|
||||||
|
const afterMatch = text.slice(index + searchTerm.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{beforeMatch}
|
||||||
|
<span className="bg-yellow-200 font-medium">{match}</span>
|
||||||
|
{afterMatch}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContactModal: React.FC<ContactDataProps> = ({
|
||||||
|
contactData,
|
||||||
|
inpFirstName,
|
||||||
|
inpLastName,
|
||||||
|
inpEmail,
|
||||||
|
openContactModal,
|
||||||
|
setRecipients,
|
||||||
|
setOpenContactModal ,
|
||||||
|
setManualFocusTriggered
|
||||||
|
}) => {
|
||||||
const handleContactClick = (indx: number) => {
|
const handleContactClick = (indx: number) => {
|
||||||
if (!contactData.contacts.length) return;
|
if (!contactData.contacts.length) return;
|
||||||
|
|
||||||
setRecipients((prev) => {
|
setRecipients((prev) => ({
|
||||||
return {
|
...prev, // Preserve other properties like uuid, newOne, etc.
|
||||||
firstName: contactData.contacts[indx].firstName,
|
firstName: contactData.contacts[indx].firstName,
|
||||||
lastName: contactData.contacts[indx].lastName,
|
lastName: contactData.contacts[indx].lastName,
|
||||||
email: contactData.contacts[indx].email,
|
email: contactData.contacts[indx].email,
|
||||||
type: prev.type
|
}));
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setOpenContactModal(false);
|
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 (
|
return (
|
||||||
<>
|
<div className="max-h-60 overflow-y-auto">
|
||||||
{
|
{contactData.contacts.map((contact, indx) => (
|
||||||
contactData.contacts.map((recipient, indx) => {
|
<div
|
||||||
return <div
|
key={contact.id || indx}
|
||||||
tabIndex={1}
|
tabIndex={0}
|
||||||
onClick={() => handleContactClick(indx)}
|
onMouseDown={() => handleContactClick(indx)}
|
||||||
style={{display: `${openContactModal ? "flex" : "none"}`}}
|
onKeyDown={(e) => handleKeyDown(e, indx)}
|
||||||
className='flex items-center px-3 py-2 cursor-pointer hover:bg-slate-100 transiton-all w-full'>
|
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"
|
||||||
<div
|
role="button"
|
||||||
key={indx}
|
aria-label={`Select contact ${contact.firstName} ${contact.lastName}`}
|
||||||
className='w-8 h-8 bg-gray-200 rounded-full'></div>
|
>
|
||||||
<div className='ml-2'>
|
{/* Avatar */}
|
||||||
<div className='flex gap-1.5'>
|
<div className="w-8 h-8 bg-[var(--primaryLight2)] rounded-full flex items-center justify-center text-white text-sm font-medium">
|
||||||
<p className='text-sm font-semibold'
|
{(contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')}
|
||||||
style={{ background: `${contactData.type === "firstName" ? `linear-gradient(to right, yellow 0ch, yellow ${inpFirstName.length}ch, white ${inpFirstName.length}ch, white 100%)` : "white"}` }}
|
</div>
|
||||||
>
|
|
||||||
{recipient.firstName}
|
{/* Contact Info */}
|
||||||
</p>
|
<div className="ml-3 flex-1 min-w-0">
|
||||||
<p
|
<div className="flex gap-1.5 items-center">
|
||||||
style={{ background: `${contactData.type === "lastName" ? `linear-gradient(to right, yellow 0ch, yellow ${inpLastName.length}ch, white ${inpLastName.length}ch, white 100%)` : "white"}` }}
|
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||||
className='text-sm font-semibold'>
|
{highlightText(
|
||||||
{recipient.lastName}
|
contact.firstName,
|
||||||
</p>
|
inpFirstName,
|
||||||
</div>
|
contactData.type === "firstName"
|
||||||
<p className='text-xs text-gray-500'
|
)}
|
||||||
style={{ background: `${contactData.type === "email" ? `linear-gradient(to right, yellow 0ch, yellow ${inpEmail.length}ch, white ${inpEmail.length}ch, white 100%)` : "white"}` }}
|
</p>
|
||||||
>
|
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||||
{recipient.email}
|
{highlightText(
|
||||||
|
contact.lastName,
|
||||||
|
inpLastName,
|
||||||
|
contactData.type === "lastName"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 truncate">
|
||||||
|
{highlightText(
|
||||||
|
contact.email,
|
||||||
|
inpEmail,
|
||||||
|
contactData.type === "email"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
})
|
</div>
|
||||||
}
|
))}
|
||||||
</>
|
|
||||||
)
|
{/* No results message */}
|
||||||
}
|
{contactData.contacts.length === 0 && (
|
||||||
|
<div className="px-3 py-4 text-center text-gray-500 text-sm">
|
||||||
|
No contacts found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// return <p
|
export default ContactModal;
|
||||||
// key={indx}
|
|
||||||
// className='w-full hover:bg-slate-100 cursor-pointer px-2 py-1'>
|
|
||||||
// {val.firstName}
|
|
||||||
// </p>
|
|
||||||
export default ContactModal
|
|
||||||
|
|
@ -6,7 +6,7 @@ import SelectedDocuments from "./SelectedDocuments";
|
||||||
import api from "../../../config/api";
|
import api from "../../../config/api";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RootState } from "../../../store/store";
|
import { RootState } from "../../../store/store";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { setImages } from "../../../store/slices/selectedDocsSlice";
|
import { setImages } from "../../../store/slices/selectedDocsSlice";
|
||||||
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
|
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
|
||||||
|
|
@ -39,10 +39,10 @@ const ProcessStepOne = () => {
|
||||||
dispatch(toggleToTemplates());
|
dispatch(toggleToTemplates());
|
||||||
}
|
}
|
||||||
|
|
||||||
const [editCase , setEditCase] = useState<string>("")
|
const [editCase, setEditCase] = useState<string>("")
|
||||||
const [editRecipient , setEditRecipient] = useState({})
|
const [editRecipient, setEditRecipient] = useState({})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if (!accountId || !transactionId) {
|
// if (!accountId || !transactionId) {
|
||||||
// window.location.href = "/";
|
// window.location.href = "/";
|
||||||
|
|
@ -55,6 +55,9 @@ const ProcessStepOne = () => {
|
||||||
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
||||||
const userData = useSelector((state: RootState) => state.user)
|
const userData = useSelector((state: RootState) => state.user)
|
||||||
|
// ProcessStepOne.tsx
|
||||||
|
const topRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// const userData = useSelector((state: RootState) => state.user)
|
// const userData = useSelector((state: RootState) => state.user)
|
||||||
|
|
||||||
const [transactionDetails, setTransactionDetails] = useState<any>([]);
|
const [transactionDetails, setTransactionDetails] = useState<any>([]);
|
||||||
|
|
@ -62,24 +65,29 @@ const ProcessStepOne = () => {
|
||||||
console.log("selectedRecipients", selectedRecipients)
|
console.log("selectedRecipients", selectedRecipients)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// <<<<<<< HEAD
|
// <<<<<<< HEAD
|
||||||
// Fetching Transaction Details - START
|
// Fetching Transaction Details - START
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
|
const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`);
|
||||||
setTransactionDetails(res.data)
|
setTransactionDetails(res.data)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error - transactions details")
|
console.log("Error - transactions details")
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
// Fetching Transaction Details - END
|
// Fetching Transaction Details - END
|
||||||
|
|
||||||
// =======
|
// =======
|
||||||
// (async () => {
|
// (async () => {
|
||||||
// const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`)
|
// const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`)
|
||||||
// console.log(res.data);
|
// console.log(res.data);
|
||||||
// })()
|
// })()
|
||||||
// >>>>>>> origin/main
|
// >>>>>>> origin/main
|
||||||
|
|
||||||
|
// (async () => {
|
||||||
|
// const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`)
|
||||||
|
// console.log(res.data);
|
||||||
|
// })()
|
||||||
|
|
||||||
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
||||||
|
|
@ -177,7 +185,7 @@ const ProcessStepOne = () => {
|
||||||
language: "en",
|
language: "en",
|
||||||
name: `${recipient.firstName} ${recipient.lastName}`,
|
name: `${recipient.firstName} ${recipient.lastName}`,
|
||||||
roleName: recipient.roleName,
|
roleName: recipient.roleName,
|
||||||
allowSenderToEdit: recipient.allowSenderToEdit ,
|
allowSenderToEdit: recipient.allowSenderToEdit,
|
||||||
allowSenderToDelete: recipient.allowSenderToDelete,
|
allowSenderToDelete: recipient.allowSenderToDelete,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -308,14 +316,14 @@ const ProcessStepOne = () => {
|
||||||
<p className="text-base font-normal text-gray-700 border-l pl-4">Add Recipient and Message</p>
|
<p className="text-base font-normal text-gray-700 border-l pl-4">Add Recipient and Message</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="add-tags-btn-step-one"
|
id="add-tags-btn-step-one"
|
||||||
className='flex items-center gap-3'>
|
className='flex items-center gap-3'>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNextClick(false)}
|
onClick={() => handleNextClick(false)}
|
||||||
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition"
|
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition"
|
||||||
>
|
>
|
||||||
{signType === SignTypeEnum.selfSign ? "Add Tags" : "Add Tags"}
|
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -350,9 +358,17 @@ const ProcessStepOne = () => {
|
||||||
{
|
{
|
||||||
signType !== SignTypeEnum.selfSign &&
|
signType !== SignTypeEnum.selfSign &&
|
||||||
<div className="flex flex-col gap-4 align-start">
|
<div className="flex flex-col gap-4 align-start">
|
||||||
<RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase}/>
|
<RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} />
|
||||||
{/* {signType !== "self-sign" && <RecipientAddCard />} */}
|
{/* {signType !== "self-sign" && <RecipientAddCard />} */}
|
||||||
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase}/>
|
|
||||||
|
<div
|
||||||
|
ref={topRef}
|
||||||
|
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none h-screen overflow-hidden' : ''}`}
|
||||||
|
>
|
||||||
|
|
||||||
|
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -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<string | null>(
|
||||||
|
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
|
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
|
||||||
const [openContactModal, setOpenContactModal] = useState(false);
|
const [openContactModal, setOpenContactModal] = useState(false);
|
||||||
|
|
@ -32,11 +36,31 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||||||
|
|
||||||
const [recipient, setRecipients] = useState<Recipient>({ uuid:null, newOne:false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false });
|
const [recipient, setRecipients] = useState<Recipient>({ uuid: null, newOne: false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false });
|
||||||
|
|
||||||
const [showAdvanceSettings, setShowAdvanceSettings] = useState(false)
|
const [showAdvanceSettings, setShowAdvanceSettings] = useState(false)
|
||||||
|
|
||||||
|
const [cachedContacts, setCachedContacts] = useState<ContactType[]>([]);
|
||||||
|
const [filteredContacts, setFilteredContacts] = useState<ContactType[]>([]);
|
||||||
|
const [activeField, setActiveField] = useState<"firstName" | "lastName" | "email" | null>(null);
|
||||||
|
const [hasFetchedInitial, setHasFetchedInitial] = useState(false);
|
||||||
|
const [lastQuery, setLastQuery] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [manualFocusTriggered, setManualFocusTriggered] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
console.log(recipient)
|
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(() => {
|
useEffect(() => {
|
||||||
if (editCase === "Edit" && editRecipient) {
|
if (editCase === "Edit" && editRecipient) {
|
||||||
setRecipients({
|
setRecipients({
|
||||||
|
|
@ -51,56 +75,55 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
allowSenderToEdit: editRecipient.allowSenderToEdit ?? false,
|
allowSenderToEdit: editRecipient.allowSenderToEdit ?? false,
|
||||||
allowSenderToDelete: editRecipient.allowSenderToDelete ?? false,
|
allowSenderToDelete: editRecipient.allowSenderToDelete ?? false,
|
||||||
uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''),
|
uuid: String(editRecipient?.id ?? editRecipient?.uuid ?? ''),
|
||||||
newOne:editRecipient.newOne ?? false
|
newOne: editRecipient.newOne ?? false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [editCase, editRecipient]);
|
}, [editCase, editRecipient]);
|
||||||
|
|
||||||
|
|
||||||
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
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
|
// ✅ Email format validation
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!emailRegex.test(recipient.email)) {
|
if (!emailRegex.test(recipient.email)) {
|
||||||
console.log("Invalid email format");
|
console.log("Invalid email format");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editCase === 'Edit') {
|
if (editCase === 'Edit') {
|
||||||
dispatch(updateRecipient(recipient));
|
dispatch(updateRecipient(recipient));
|
||||||
} else {
|
} else {
|
||||||
dispatch(addRecipient({
|
dispatch(addRecipient({
|
||||||
...recipient,
|
...recipient,
|
||||||
uuid:uuidv4(),
|
uuid: uuidv4(),
|
||||||
newOne:true
|
newOne: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Reset state and form
|
setRecipients({
|
||||||
setRecipients({
|
firstName: "",
|
||||||
firstName: "",
|
lastName: "",
|
||||||
lastName: "",
|
email: "",
|
||||||
email: "",
|
hostName: "",
|
||||||
hostName: "",
|
hostEmail: "",
|
||||||
hostEmail: "",
|
roleName: "",
|
||||||
roleName: "",
|
type: recipientType.SIGNER,
|
||||||
type: recipientType.SIGNER,
|
allowSenderToEdit: false,
|
||||||
allowSenderToEdit: false,
|
allowSenderToDelete: false,
|
||||||
allowSenderToDelete: false,
|
uuid: "",
|
||||||
uuid:"",
|
newOne: false
|
||||||
newOne:false
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// setRecipients(null),
|
// setRecipients(null),
|
||||||
setEditCase("")
|
setEditCase("")
|
||||||
|
setManualFocusTriggered(false)
|
||||||
formRef.current?.reset();
|
formRef.current?.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,7 +178,97 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
}, 350)
|
}, 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 (
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
<form
|
<form
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
onSubmit={handleRecipientAdd}
|
onSubmit={handleRecipientAdd}
|
||||||
|
|
@ -292,7 +405,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
<div className="grid grid-cols-12 gap-4 mt-3">
|
<div className="grid grid-cols-12 gap-4 mt-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
onFocus={() => setOpenContactModal(true)}
|
onFocus={() => handleFocus("firstName")}
|
||||||
onBlur={handleContactModalBlur}
|
onBlur={handleContactModalBlur}
|
||||||
value={recipient.firstName}
|
value={recipient.firstName}
|
||||||
placeholder="First Name"
|
placeholder="First Name"
|
||||||
|
|
@ -302,7 +415,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
onFocus={() => setOpenContactModal(true)}
|
onFocus={() => handleFocus("lastName")}
|
||||||
onBlur={handleContactModalBlur}
|
onBlur={handleContactModalBlur}
|
||||||
value={recipient.lastName}
|
value={recipient.lastName}
|
||||||
placeholder="Last Name"
|
placeholder="Last Name"
|
||||||
|
|
@ -312,7 +425,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="email"
|
type="email"
|
||||||
onFocus={() => setOpenContactModal(true)}
|
onFocus={() => handleFocus("email")}
|
||||||
onBlur={handleContactModalBlur}
|
onBlur={handleContactModalBlur}
|
||||||
value={recipient.email}
|
value={recipient.email}
|
||||||
placeholder="Recipient Email"
|
placeholder="Recipient Email"
|
||||||
|
|
@ -346,8 +459,8 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{/* Contact Modal */}
|
{/* Contact Modal */}
|
||||||
{contactData.contacts.length > 0 && openContactModal && (
|
{openContactModal && contactData.contacts.length > 0 && (
|
||||||
<div className="absolute z-50 mt-1 w-full max-w-2xl bg-white shadow-lg rounded-md p-3 border border-gray-100">
|
<div className="absolute z-50 mt-1 w-2/3 max-h-[50vh] bg-white shadow-lg rounded-md p-3 border border-gray-100 overflow-y-auto no-scrollbar">
|
||||||
<ContactModal
|
<ContactModal
|
||||||
contactData={contactData}
|
contactData={contactData}
|
||||||
inpFirstName={recipient.firstName}
|
inpFirstName={recipient.firstName}
|
||||||
|
|
@ -356,10 +469,14 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
openContactModal={openContactModal}
|
openContactModal={openContactModal}
|
||||||
setRecipients={setRecipients}
|
setRecipients={setRecipients}
|
||||||
setOpenContactModal={setOpenContactModal}
|
setOpenContactModal={setOpenContactModal}
|
||||||
|
topRef={topRef}
|
||||||
|
setManualFocusTriggered={setManualFocusTriggered}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Signing options + Add button in one line */}
|
{/* Signing options + Add button in one line */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div className="flex flex-wrap gap-6">
|
<div className="flex flex-wrap gap-6">
|
||||||
|
|
@ -386,11 +503,11 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,
|
||||||
type="submit"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
|
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,79 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
|
||||||
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move Down">
|
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move Down">
|
||||||
<FaArrowDown />
|
<FaArrowDown />
|
||||||
</button>
|
</button>
|
||||||
|
{/* Middle Row: Left - Center (Inputs) - Right */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{/* Left Button */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => handleMove('left')}
|
||||||
|
disabled={disabled}
|
||||||
|
className={buttonClass}
|
||||||
|
title="Move left"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<FaArrowLeft size={12} />
|
||||||
|
</button>
|
||||||
|
<span className="mt-1">Left</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center Panel */}
|
||||||
|
<div className="relative flex items-center justify-center bg-white rounded-lg px-1 py-2 w-24 ">
|
||||||
|
{/* Floating dimensions tooltip */}
|
||||||
|
<div className="absolute -top-2 left-1/2 -translate-x-1/2 px-2 py-[1px] text-[10px] font-mono text-black z-10 rounded-sm flex items-center gap-[2px]">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={leftValue}
|
||||||
|
onChange={(e) => handlePositionInputChange('left', e.target.value)}
|
||||||
|
onBlur={() => handlePositionInputBlur('left')}
|
||||||
|
onKeyPress={(e) => handleKeyPress(e, 'left')}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-[40px] h-[16px] text-[10px] text-center border-none focus:outline-none text-black font-mono"
|
||||||
|
/>
|
||||||
|
<span className="mx-[1px]">×</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={topValue}
|
||||||
|
onChange={(e) => handlePositionInputChange('top', e.target.value)}
|
||||||
|
onBlur={() => handlePositionInputBlur('top')}
|
||||||
|
onKeyPress={(e) => handleKeyPress(e, 'top')}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-[40px] h-[16px] text-[10px] text-center border-none focus:outline-none text-black font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Right Button */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => handleMove('right')}
|
||||||
|
disabled={disabled}
|
||||||
|
className={buttonClass}
|
||||||
|
title="Move right"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<FaArrowRight size={12} />
|
||||||
|
</button>
|
||||||
|
<span className="mt-1">Right</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Button */}
|
||||||
|
<div className="flex justify-center me-4">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => handleMove('down')}
|
||||||
|
disabled={disabled}
|
||||||
|
className={buttonClass}
|
||||||
|
title="Move down"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<FaArrowDown size={12} />
|
||||||
|
</button>
|
||||||
|
<span className="mt-1">Bottom</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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<ContactData>({
|
||||||
|
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 (
|
||||||
|
<div className="ms-2 space-y-2 max-w-[60vw]">
|
||||||
|
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{[
|
||||||
|
{ 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 }) => (
|
||||||
|
<div key={field} className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
|
||||||
|
<input
|
||||||
|
type={field === 'email' ? 'email' : 'text'}
|
||||||
|
value={formData[field as keyof ContactData] as string}
|
||||||
|
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] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.phones.map((phone, index) => (
|
||||||
|
<div key={index} className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={phone}
|
||||||
|
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 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removePhoneField(index)}
|
||||||
|
className="text-xs text-red-500 hover:underline"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{errors[`phones_${index}`] && (
|
||||||
|
<span className="text-sm text-red-500">{errors[`phones_${index}`]}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addPhoneField}
|
||||||
|
className="text-sm text-blue-500 hover:underline mt-1 cursor-pointer"
|
||||||
|
>
|
||||||
|
+ Add another phone
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-gray-300 px-5 py-2 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin inline-block mr-2" />
|
||||||
|
SAVING...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'SAVE CONTACT'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactForm;
|
||||||
|
|
@ -9,6 +9,7 @@ import AsideSection from "./AsideSection";
|
||||||
import Signature from "./Signature/Signature";
|
import Signature from "./Signature/Signature";
|
||||||
import { Bounce, toast } from "react-toastify";
|
import { Bounce, toast } from "react-toastify";
|
||||||
import AccountNav from "../accounts/AccountNav";
|
import AccountNav from "../accounts/AccountNav";
|
||||||
|
import ContactForm , {ContactData} from "./ContactForm";
|
||||||
|
|
||||||
// Define the shape of state object
|
// Define the shape of state object
|
||||||
interface Account {
|
interface Account {
|
||||||
|
|
@ -57,7 +58,7 @@ const Setting = () => {
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState("userProfile");
|
const [activeTab, setActiveTab] = useState("userProfile");
|
||||||
const [activeSubSection, setActiveSubSection] = useState<string>("");
|
const [activeSubSection, setActiveSubSection] = useState<string>("");
|
||||||
const [user, setUser] = useState<UserData>({
|
const [user, setUser] = useState<UserData>({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
|
@ -212,6 +213,13 @@ const Setting = () => {
|
||||||
accountOptionsLoading={accountOptionsLoading}
|
accountOptionsLoading={accountOptionsLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "contacts":
|
||||||
|
return (
|
||||||
|
<ContactForm
|
||||||
|
onSubmit={handleContactSubmit}
|
||||||
|
loading={createAndInviteLoader}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -236,6 +244,7 @@ const Setting = () => {
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "userProfile", label: "User Profile", type: "settings" },
|
{ id: "userProfile", label: "User Profile", type: "settings" },
|
||||||
{ id: "create and invite", label: "Create and invite", 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: "language", label: "Language", type: "settings" },
|
||||||
{ id: "contactAdd", label: "Add Contact", type: "contact" },
|
{ id: "contactAdd", label: "Add Contact", type: "contact" },
|
||||||
{ id: "contactRemove", label: "Remove 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <SettingNav /> */}
|
{/* <SettingNav /> */}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const Templates = () => {
|
||||||
console.log("handleAddTemplate called");
|
console.log("handleAddTemplate called");
|
||||||
try {
|
try {
|
||||||
const res = await api.post(`/api/accounts/${activeAccountId}/templates`);
|
const res = await api.post(`/api/accounts/${activeAccountId}/templates`);
|
||||||
navigate(`/process-step-one/${activeAccountId}/template/${res.data}`)
|
navigate(`/process-step-one/${activeAccountId}/template/${res.data.id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { AiFillSignature } from 'react-icons/ai';
|
import { AiFillSignature } from 'react-icons/ai';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { LuGitGraph } from 'react-icons/lu';
|
import { LuGitGraph } from 'react-icons/lu';
|
||||||
import AccountNav from '../accounts/AccountNav';
|
import AccountNav from '../accounts/AccountNav';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
@ -7,6 +7,8 @@ import api from '../../config/api';
|
||||||
import SigningOrderDiagram from '../../SigningOrderDiagram ';
|
import SigningOrderDiagram from '../../SigningOrderDiagram ';
|
||||||
import { IoCloseOutline } from 'react-icons/io5';
|
import { IoCloseOutline } from 'react-icons/io5';
|
||||||
import { MdEditDocument } from "react-icons/md";
|
import { MdEditDocument } from "react-icons/md";
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
|
||||||
// Sample recipient data — you can replace this with actual API data later
|
// Sample recipient data — you can replace this with actual API data later
|
||||||
const recipients = [
|
const recipients = [
|
||||||
|
|
@ -30,8 +32,8 @@ const recipients = [
|
||||||
|
|
||||||
const TransactionView = () => {
|
const TransactionView = () => {
|
||||||
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
console.log("account" , accountId , transactionId)
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (!accountId || !transactionId) {
|
if (!accountId || !transactionId) {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|
@ -40,6 +42,8 @@ const TransactionView = () => {
|
||||||
|
|
||||||
const [transactionDetails, setTransactionDetails] = useState(null);
|
const [transactionDetails, setTransactionDetails] = useState(null);
|
||||||
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
|
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
|
||||||
|
const user = useSelector((state: RootState) => state.user)
|
||||||
|
|
||||||
console.log(transactionDetails, "line 37");
|
console.log(transactionDetails, "line 37");
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
@ -56,6 +60,11 @@ const TransactionView = () => {
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [accountId, transactionId])
|
}, [accountId, transactionId])
|
||||||
|
|
||||||
|
|
||||||
|
const handleNavigateToSign = (id: number) => {
|
||||||
|
window.open(`./../../../../sign/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{transactionDetails &&
|
{transactionDetails &&
|
||||||
|
|
@ -64,17 +73,17 @@ const TransactionView = () => {
|
||||||
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<h1 className='text-lg font-semibold text-gray-800'>
|
<h1 className='text-lg font-semibold text-gray-800'>
|
||||||
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails?.transaction?.subject ?? "Untitled"}</span>
|
Complete with Forms Made Easy: <span className='text-slate-600'>{transactionDetails?.transaction?.subject ?? "Untitled"}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
|
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
|
||||||
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
|
<p className='text-slate-600'>From <span className='text-blue-600'>{transactionDetails?.transaction.createdBy.firstName + " " + transactionDetails?.transaction.createdBy.lastName}</span></p>
|
||||||
<p className=''><span className='text-slate-600 font-semibold'>Last Change on</span> <span className='text-slate-400'>{new Date(transactionDetails?.transaction?.lastModifiedAt * 1000).toISOString()}</span></p>
|
<p className=''><span className='text-slate-600 font-semibold'>Last Change on</span> <span className='text-slate-400'>{new Date(transactionDetails?.transaction?.lastModifiedAt * 1000).toISOString()}</span></p>
|
||||||
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
|
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
|
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>{transactionDetails?.transaction.status}</p>
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className='my-2 flex gap-2'>
|
<div className='my-2 flex gap-2'>
|
||||||
|
|
@ -97,51 +106,66 @@ const TransactionView = () => {
|
||||||
<hr className='my-6 border-slate-200' />
|
<hr className='my-6 border-slate-200' />
|
||||||
|
|
||||||
{/* Recipients Section */}
|
{/* Recipients Section */}
|
||||||
{transactionDetails.recipients.length > 0 &&
|
{transactionDetails.recipients.length > 0 &&
|
||||||
<div>
|
<div>
|
||||||
<div className='flex justify-between items-center text-md'>
|
<div className='flex justify-between items-center text-md'>
|
||||||
<h2 className='text-lg font-semibold text-gray-800 cursor-pointer'>Signers</h2>
|
<h2 className='text-lg font-semibold text-gray-800 cursor-pointer'>Signers</h2>
|
||||||
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer' onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}>
|
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer' onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}>
|
||||||
<LuGitGraph className='text-[0.9rem] text-[var(--primary)]' />
|
<LuGitGraph className='text-[0.9rem] text-[var(--primary)]' />
|
||||||
Signing Graph
|
Signing Graph
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recipients List */}
|
{/* Recipients List */}
|
||||||
{transactionDetails.recipients.map((recipient, index) => (
|
{transactionDetails.recipients.map((recipient, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<div className='mt-2 flex justify-between items-center'>
|
<div className='mt-2 flex justify-between items-center'>
|
||||||
<div className='flex gap-8 items-center'>
|
<div className='flex gap-8 items-center'>
|
||||||
<p className='text-slate-600'>{recipient.id}</p>
|
{/* <p className='text-slate-600'>{index+1}</p> */}
|
||||||
<div>
|
<div>
|
||||||
<p className='text-slate-600 font-semibold'>{recipient.name}</p>
|
<p className='text-slate-600 font-semibold'>{recipient.name}</p>
|
||||||
<p className='text-slate-600'>{recipient.email}</p>
|
<p className='text-slate-600'>{recipient.email}</p>
|
||||||
{recipient.hostEmail !== null && <p className='text-xs'><i><b>Host: {recipient.hostEmail}</b></i></p>}
|
{recipient.hostEmail !== null && recipient.hostEmail !== "" && <p className='text-xs'><i><b>Host: {recipient.hostEmail}</b></i></p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col leading-4.5 items-start text-md text-slate-600 pr-18'>
|
<div className='flex flex-col leading-4.5 items-start text-md text-slate-600'>
|
||||||
<div className='flex gap-1 items-center relative'>
|
<div className='flex gap-3 items-center relative'>
|
||||||
<AiFillSignature className='absolute left-[-30px] text-[1rem]' />
|
<div className='flex gap-1 items-center relative'>
|
||||||
<p className='text-black font-semibold'>{recipient.status}</p>
|
<AiFillSignature className='absolute left-[-30px] text-[1rem]' />
|
||||||
</div>
|
<p className='text-black font-semibold'>{recipient.status}</p>
|
||||||
{recipient.status !== "PENDING" && <p>on {recipient.completedAt}</p>}
|
</div>
|
||||||
{/* <a href={recipient.viewLink} className='text-blue-600 hover:underline'>
|
{recipient.email === user.email &&
|
||||||
|
<p
|
||||||
|
onClick={() => handleNavigateToSign(recipient.id)}
|
||||||
|
className='text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold'>
|
||||||
|
Sign
|
||||||
|
</p>}
|
||||||
|
</div>
|
||||||
|
{recipient.status !== "PENDING" && <p>on {recipient.completedAt}</p>}
|
||||||
|
{/* <a href={recipient.viewLink} className='text-blue-600 hover:underline'>
|
||||||
View signed location
|
View signed location
|
||||||
</a> */}
|
</a> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<hr className='my-2 border-slate-200' />
|
||||||
</div>
|
</div>
|
||||||
<hr className='my-2 border-slate-200' />
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<div>
|
{transactionDetails?.transaction?.subject !== "" &&
|
||||||
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
<div>
|
||||||
<p className='mt-2 text-slate-600'>{ transactionDetails?.transaction?.message ?? "No Message Found...!"}</p>
|
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Subject</h2>
|
||||||
</div>
|
<p className='mt-2 text-slate-600'>{`${transactionDetails?.transaction?.subject ? transactionDetails?.transaction?.subject : "Not Found"} `}</p>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{transactionDetails?.transaction?.message !== "" &&
|
||||||
|
<div>
|
||||||
|
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
||||||
|
<p className='mt-2 text-slate-600'>{`${transactionDetails?.transaction?.message ? transactionDetails?.transaction?.message : "Not Found"} `}</p>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-[210px] text-xs rounded-sm '>
|
<div className='w-[210px] text-xs rounded-sm '>
|
||||||
|
|
@ -152,10 +176,10 @@ const TransactionView = () => {
|
||||||
<img style={{ width: "200px", height: "160px" }} src="https://images.unsplash.com/photo-1746469535771-71a672e8719f?q=80&w=870&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="" />
|
<img style={{ width: "200px", height: "160px" }} src="https://images.unsplash.com/photo-1746469535771-71a672e8719f?q=80&w=870&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="" />
|
||||||
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
|
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
|
||||||
<p className='text-[0.7rem] text-slate-500 flex justify-between items-center'>
|
<p className='text-[0.7rem] text-slate-500 flex justify-between items-center'>
|
||||||
<span> 1 of 10</span>
|
<span> 1 of 10</span>
|
||||||
<span className='cursor-pointer'><Link to={`/process-step-two/${accountId}/${transactionId}`}><MdEditDocument size={16}/></Link></span>
|
<span className='cursor-pointer'><Link to={`/process-step-two/${accountId}/${transactionId}`}><MdEditDocument size={16} /></Link></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -165,7 +189,7 @@ const TransactionView = () => {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{signingOrderModal.open &&
|
{signingOrderModal.open &&
|
||||||
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center overflow-hidden">
|
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center overflow-hidden">
|
||||||
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full ">
|
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full ">
|
||||||
<div className="pb-2 text-white flex justify-between items-center bg-[var(--primary)] px-2 py-3 rounded-t-lg">
|
<div className="pb-2 text-white flex justify-between items-center bg-[var(--primary)] px-2 py-3 rounded-t-lg">
|
||||||
|
|
|
||||||
|
|
@ -113,14 +113,14 @@ canvas {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
color: var(--primaryLight);
|
color: var(--primaryLight);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-border>div:hover {
|
.bottom-border>div:hover {
|
||||||
background-color: var(--primaryLight2) ;
|
background-color: var(--primaryLight2);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -129,8 +129,8 @@ canvas {
|
||||||
/* Group Border - END */
|
/* Group Border - END */
|
||||||
|
|
||||||
|
|
||||||
/* Loader CSS */
|
/* Loader CSS */
|
||||||
/* From Uiverse.io by Juanes200122 */
|
/* From Uiverse.io by Juanes200122 */
|
||||||
.pl {
|
.pl {
|
||||||
width: 6em;
|
width: 6em;
|
||||||
height: 6em;
|
height: 6em;
|
||||||
|
|
@ -158,10 +158,12 @@ canvas {
|
||||||
animation-name: ringD;
|
animation-name: ringD;
|
||||||
stroke: #41644A;
|
stroke: #41644A;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes ringA {
|
@keyframes ringA {
|
||||||
from, 4% {
|
|
||||||
|
from,
|
||||||
|
4% {
|
||||||
stroke-dasharray: 0 660;
|
stroke-dasharray: 0 660;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -330;
|
stroke-dashoffset: -330;
|
||||||
|
|
@ -179,7 +181,8 @@ canvas {
|
||||||
stroke-dashoffset: -595;
|
stroke-dashoffset: -595;
|
||||||
}
|
}
|
||||||
|
|
||||||
40%, 54% {
|
40%,
|
||||||
|
54% {
|
||||||
stroke-dasharray: 0 660;
|
stroke-dasharray: 0 660;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -660;
|
stroke-dashoffset: -660;
|
||||||
|
|
@ -197,7 +200,8 @@ canvas {
|
||||||
stroke-dashoffset: -925;
|
stroke-dashoffset: -925;
|
||||||
}
|
}
|
||||||
|
|
||||||
90%, to {
|
90%,
|
||||||
|
to {
|
||||||
stroke-dasharray: 0 660;
|
stroke-dasharray: 0 660;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -990;
|
stroke-dashoffset: -990;
|
||||||
|
|
@ -205,7 +209,9 @@ canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ringB {
|
@keyframes ringB {
|
||||||
from, 12% {
|
|
||||||
|
from,
|
||||||
|
12% {
|
||||||
stroke-dasharray: 0 220;
|
stroke-dasharray: 0 220;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -110;
|
stroke-dashoffset: -110;
|
||||||
|
|
@ -223,7 +229,8 @@ canvas {
|
||||||
stroke-dashoffset: -195;
|
stroke-dashoffset: -195;
|
||||||
}
|
}
|
||||||
|
|
||||||
48%, 62% {
|
48%,
|
||||||
|
62% {
|
||||||
stroke-dasharray: 0 220;
|
stroke-dasharray: 0 220;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -220;
|
stroke-dashoffset: -220;
|
||||||
|
|
@ -241,7 +248,8 @@ canvas {
|
||||||
stroke-dashoffset: -305;
|
stroke-dashoffset: -305;
|
||||||
}
|
}
|
||||||
|
|
||||||
98%, to {
|
98%,
|
||||||
|
to {
|
||||||
stroke-dasharray: 0 220;
|
stroke-dasharray: 0 220;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -330;
|
stroke-dashoffset: -330;
|
||||||
|
|
@ -267,7 +275,8 @@ canvas {
|
||||||
stroke-dashoffset: -175;
|
stroke-dashoffset: -175;
|
||||||
}
|
}
|
||||||
|
|
||||||
36%, 58% {
|
36%,
|
||||||
|
58% {
|
||||||
stroke-dasharray: 0 440;
|
stroke-dasharray: 0 440;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -220;
|
stroke-dashoffset: -220;
|
||||||
|
|
@ -285,7 +294,8 @@ canvas {
|
||||||
stroke-dashoffset: -395;
|
stroke-dashoffset: -395;
|
||||||
}
|
}
|
||||||
|
|
||||||
94%, to {
|
94%,
|
||||||
|
to {
|
||||||
stroke-dasharray: 0 440;
|
stroke-dasharray: 0 440;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -440;
|
stroke-dashoffset: -440;
|
||||||
|
|
@ -293,7 +303,9 @@ canvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ringD {
|
@keyframes ringD {
|
||||||
from, 8% {
|
|
||||||
|
from,
|
||||||
|
8% {
|
||||||
stroke-dasharray: 0 440;
|
stroke-dasharray: 0 440;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: 0;
|
stroke-dashoffset: 0;
|
||||||
|
|
@ -311,7 +323,8 @@ canvas {
|
||||||
stroke-dashoffset: -175;
|
stroke-dashoffset: -175;
|
||||||
}
|
}
|
||||||
|
|
||||||
44%, 50% {
|
44%,
|
||||||
|
50% {
|
||||||
stroke-dasharray: 0 440;
|
stroke-dasharray: 0 440;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -220;
|
stroke-dashoffset: -220;
|
||||||
|
|
@ -329,14 +342,16 @@ canvas {
|
||||||
stroke-dashoffset: -395;
|
stroke-dashoffset: -395;
|
||||||
}
|
}
|
||||||
|
|
||||||
86%, to {
|
86%,
|
||||||
|
to {
|
||||||
stroke-dasharray: 0 440;
|
stroke-dasharray: 0 440;
|
||||||
stroke-width: 20;
|
stroke-width: 20;
|
||||||
stroke-dashoffset: -440;
|
stroke-dashoffset: -440;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-border>div:hover {
|
.bottom-border>div:hover {
|
||||||
background-color: var(--primaryLight2) ;
|
background-color: var(--primaryLight2);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -344,3 +359,11 @@ canvas {
|
||||||
|
|
||||||
/* Group Border - END */
|
/* Group Border - END */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* JoyRide - START */
|
||||||
|
.__floater__body > div > button:last-child {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JoyRide - END */
|
||||||
|
|
@ -144,7 +144,7 @@ const JoyrideController = () => {
|
||||||
target: "body", // This makes the welcome step show in center of screen
|
target: "body", // This makes the welcome step show in center of screen
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>👋 Welcome to SME, Let's Start the tour</p>
|
<p>👋 Hi there! Welcome to SME. Let’s start signing.</p>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
placement: "center",
|
placement: "center",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue