Merge remote-tracking branch 'origin/main'

This commit is contained in:
=bhagwanSingh 2025-08-04 15:35:17 +05:30
commit 479ad820be
13 changed files with 1009 additions and 800 deletions

View File

@ -11,6 +11,7 @@ type ContactSchema = [{
firstName: string
lastName: string
email: string
phone: string
}]
const Contacts = () => {

View File

@ -1,203 +1,214 @@
// ContactForm.tsx
import React, { useEffect, useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
import React, { useEffect, 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[];
contactId?: number;
email: string;
firstName: string;
lastName: string;
title: string;
company: string;
website: string;
phone: string;
}
interface Props {
onSubmit: (data: ContactData) => void;
loading: boolean;
setShowModal: () => React.Dispatch<React.SetStateAction<boolean>>
actionStatus: string,
editJsonData: any
onSubmit: (data: ContactData) => void;
loading: boolean;
setShowModal: () => React.Dispatch<React.SetStateAction<boolean>>;
actionStatus: string;
editJsonData: any;
}
const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJsonData }: Props) => {
const [formData, setFormData] = useState<ContactData>({
email: '',
firstName: '',
lastName: '',
title: '',
company: '',
website: '',
phones: [''],
});
const ContactFormModal = ({
onSubmit,
loading,
setShowModal,
actionStatus,
editJsonData,
}: Props) => {
const [formData, setFormData] = useState<ContactData>({
email: "",
firstName: "",
lastName: "",
title: "",
company: "",
website: "",
phone: "",
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
useEffect(() => {
if (actionStatus === 'Edit' && editJsonData) {
setFormData({
email: editJsonData.email,
firstName: editJsonData.firstName,
lastName: editJsonData.lastName,
title: editJsonData.title,
company: editJsonData.company,
website: editJsonData.website,
phones: editJsonData.phones,
})
}
}, [actionStatus, editJsonData])
useEffect(() => {
if (actionStatus === "Edit" && editJsonData) {
setFormData({
email: editJsonData.email,
firstName: editJsonData.firstName,
lastName: editJsonData.lastName,
title: editJsonData.title,
company: editJsonData.company,
website: editJsonData.website,
phone: editJsonData.phone,
});
}
}, [actionStatus, editJsonData]);
const handleChange = (field: keyof ContactData, value: string) => {
console.log(field, value);
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
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.phone];
// updatedPhones[index] = numericValue;
// setFormData((prev) => ({ ...prev, phones: updatedPhones }));
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 errorKey = `phones_${index}`;
if (errors[errorKey]) {
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
}
};
// const addPhoneField = () => {
// setFormData((prev) => ({ ...prev, phone: [...prev.phone, ''] }));
// };
// const removePhoneField = (index: number) => {
// const updatedPhones = formData.phones.filter((_, i) => i !== index);
// setFormData((prev) => ({ ...prev, phones: updatedPhones }));
// };
const addPhoneField = () => {
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
};
const validate = () => {
const newErrors: { [key: string]: string } = {};
const removePhoneField = (index: number) => {
const updatedPhones = formData.phones.filter((_, i) => i !== index);
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
};
const emailRegex = /\S+@\S+\.\S+/;
// const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex =
/^\+?\d{1,4}?[-.\s]?\(?\d{2,5}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}$/;
const validate = () => {
const newErrors: { [key: string]: string } = {};
// Email
if (!formData.email.trim()) {
newErrors.email = "Email is required";
} else if (!emailRegex.test(formData.email)) {
newErrors.email = "Invalid email address";
}
const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex = /^\d+$/;
// // First Name
// if (!formData.firstName.trim()) {
// newErrors.firstName = 'First name is required';
// } else
// if (formData.firstName.trim() && !nameRegex.test(formData.firstName)) {
// newErrors.firstName = "First name contains invalid characters";
// }
// Email
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email address';
}
// 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";
// }
// // 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';
// }
// // Title
// if (!formData.title.trim()) {
// newErrors.title = 'Title is required';
// }
// 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';
}
// Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = "Invalid website URL";
}
// // Title
// if (!formData.title.trim()) {
// newErrors.title = 'Title is required';
// }
// Phone
if (formData.phone.trim() && !phoneRegex.test(formData.phone)) {
newErrors.phone = "Phone number must contain only digits";
}
return newErrors;
};
const handleSubmit = async () => {
const validationErrors = validate();
let type = "Add";
let payload = formData;
console.log(payload);
// Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = 'Invalid website URL';
}
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
if (actionStatus === "Edit") {
type = "Edit";
payload = {
...formData,
contactId: editJsonData.id,
};
}
await onSubmit(payload, type);
// 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';
// }
// });
setFormData({
email: "",
firstName: "",
lastName: "",
title: "",
company: "",
website: "",
phone: "",
});
setErrors({});
} catch (err) {
console.error("Submit failed:", err);
}
};
return newErrors;
};
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", fieldType: "email" },
{ label: "Title", field: "title" },
{ label: "Company", field: "company" },
{ label: "Website", field: "website", fieldType: "url" },
{ label: "Phone", field: "phone", fieldType: "tel" },
].map(({ label, field, fieldType }) => (
<div key={field} className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
{label}
</label>
<input
type={fieldType ? fieldType : "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>
const handleSubmit = async () => {
const validationErrors = validate();
let type = 'Add'
let payload = formData
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
if (actionStatus === 'Edit') {
type = 'Edit'
payload = {
...formData,
contactId: editJsonData.id
}
}
await onSubmit(payload, type);
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">
{/* <div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Number</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">
@ -233,40 +244,38 @@ const ContactFormModal = ({ onSubmit, loading, setShowModal, actionStatus, editJ
+ Add another phone
</button>
</div>
</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...
</>
) : (
`${actionStatus?.toUpperCase() || 'SAVE'} CONTACT`
)}
</button>
</div>
</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...
</>
) : (
`${actionStatus?.toUpperCase() || "SAVE"} CONTACT`
)}
</button>
</div>
</div>
);
</div>
</div>
);
};
export default ContactFormModal;

View File

@ -204,7 +204,7 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRa
// }
return (
<div className="w-full px-4 py-3 mt-2 rounded-md">
<div className="w-full px-3 rounded-md">
{/* Header */}
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-2 gap-4">
<div className="flex items-center gap-3">
@ -212,7 +212,7 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRa
<FaFilter className="text-white text-lg" />
</div> */}
<div>
<h2 className="text-lg font-semibold text-gray-800">Overview</h2>
{/* <h2 className="text-lg font-semibold text-gray-800">Overview</h2> */}
{/* <p className="text-gray-600 text-sm">Monitor performance metrics</p> */}
</div>
</div>
@ -231,11 +231,11 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRa
</div>
</div>
</div> :
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<div className="flex flex-wrap gap-2 justify-between">
{cards.map((card, index) => (
<div
key={card.title}
className={`group relative overflow-hidden rounded-xl ${card.gradient} border border-white/40 p-4 transition-all duration-300 hover:scale-105 hover:shadow-lg ${card.shadowColor} cursor-pointer backdrop-blur-sm`}
className={`group relative overflow-hidden rounded-lg ${card.gradient} border border-white/40 px-3 py-1 transition-all duration-300 hover:scale-105 hover:shadow-lg ${card.shadowColor} cursor-pointer backdrop-blur-sm min-w-[200px] max-w-[220px]`}
style={{
animationDelay: `${index * 100}ms`
}}
@ -247,24 +247,20 @@ const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal, onDateRa
</div>
{/* Content */}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<div className="relative flex justify-between items-center z-10">
<div className="flex items-center gap-2">
<div className={`p-2 rounded-lg border ${card.iconBg} group-hover:scale-110 transition-transform duration-200`}>
{card.icon}
</div>
{renderTrendIndicator(card.trend, card.percentage)}
</div>
<div className="space-y-1">
<h3 className="text-xs font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
<h3 className="text-sm font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
{card.title}
</h3>
<div className="flex items-baseline gap-1">
<p className={`text-xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200`}>
{card.count}
</p>
<span className="text-[10px] text-gray-500">items</span>
</div>
</div>
<div className="flex items-center">
<p className={`text-xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200 leading-none mb-1`}>
{card.count}
</p>
</div>
</div>

View File

@ -35,7 +35,7 @@ const Home = () => {
{
title: 'Action Needed',
count: data.actionNeeded || 0,
icon: <FaTasks size={18} className="text-indigo-600" />,
icon: <FaTasks size={15} className="text-indigo-600" />,
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
shadowColor: 'shadow-indigo-200/50',
iconBg: 'bg-indigo-100 border-indigo-200',
@ -45,7 +45,7 @@ const Home = () => {
{
title: 'Expiring Soon',
count: data.expiring || 0,
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
icon: <FaHourglassHalf size={15} className="text-rose-600" />,
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
shadowColor: 'shadow-rose-200/50',
iconBg: 'bg-rose-100 border-rose-200',
@ -55,7 +55,7 @@ const Home = () => {
{
title: 'Completed',
count: data.completed || 0,
icon: <FaCheckCircle size={18} className="text-teal-600" />,
icon: <FaCheckCircle size={15} className="text-teal-600" />,
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
shadowColor: 'shadow-teal-200/50',
iconBg: 'bg-teal-100 border-teal-200',
@ -65,7 +65,7 @@ const Home = () => {
{
title: 'Draft',
count: data.draft || 0,
icon: <FaClock size={18} className="text-stone-600" />,
icon: <FaClock size={15} className="text-stone-600" />,
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
shadowColor: 'shadow-blue-200/50',
iconBg: 'bg-blue-100 border-blue-200',

View File

@ -7,15 +7,17 @@ import api from "../../../config/api";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../store/store";
import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { setImages } from "../../../store/slices/selectedDocsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, removeAllRecipients, removeRecipientAsync, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
import Loader from "../../dashboard/Loader";
import { Bounce, toast } from "react-toastify";
import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from "../../../store/slices/documentSlice";
import { SignTypeEnum } from "../../../utlity/types";
import { RiPencilFill } from "react-icons/ri";
import SelfSignWarnModal from "./SelfSignWarnModal";
const ProcessStepOne = () => {
interface EmailDtls {
@ -28,6 +30,9 @@ const ProcessStepOne = () => {
const signType = useParams<{ signType: string }>().signType;
const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const [searchParams] = useSearchParams();
console.log("searchParams", searchParams.get('useTemplate'))
const isTamplate = searchParams.get('useTemplate')
const navigate = useNavigate();
const dispatch = useDispatch();
@ -54,10 +59,29 @@ const ProcessStepOne = () => {
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const [showSelfSignWarnModal, setShowSelfSignWarnModal] = useState(false)
console.log()
const userData = useSelector((state: RootState) => state.user)
// ProcessStepOne.tsx
const topRef = useRef<HTMLDivElement | null>(null);
const [templateName, setTemplateName] = useState('Untitled');
const [isTyping, setIsTyping] = useState(false);
// Optional: use a timer to detect pause in typing
let typingTimeout = null;
const handleChange = (e) => {
setTemplateName(e.target.value);
setIsTyping(true);
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
setIsTyping(false);
}, 5000); // hide icon while typing, show it again after 500ms of no input
};
// const userData = useSelector((state: RootState) => state.user)
const [transactionDetails, setTransactionDetails] = useState<any>([]);
@ -71,6 +95,7 @@ const ProcessStepOne = () => {
try {
const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`);
setTransactionDetails(res.data)
// console.log("setTransactionDetails",res.data)
} catch (e) {
console.log("Error - transactions details")
}
@ -111,38 +136,41 @@ const ProcessStepOne = () => {
subject: false
}
if (!selectedDocuments.length) {
formError.documents = true
}
if (!selectedRecipients.length) {
formError.recipients = true
}
if (!emailDtls.subject.length) {
formError.subject = true
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
if (!selectedDocuments.length) {
formError.documents = true
}
if (!selectedRecipients.length) {
formError.recipients = true
}
if (!emailDtls.subject.length) {
formError.subject = true
}
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
if (formError.documents || formError.recipients || formError.subject) {
console.log(selectedDocuments, 'line 193')
toast.dismiss();
toast.error(`${formError.documents ? "Add Document" : ""}
${(formError.documents && formError.recipients) ? " | " : ""}
${formError.recipients ? "Add Recipient" : ""}
${(formError.documents || formError.recipients) ? " | " : ""}
${formError.subject ? "Add Subject" : ""}`, {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
return;
}
}
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
if (formError.documents || formError.recipients || formError.subject) {
console.log(selectedDocuments, 'line 193')
toast.dismiss();
toast.error(`${formError.documents ? "Add Document" : ""}
${(formError.documents && formError.recipients) ? " | " : ""}
${formError.recipients ? "Add Recipient" : ""}
${(formError.documents || formError.recipients) ? " | " : ""}
${formError.subject ? "Add Subject" : ""}`, {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
return;
}
try {
dispatch(showLoader("Processing Documents..."));
@ -194,6 +222,17 @@ const ProcessStepOne = () => {
let recipientId = null;
if (signType === SignTypeEnum.selfSign) {
dispatch(removeAllRecipients());
await Promise.all(
selectedRecipients.map((recipient) => dispatch(removeRecipientAsync({
recipientId: recipient.id,
email: recipient.email,
accountId,
transactionId,
}))
)
);
recipientsDataTemp = [{
id: null,
routingOrder: 1,
@ -231,6 +270,7 @@ const ProcessStepOne = () => {
dispatch(showLoader("Processing Message..."));
let res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, {
name: templateName,
subject: emailDtls.subject, message: emailDtls.message,
status: "CREATED"
})
@ -248,7 +288,7 @@ const ProcessStepOne = () => {
dispatch(hideLoader());
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
console.log(`/process-step-two/${accountId}/template/${transactionId}`)
if(!saveAndClose) {
if (!saveAndClose) {
navigate(`/process-step-two/${accountId}/template/${transactionId}`)
} else {
navigate(`/templates`)
@ -297,6 +337,7 @@ const ProcessStepOne = () => {
const handleSelfSignCb = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
navigate("self_sign");
setShowSelfSignWarnModal((prev) => !prev)
} else {
navigate("./../");
}
@ -315,7 +356,21 @@ const ProcessStepOne = () => {
onClick={handleClose}
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
/>
<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 relative">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ?
<>
<input
value={templateName}
onChange={handleChange}
className="w-full px-4 py-1.5 border-2 border-dashed rounded-md text-sm focus:outline-none transition border-gray-300 focus:ring-2 focus:border-dashed focus:ring-[var(--primary)]"
/>
{!isTyping && (
<RiPencilFill className="absolute top-2.5 right-2 text-[17px]" />
)}
</> :
"Add Recipient and Message"
}
</p>
</div>
<div
@ -352,7 +407,7 @@ const ProcessStepOne = () => {
{/* Recipients */}
<section className="mt-8">
{
{!isTamplate &&
transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&
<div className="flex justify-between items-center">
<h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Signers</h2>
@ -368,9 +423,9 @@ const ProcessStepOne = () => {
}
{
signType !== SignTypeEnum.selfSign &&
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
<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 />} */}
<div
@ -418,6 +473,7 @@ const ProcessStepOne = () => {
</div>
{globalLoader.isLoading && <Loader />}
{showSelfSignWarnModal && <SelfSignWarnModal setShowSelfSignWarnModal={setShowSelfSignWarnModal} />}
</div >
</>
);

View File

@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { FaPlus } from "react-icons/fa";
import { addRecipient, Recipient, recipientType, TRANSACTION_TEMPLATE_TOGGLE, updateRecipient } from "../../../store/slices/selectedRecipientsSlice";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../../store/store";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../../store/store";
import api from "../../../config/api";
import ContactModal from "./ContactModal";
import useDebouncedEffect from "../../../utlity/useDebounce";
@ -29,6 +29,8 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
console.log("recipients.length in AddCase", recipients)
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
const [openContactModal, setOpenContactModal] = useState(false);
@ -47,6 +49,14 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
const [lastQuery, setLastQuery] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [manualFocusTriggered, setManualFocusTriggered] = useState(false);
const [errors, setErrors] = useState({
roleName: false,
lastName: false,
email: false,
emailFormat: false,
type: false,
});
console.log(recipient)
@ -62,7 +72,37 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
}, [openContactModal]);
useEffect(() => {
// Check if the recipient exists in the list (using uuid or id or email)
const exists = recipients.some((r) => {
return (
(r.uuid && r.uuid === editRecipient?.uuid) ||
(r.id && r.id === editRecipient?.id) ||
(r.email && r.email === editRecipient?.email)
);
});
console.log("Line 83", exists)
if (editCase === "Edit" && editRecipient) {
console.log("editRecipientAdd", editRecipient)
if (!exists) {
// Recipient was deleted -> reset
setEditCase('');
setEditRecipient(null);
setRecipients({
firstName: "",
lastName: "",
email: "",
hostName: "",
hostEmail: "",
roleName: "",
type: recipientType.SIGNER,
allowSenderToEdit: true,
allowSenderToDelete: true,
uuid: "",
newOne: false,
});
return;
}
setRecipients({
// id:editRecipient.id,
firstName: editRecipient.firstName || "",
@ -78,104 +118,135 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
newOne: editRecipient.newOne ?? false
});
}
}, [editCase, editRecipient]);
}, [editCase, editRecipient, recipients]);
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("recipientrecipient", recipient)
const newErrors = {
roleName: false,
lastName: false,
email: false,
emailFormat: false,
type: false,
};
if(transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS) {
if (!recipient.lastName || !recipient.email || !recipient.type) return;
const roleNameFilled = recipient.roleName.trim();
const lastNameFilled = recipient.lastName.trim();
const emailFilled = recipient.email.trim();
const firstNameFilled = recipient.firstName.trim()
// ✅ Email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(recipient.email)) {
console.log("Invalid email format");
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// 🔹 Check if we're in TRANSACTIONS mode
if (transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS) {
if (lastNameFilled || emailFilled) {
if (lastNameFilled && !emailFilled) {
setErrors({ ...newErrors, email: true });
return;
}
if (!lastNameFilled && emailFilled) {
setErrors({ ...newErrors, lastName: true });
return;
}
if (!emailRegex.test(emailFilled)) {
setErrors({ ...newErrors, emailFormat: true });
return;
}
submitForm();
return;
}
// Neither lastName nor email is filled — treat as invalid
setErrors({ ...newErrors, lastName: true, email: true });
return;
}
if (editCase === 'Edit') {
dispatch(updateRecipient(recipient));
} else {
dispatch(addRecipient({
...recipient,
uuid: uuidv4(),
newOne: true
}));
// // 1. If either lastName or email is filled, validate their presence mutually
// if ( emailFilled) {
// // if (lastNameFilled && !emailFilled) {
// // setErrors({ ...newErrors, email: true });
// // return;
// // }
// // if (!lastNameFilled && emailFilled) {
// // setErrors({ ...newErrors, lastName: true });
// // return;
// // }
// // // Both lastName and email are filled — check email format
// if (!emailRegex.test(emailFilled)) {
// setErrors({ ...newErrors, emailFormat: true });
// return;
// }
// // Passed validation, submit
// submitForm();
// return;
// }
if (roleNameFilled || lastNameFilled || emailFilled || firstNameFilled) {
submitForm()
return
}
setRecipients({
firstName: "",
lastName: "",
email: "",
hostName: "",
hostEmail: "",
roleName: "",
type: recipientType.SIGNER,
allowSenderToEdit: true,
allowSenderToDelete: true,
uuid: "",
newOne: false
});
// 2. If both lastName and email empty, validate roleName only
if (!roleNameFilled) {
setErrors({ ...newErrors, roleName: true });
return;
}
// setRecipients(null),
setEditCase("")
setManualFocusTriggered(false)
formRef.current?.reset();
// roleName filled, submit immediately
submitForm();
function submitForm() {
if (editCase === 'Edit') {
dispatch(updateRecipient(recipient));
} else {
dispatch(addRecipient({
...recipient,
uuid: uuidv4(),
newOne: true,
}));
}
setRecipients({
firstName: "",
lastName: "",
email: "",
hostName: "",
hostEmail: "",
roleName: "",
type: recipientType.SIGNER,
allowSenderToEdit: true,
allowSenderToDelete: true,
uuid: "",
newOne: false,
});
setErrors({
roleName: false,
lastName: false,
email: false,
emailFormat: false,
type: false,
});
setEditCase("");
setManualFocusTriggered(false);
formRef.current?.reset();
}
};
// useEffect(() => {
// if (!recipient.firstName.length) return;
// const handler = setTimeout(() => {
// api.get(`/api/contact?firstName=${recipient.firstName}`)
// .then(res => {
// setContactData({ type: "firstName", contacts: res.data.content });
// })
// .catch(err => console.log(err));
// }, 500); // wait 500ms after the last change
// return () => {
// clearTimeout(handler); // cancel previous timeout if effect runs again
// };
// }, [recipient.firstName]);
// useDebouncedEffect(() => {
// if (!recipient.firstName.length) return;
// api.get(`/api/contact?firstName=${recipient.firstName}`)
// .then(res => {
// setContactData({ type: "firstName", contacts: res.data.content });
// })
// .catch(err => console.log(err));
// }, [recipient.firstName], 500);
// useDebouncedEffect(() => {
// if (!recipient.lastName.length) return
// api.get(`/api/contact?lastName=${recipient.lastName}`)
// .then(res => {
// setContactData({ type: "lastName", contacts: res.data.content });
// })
// .catch(err => console.log(err))
// }, [recipient.lastName], 500);
// useDebouncedEffect(() => {
// if (!recipient.email.length) return
// api.get(`/api/contact?email=${recipient.email}`)
// .then(res => {
// setContactData({ type: "email", contacts: res.data.content });
// })
// .catch(err => console.log(err))
// }, [recipient.email], 500);
const handleContactModalBlur = () => {
setTimeout(() => {
setOpenContactModal(false)
@ -249,131 +320,138 @@ setManualFocusTriggered(false)
}
}, [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);
}
};
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
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("focusin", handleClickOutside); // Also handles tabbing out
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("focusin", handleClickOutside);
};
}, []);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("focusin", handleClickOutside);
};
}, []);
return (
<div ref={containerRef}>
<form
ref={formRef}
onSubmit={handleRecipientAdd}
id="add-recipient-step-one"
className="w-full bg-white border border-gray-200 rounded-lg shadow-md px-6 py-4 space-y-3 relative"
>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="space-y-1">
{/* Top Row: Role Input + Settings */}
<div className="flex justify-between items-center">
{/* Role Name Input */}
<input
required={recipient.firstName === "" && recipient.lastName === "" && recipient.email === ""}
type="text"
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.roleName}
placeholder="Role name"
className="w-64 px-3 py-2 me-3 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, roleName: e.target.value })}
/>
<form
ref={formRef}
onSubmit={handleRecipientAdd}
id="add-recipient-step-one"
className="w-full bg-white border border-gray-200 rounded-lg shadow-md px-6 py-4 space-y-3 relative"
>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="space-y-1">
{/* Top Row: Role Input + Settings */}
<div className="flex justify-between items-center">
{/* Role Name Input */}
{/* Role Name Input */}
<div className="w-64">
<input
type="text"
value={recipient.roleName}
placeholder="Role name"
className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none transition
${errors.roleName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => {
setRecipients({ ...recipient, roleName: e.target.value });
setErrors(prev => ({ ...prev, roleName: false }));
}}
/>
{errors.roleName && (
<p className="text-red-500 text-xs mt-1">Role is required.</p>
)}
</div>
{/* Settings + Permissions */}
<div className="flex items-center">
<div className="flex justify-between">
{/* Permission Status Text - Always Visible when settings open */}
<div
className={`transition-all duration-600 ease-in-out ${showAdvanceSettings ? 'max-h-10 opacity-100 me-3' : 'max-h-0 opacity-0 '
} overflow-hidden`}
>
<div className="text-sm text-gray-600">
{recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Full permissions granted"}
{recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "Edit permission granted"}
{!recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Delete permission granted"}
{!recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "No permissions granted"}
{/* Settings + Permissions */}
<div className="flex items-center">
<div className="flex justify-between w-full">
{/* Permission Status Text - Always Visible */}
<div className="max-h-10 opacity-100 me-3 overflow-hidden">
<div className="text-sm text-gray-600 pt-0.5">
{/* {recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Full permissions granted"}
{recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "Edit permission granted"}
{!recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Delete permission granted"}
{!recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "No permissions granted"} */}
Permissions granted :
</div>
</div>
</div>
{/* Animated Permission Controls */}
<div className={`overflow-visible transition-all duration-900 ease-in-out ${showAdvanceSettings ? 'max-w-80 opacity-100' : 'max-w-0 opacity-0'
}`}>
<div className="flex items-center space-x-3 pr-1.5">
{/* Edit Permission */}
<div className="relative group">
<label
className={`relative flex items-center justify-center w-6 h-6 border-2 rounded-lg cursor-pointer transition-all duration-300 transform hover:scale-105 ${recipient.allowSenderToEdit
? 'bg-blue-100 border-blue-500 shadow-lg'
: 'bg-white border-gray-300 hover:border-blue-400'
}`}
>
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={(e) =>
setRecipients({ ...recipient, allowSenderToEdit: e.target.checked })
}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
className="absolute w-full h-full opacity-0 cursor-pointer"
/>
<FiEdit
className={`w-3 h-3 pointer-events-none transition-all duration-300 ${recipient.allowSenderToEdit ? 'text-blue-600' : 'text-gray-400'
{/* Permission Controls - Always Visible */}
<div className="max-w-80 opacity-100 overflow-visible">
<div className="flex items-center space-x-3 pr-1.5">
{/* Edit Permission */}
<div className="relative group">
<label
className={`relative flex items-center justify-center w-12 h-7 border-2 rounded-lg cursor-pointer transition-all duration-300 transform hover:scale-105 ${recipient.allowSenderToEdit
? 'bg-blue-100 border-blue-500 shadow-md'
: 'bg-white border-gray-300 hover:border-blue-400 text-gray-400'
}`}
/>
</label>
{/* Tooltip */}
<div className="absolute bottom-0 -translate-y-full left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
<div className="bg-gray-800 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
Allow Edit Permission
<div className="absolute bottom-[-5px] left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-800"></div>
>
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={(e) =>
setRecipients({ ...recipient, allowSenderToEdit: e.target.checked })
}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
className="absolute w-full h-full opacity-0 cursor-pointer"
/>
<FiEdit
className={`w-2.5 h-2.5 pointer-events-none transition-all duration-300 ${recipient.allowSenderToEdit ? 'text-blue-600' : 'text-gray-400'
}`}
/><span className="ms-0.5 text-[12px]">Edit</span>
</label>
{/* Tooltip */}
<div className="absolute bottom-0 -translate-y-full left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
<div className="bg-gray-800 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
Edit Permission
<div className="absolute bottom-[-5px] left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-800"></div>
</div>
</div>
</div>
</div>
{/* Delete Permission */}
<div className="relative group">
<label
className={`relative flex items-center justify-center w-6 h-6 border-2 rounded-lg cursor-pointer transition-all duration-300 transform hover:scale-105 ${recipient.allowSenderToDelete
? 'bg-red-100 border-red-500 shadow-lg'
: 'bg-white border-gray-300 hover:border-red-400'
}`}
>
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={(e) =>
setRecipients({ ...recipient, allowSenderToDelete: e.target.checked })
}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
className="absolute w-full h-full opacity-0 cursor-pointer"
/>
<FiTrash
className={`w-3 h-3 pointer-events-none transition-all duration-300 ${recipient.allowSenderToDelete ? 'text-red-600' : 'text-gray-400'
{/* Delete Permission */}
<div className="relative group">
<label
className={`relative flex items-center justify-center w-15 h-7 border-2 rounded-lg cursor-pointer transition-all duration-300 transform hover:scale-105 ${recipient.allowSenderToDelete
? 'bg-red-100 border-red-500 shadow-md'
: 'bg-white border-gray-300 hover:border-red-400 text-gray-400'
}`}
/>
</label>
{/* Tooltip */}
<div className="absolute bottom-0 -translate-y-full left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
<div className="bg-gray-800 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
Allow Delete Permission
<div className="absolute bottom-[-5px] left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-800"></div>
>
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={(e) =>
setRecipients({ ...recipient, allowSenderToDelete: e.target.checked })
}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
className="absolute w-full h-full opacity-0 cursor-pointer"
/>
<FiTrash
className={`w-3 h-3 pointer-events-none transition-all duration-300 ${recipient.allowSenderToDelete ? 'text-red-600' : 'text-gray-400'
}`}
/><span className="ms-0.5 text-[12px]">Delete</span>
</label>
{/* Tooltip */}
<div className="absolute bottom-0 -translate-y-full left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
<div className="bg-gray-800 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
Delete Permission
<div className="absolute bottom-[-5px] left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-800"></div>
</div>
</div>
</div>
</div>
@ -381,137 +459,142 @@ useEffect(() => {
</div>
</div>
{/* Settings Icon */}
<div className="relative group">
<p
className="text-blue-600 hover:text-blue-700 cursor-pointer transition-colors duration-200 p-2 rounded-full hover:bg-blue-50"
onClick={() => setShowAdvanceSettings((prev) => !prev)}
>
<IoIosSettings
className={`h-6 w-6 transition-transform duration-300 ${showAdvanceSettings ? 'rotate-90' : 'rotate-0'
}`}
/>
</p>
{/* Tooltip */}
<div className="absolute top-0 -translate-y-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
<div className="bg-gray-800 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
Advanced Settings
<div className="absolute bottom-[-6px] left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-800"></div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Name Inputs */}
<div className="grid grid-cols-12 gap-4 mt-3">
<div className="col-span-4">
<input
type="text"
onFocus={() => handleFocus("firstName")}
onBlur={handleContactModalBlur}
value={recipient.firstName}
placeholder="First Name"
className={` w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition
${errors.firstName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => {
setRecipients({ ...recipient, firstName: e.target.value });
// setErrors((prev) => ({ ...prev, firstName: false }));
}}
/>
</div>
{/* {errors.firstName && (
<p className="text-red-500 text-xs mt-1">First Name is required</p>
)} */}
<div className="col-span-4">
<input
type="text"
onFocus={() => handleFocus("lastName")}
onBlur={handleContactModalBlur}
value={recipient.lastName}
placeholder="Last Name"
className={` w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition
${errors.lastName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => {
setRecipients({ ...recipient, lastName: e.target.value });
setErrors((prev) => ({ ...prev, lastName: false }));
}}
/>
{errors.lastName && (
<p className="text-red-500 text-xs mt-1">Last Name is required</p>
)}
</div>
<div className="col-span-4">
<input
type="email"
onFocus={() => handleFocus("email")}
onBlur={handleContactModalBlur}
value={recipient.email}
placeholder="Recipient Email"
className={`w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition
${errors.email ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => {
setRecipients({ ...recipient, email: e.target.value });
setErrors((prev) => ({ ...prev, email: false }));
}}
/>
{errors.email && (
<p className="text-red-500 text-xs mt-1">Email is required</p>
)}
</div>
</div>
)}
{/* Name Inputs */}
<div className="grid grid-cols-12 gap-4 mt-3">
<input
type="text"
onFocus={() => handleFocus("firstName")}
onBlur={handleContactModalBlur}
value={recipient.firstName}
placeholder="First Name"
className="col-span-4 w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, firstName: e.target.value })}
/>
<input
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
type="text"
onFocus={() => handleFocus("lastName")}
onBlur={handleContactModalBlur}
value={recipient.lastName}
placeholder="Last Name"
className="col-span-4 w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, lastName: e.target.value })}
/>
<input
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
type="email"
onFocus={() => handleFocus("email")}
onBlur={handleContactModalBlur}
value={recipient.email}
placeholder="Recipient Email"
className="col-span-4 w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, email: e.target.value })}
/>
</div>
{recipient.type === recipientType.INPERSONSIGNER && <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
type="text"
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.hostName}
placeholder="Host Name"
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, hostName: e.target.value })}
/>
<input
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
type="email"
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.hostEmail}
placeholder="Host Email"
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, hostEmail: e.target.value })}
/>
</div>}
{/* Contact Modal */}
{openContactModal && contactData.contacts.length > 0 && (
<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
contactData={contactData}
inpFirstName={recipient.firstName}
inpLastName={recipient.lastName}
inpEmail={recipient.email}
openContactModal={openContactModal}
setRecipients={setRecipients}
setOpenContactModal={setOpenContactModal}
topRef={topRef}
setManualFocusTriggered={setManualFocusTriggered}
{recipient.type === recipientType.INPERSONSIGNER && <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
type="text"
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.hostName}
placeholder="Host Name"
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, hostName: e.target.value })}
/>
<input
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
type="email"
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.hostEmail}
placeholder="Host Email"
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
onChange={(e) => setRecipients({ ...recipient, hostEmail: e.target.value })}
/>
</div>}
{/* Contact Modal */}
{openContactModal && contactData.contacts.length > 0 && (
<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
contactData={contactData}
inpFirstName={recipient.firstName}
inpLastName={recipient.lastName}
inpEmail={recipient.email}
openContactModal={openContactModal}
setRecipients={setRecipients}
setOpenContactModal={setOpenContactModal}
topRef={topRef}
setManualFocusTriggered={setManualFocusTriggered}
/>
</div>
)}
{/* 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-wrap gap-6">
{[
{ label: "Signer", value: recipientType.SIGNER },
{ label: "In Person Signer", value: recipientType.INPERSONSIGNER },
{ label: "CC", value: recipientType.CC },
].map((option) => (
<label key={option.value} className="flex items-center gap-2 text-gray-700 text-sm cursor-pointer">
<input
type="radio"
name="recipient-type"
value={option.value}
defaultChecked={option.value === recipientType.SIGNER}
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
className="accent-[var(--primary)] cursor-pointer"
/>
{option.label}
</label>
))}
</div>
<button
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"
>
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
</button>
</div>
)}
{/* 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-wrap gap-6">
{[
{ label: "Signer", value: recipientType.SIGNER },
{ label: "In Person Signer", value: recipientType.INPERSONSIGNER },
{ label: "CC", value: recipientType.CC },
].map((option) => (
<label key={option.value} className="flex items-center gap-2 text-gray-700 text-sm cursor-pointer">
<input
type="radio"
name="recipient-type"
value={option.value}
defaultChecked={option.value === recipientType.SIGNER}
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
className="accent-[var(--primary)] cursor-pointer"
/>
{option.label}
</label>
))}
</div>
<button
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"
>
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
</button>
</div>
</form>
</div>
</form>
</div>
);
};

View File

@ -3,7 +3,7 @@ import {
removeRecipientAsync,
removeSelectedRecipient,
// setSigningOrder,
updateRecipientOrder, // 👈 You need to define this in your slice
updateRecipientOrder,
} from "../../../store/slices/selectedRecipientsSlice";
import { AppDispatch, RootState } from "../../../store/store";
@ -14,6 +14,7 @@ import SigningOrderDiagram from "../../../SigningOrderDiagram ";
import { IoCloseOutline } from "react-icons/io5";
import { useParams } from "react-router-dom";
import { FiEdit } from "react-icons/fi";
import { RiPencilFill } from "react-icons/ri";
const getInitials = (name: string) => {
if (!name) return "";
@ -30,7 +31,7 @@ type CardsData = {
transactionTemplateToggle: string
};
const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateToggle }: CardsData) => {
const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateToggle , editRecipient ,editCase }: CardsData) => {
const templateId = useParams<{ templateId: string }>().templateId;
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
@ -59,7 +60,6 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg
const handleRemoveRecipient = (recipient) => {
if (!accountId || !transactionId) return;
// <<<<<<< HEAD
// dispatch(removeSelectedRecipient(recipient))
// dispatch(removeRecipientAsync([{
// recipientId: recipient.id,
@ -67,7 +67,7 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg
// accountId: accountId,
// transactionId: transactionId,
// }]))
// =======
// =======
if (recipient.uuid) {
dispatch(removeSelectedRecipient(recipient))
} else {
@ -78,7 +78,6 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg
transactionId: transactionId,
}))
}
// >>>>>>> origin/main
if (recipients.length === 2) {
let leftRecipient = recipients.filter((obj) => {
if ("id" in obj && "id" in recipient) {
@ -135,24 +134,28 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg
>
<div className="flex items-center gap-3 min-w-0">
{/* {signingOrder && ( */}
{recipients.length > 1 ? <input
type="number"
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
/> : <input
type="number"
disabled
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
/>}
{recipients.length > 1 ? <div className="relative">
<input
type="number"
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400 bg-slate-100"
/>
{/* <RiPencilFill className="absolute top-1.5 right-0 text-[14px] text-gray-500" /> */}
</div>
: <input
type="number"
disabled
value={recipient.routingOrder || 1}
min={1}
onChange={(e) =>
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
}
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
/>}
{/* )} */}
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
@ -220,11 +223,10 @@ const RecipientCards = ({ setEditRecipient, setEditCase, transactionTemplateTogg
</div>
)}
{signingOrderModal.open &&
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center">
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full overflow-auto">
<div className="pb-2 text-white flex justify-between items-center">
<div className="p-2 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
<p className="font-semibold">Signing Order</p>
<div
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}

View File

@ -0,0 +1,44 @@
import { useNavigate } from "react-router-dom";
type PropsType = {
setShowSelfSignWarnModal: (val: boolean) => void;
onConfirm: () => void;
};
const SelfSignWarnModal = ({ setShowSelfSignWarnModal }: PropsType) => {
const navigate = useNavigate()
return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-100">
<h3 className="text-lg font-medium text-gray-800 mb-4">Confirm Self Sign</h3>
<p className="text-gray-600 mb-6">
Are you sure you want to proceed with self sign? This will remove all the recipient that you added.
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowSelfSignWarnModal(false);
navigate("./../");
}}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
No
</button>
<button
onClick={() => {
// onConfirm();
setShowSelfSignWarnModal(false);
}}
className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors"
>
Yes
</button>
</div>
</div>
</div>
);
};
export default SelfSignWarnModal;

View File

@ -1,230 +1,232 @@
// ContactForm.tsx
import React, { useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
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[];
contactId?: number;
email: string;
firstName: string;
lastName: string;
title: string;
company: string;
website: string;
phone: string;
}
interface Props {
onSubmit: (data: ContactData) => void;
loading: boolean;
onSubmit: (data: ContactData) => void;
loading: boolean;
}
const ContactForm = ({ onSubmit, loading }: Props) => {
const [formData, setFormData] = useState<ContactData>({
email: '',
firstName: '',
lastName: '',
title: '',
company: '',
website: '',
phones: [''],
});
const [formData, setFormData] = useState<ContactData>({
email: "",
firstName: "",
lastName: "",
title: "",
company: "",
website: "",
phone: "",
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
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 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 handlePhoneChange = (index: number, value: string) => {
// const numericValue = value.replace(/\D/g, ""); // Remove non-numeric
// const updatedPhone = [...formData.phone];
// updatedPhone[index] = numericValue;
// setFormData((prev) => ({ ...prev, phone: updatedPhone }));
const errorKey = `phones_${index}`;
if (errors[errorKey]) {
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
}
};
// const errorKey = `phone_${index}`;
// if (errors[errorKey]) {
// setErrors((prev) => ({ ...prev, [errorKey]: "" }));
// }
// };
// const addPhoneField = () => {
// setFormData((prev) => ({ ...prev, phone: [...prev.phone, ""] }));
// };
const addPhoneField = () => {
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
};
// const removePhoneField = (index: number) => {
// const updatedPhone = formData.phone.filter((_, i) => i !== index);
// setFormData((prev) => ({ ...prev, phone: updatedPhone }));
// };
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 validate = () => {
const newErrors: { [key: string]: string } = {};
// const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex =
/^\+?\d{1,4}?[-.\s]?\(?\d{2,5}\)?[-.\s]?\d{3,5}[-.\s]?\d{3,5}$/;
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";
}
// 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';
// }
// // 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";
}
// 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';
// }
// // 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";
}
// Phone
if (formData.phone.trim() && !phoneRegex.test(formData.phone)) {
newErrors.phone = "Phone number must contain only digits";
}
// Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = 'Invalid website URL';
}
return newErrors;
};
// 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';
// }
// });
const handleSubmit = async () => {
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
return newErrors;
};
try {
await onSubmit(formData);
setFormData({
email: "",
firstName: "",
lastName: "",
title: "",
company: "",
website: "",
phone: "",
});
setErrors({});
} catch (err) {
console.error("Submit failed:", err);
}
};
const handleSubmit = async () => {
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
return (
<div className="ms-2 space-y-2 max-w-[60vw]">
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
try {
await onSubmit(formData);
<div className="grid grid-cols-2 gap-3">
{[
{ label: "First Name", field: "firstName" },
{ label: "Last Name", field: "lastName" },
{ label: "Email", field: "email", fieldType: "email" },
{ label: "Title", field: "title" },
{ label: "Company", field: "company" },
{ label: "Website", field: "website", fieldType: "url" },
{ label: "Phone", field: "phone", fieldType: "tel" },
].map(({ label, field, fieldType }) => (
<div key={field} className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
{label}
</label>
<input
type={fieldType ? fieldType : "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>
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
{/* <div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Phone Numbers
</label>
<div className="space-y-2">
{formData.phone.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.phone.length > 1 && (
<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>
onClick={() => removePhoneField(index)}
className="text-xs text-red-500 hover:underline"
>
Remove
</button>
)}
</div>
{errors[`phone_${index}`] && (
<span className="text-sm text-red-500">
{errors[`phone_${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;

View File

@ -78,7 +78,7 @@ const Templates = () => {
let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload)
console.log("handleUseClick", res.data)
// /process-step-one/:accountId/:transactionId
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}`);
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) {
console.log(error)
}

View File

@ -100,6 +100,18 @@ canvas {
}
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
/* Group Border - START */
.group-border {
position: absolute;

View File

@ -437,6 +437,8 @@ export const removeRecipientAsync = createAsyncThunk<
const basePath = `/api/accounts/${accountId}/${toggle}/${transactionId}/recipients`;
console.log(`Line 440 : ${basePath}/${recipientId}`)
// return
if (recipientId) {
await api.delete(`${basePath}/${recipientId}`);
return recipientId;
@ -533,6 +535,9 @@ const selectedRecipientsSlice = createSlice({
removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
state.recipients = state.recipients.filter((recipient) => recipient.uuid !== action.payload.uuid);
},
removeAllRecipients: (state) => {
state.recipients = [];
},
addColorsToRecipient: (state) => {
for (const [index, recipient] of state.recipients.entries()) {
recipient.color = colors[index];
@ -639,6 +644,7 @@ export const {
setSelectedRecipients,
toggleToTemplates,
removeSelectedRecipient,
removeAllRecipients,
setSigningOrder,
addColorsToRecipient,
updateRecipientIds,

View File

@ -49,7 +49,6 @@ const JoyrideController = () => {
),
placement: "left",
disableBeacon: true,
},
{
target: "#start-now-upload-con",
@ -63,7 +62,6 @@ const JoyrideController = () => {
),
placement: "top",
disableBeacon: true,
},
];
} else if (location.pathname.includes("/process-step-one")) {