diff --git a/.env b/.env index 639667a..249b3e4 100644 --- a/.env +++ b/.env @@ -4,7 +4,6 @@ # VITE_BASE_URI="https://python-amused-adder.ngrok-free.app" # VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app" # VITE_BASE_URI="https://smashing-albacore-engaging.ngrok-free.app" -VITE_BASE_URI="https://basically-polished-yeti.ngrok-free.app" - - -# VITE_BASE_URI="http://10.10.1.248:8083" \ No newline at end of file +# VITE_BASE_URI="https://basically-polished-yeti.ngrok-free.app" +VITE_BASE_URI="https://sacha-countryfied-evolutionistically.ngrok-free.dev" +VITE_SSE_URI="http://10.10.1.75:8083" \ No newline at end of file diff --git a/public/Sign made easy logo.gif b/public/Sign made easy logo.gif new file mode 100644 index 0000000..5c91c58 Binary files /dev/null and b/public/Sign made easy logo.gif differ diff --git a/src/App.tsx b/src/App.tsx index 5a4eb38..8c731dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,7 +46,8 @@ const App = () => { {/* }> */} } /> } /> - } /> + } /> + } /> {/* } /> */} } /> @@ -111,6 +112,10 @@ const App = () => { } /> } /> + } /> + } /> + } /> + {/* Links work wothout Login */} diff --git a/src/components/CommonNav.tsx b/src/components/CommonNav.tsx index 5a330a6..9662279 100644 --- a/src/components/CommonNav.tsx +++ b/src/components/CommonNav.tsx @@ -248,16 +248,16 @@ const CommonNav: React.FC = ({ }, [open]); const handleLogout = async () => { - try { - let res = await api.post('/api/v1/auth/logout'); - if (res.status === 200) { - sessionStorage.clear(); - setOpen(false); - navigate("/login"); - } - } catch (error) { - console.log(error); - } + api.get(`${import.meta.env.VITE_BASE_URI}/api/v1/auth/logout`).then((res) => { + sessionStorage.clear(); + localStorage.clear(); + // Redirect to login or refresh the page after logout + // window.location.href = '/login'; + }).catch((err) => { + sessionStorage.clear(); + localStorage.clear(); + window.location.href = '/login'; + }); }; const handleMainDropdownToggle = () => { @@ -365,10 +365,10 @@ const CommonNav: React.FC = ({
- {/* SETINGS - START */} -
+ {/* SETINGS - START */} +
diff --git a/src/components/accounts/Account.tsx b/src/components/accounts/Account.tsx index e7406c8..333deec 100644 --- a/src/components/accounts/Account.tsx +++ b/src/components/accounts/Account.tsx @@ -33,6 +33,7 @@ const Account = () => { textColor: "text-sky-600", gradient: "bg-white", shadowColor: "shadow-sky-100", + navigateTo: "/transactions/action_required" }, { title: "Waiting for Response", @@ -42,6 +43,7 @@ const Account = () => { textColor: "text-yellow-600", gradient: "bg-white", shadowColor: "shadow-yellow-100", + navigateTo: "/transactions/waiting_for_response" }, { title: "Expiring Soon", @@ -51,6 +53,7 @@ const Account = () => { textColor: "text-red-600", gradient: "bg-white", shadowColor: "shadow-red-100", + navigateTo: "/transactions/expiring_soon" }, { title: "Completed", @@ -60,6 +63,7 @@ const Account = () => { textColor: "text-green-600", gradient: "bg-white", shadowColor: "shadow-green-100", + navigateTo: "/transactions/completed" }, ]; }; diff --git a/src/components/accounts/AccountNav.tsx b/src/components/accounts/AccountNav.tsx index 6cb2c6a..acf5f85 100644 --- a/src/components/accounts/AccountNav.tsx +++ b/src/components/accounts/AccountNav.tsx @@ -147,7 +147,7 @@ const AccountNav: React.FC = ({ accountName = null, transaction if (item) { const userData = { id: item?.id, - name : item?.firstName + " " + item?.lastName , + name: item?.firstName + " " + item?.lastName, email: item?.email, language: "", account: { @@ -157,10 +157,10 @@ const AccountNav: React.FC = ({ accountName = null, transaction }, }; - setUser(userData); + setUser(userData); sessionStorage.setItem('userDetails', JSON.stringify(userData)); sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id)); - + // Dispatch event for initial load dispatchSessionStorageChange('activeAccountId', userData.account.id); } @@ -213,16 +213,16 @@ const AccountNav: React.FC = ({ accountName = null, transaction }, [open]); const handleLogout = async () => { - try { - let res = await api.post('/api/v1/auth/logout'); - if (res.status === 200) { - sessionStorage.clear(); - setOpen(false); - navigate("/login"); - } - } catch (error) { - console.log(error); - } + api.get(`${import.meta.env.VITE_BASE_URI}/api/v1/auth/logout`).then((res) => { + sessionStorage.clear(); + localStorage.clear(); + // Redirect to login or refresh the page after logout + // window.location.href = '/login'; + }).catch((err) => { + sessionStorage.clear(); + localStorage.clear(); + window.location.href = '/login'; + }); }; const handleMainDropdownToggle = () => { diff --git a/src/components/accounts/TransactionDataTable.tsx b/src/components/accounts/TransactionDataTable.tsx index e04a667..893b2fa 100644 --- a/src/components/accounts/TransactionDataTable.tsx +++ b/src/components/accounts/TransactionDataTable.tsx @@ -1,24 +1,17 @@ -import React, { useEffect, useState } from 'react' -import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; -import api from '../../config/api'; -import { Transaction } from '../transactions/Transactions'; -import { useLocation, useNavigate } from 'react-router-dom'; -import LoaderUi from '../../helper/LoaderUi'; -import logoRecord from '../../assets/record.svg'; -import { MdOutlineFileDownload } from 'react-icons/md'; -import downloadDocumentUtils from '../../utlity/downloadDocumentsUtils'; -import { IoMdClose } from 'react-icons/io'; -import DownloadDocument from './DownloadDocument'; +import React, { useEffect, useState } from "react"; +import { MdOutlineFileDownload } from "react-icons/md"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import LoaderUi from "../../helper/LoaderUi"; +import logoRecord from "../../assets/record.svg"; +import api from "../../config/api"; +import DownloadDocument from "./DownloadDocument"; -const formatDate = (timestamp: number) => { - return new Date(timestamp * 1000).toLocaleString(); -}; - -const statusColorMap = { +const statusColorMap: Record = { CREATED: "bg-blue-100 text-blue-600", - SENT: "bg-green-100 text-green-600", - COMPLETED: "bg-purple-100 text-purple-600", - EXPIRED: "bg-red-100 text-red-600", + SENT: "bg-yellow-100 text-yellow-600", + COMPLETED: "bg-green-100 text-green-600", + DECLINED: "bg-red-100 text-red-600", + EXPIRED: "bg-gray-200 text-gray-600", }; export enum DownloadTypesEnum { @@ -27,281 +20,213 @@ export enum DownloadTypesEnum { CERTIFICATE = "certificate" } +const formatDate = (isoString: string) => { + const date = new Date(isoString); + return isNaN(date.getTime()) ? "-" : date.toLocaleString(); +}; const TransactionDataTable = ({ activeAccountId }) => { - const navigate = useNavigate() - const loaction = useLocation() + const { filter } = useParams<{ filter: string }>(); + const navigate = useNavigate(); + const location = useLocation(); - const [currentEmailId, setCurrentEmailId] = useState('null'); - const [selectedTransactions, setSelectedTransactions] = useState([]); + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(false); - const [showMenu, setShowMenu] = useState<{ open: boolean, transactionId: string | null }>({ + // Pagination + const [page, setPage] = useState(0); // 0-based + const [size, setSize] = useState(10); + const [totalPages, setTotalPages] = useState(0); + + const [showMenu, setShowMenu] = useState<{ open: boolean; transactionId: string | null }>({ open: false, - transactionId: null + transactionId: null, }); - const [loading, setLoading] = useState(false) - - - - const [downloadDocumentType, setDownloadDocumentType] = useState({ - type: DownloadTypesEnum.ALL, - includeCertificate: false - }) const fetchTransactions = async () => { - console.log("fetchTransactions") - setLoading(true) + if (!activeAccountId) return; + setLoading(true); + try { - const response = await api.get(`/api/v1/accounts/${activeAccountId}/transactions?sortBy=lastModifiedAt desc`); - setTransactions(response.data || []); + let addParams = `?sortBy=createdAt desc&page=${page}&size=${Math.min(size, 30)}` + switch(filter) { + case "action_required": + addParams = addParams+"&filter=inbox" + break; + case "waiting_for_response": + addParams = addParams+"&status=sent" + break; + case "expiring_soon": + addParams = addParams+"&filter=inbox" + break; + case "completed": + addParams = addParams+"&status=completed&filter=completed" + break; + default: + break; + } + const response = await api.get( + `/api/v1/accounts/${activeAccountId}/transactions`+addParams + ); + + // ✅ Use correct response structure + setTransactions(response.data?.content || []); + setTotalPages(response.data?.page?.totalPages || 0); } catch (error) { console.error("Error fetching transactions:", error); } finally { - setLoading(false) + setLoading(false); } }; - const [transactions, setTransactions] = useState([]); - - const handleTransactionClick = (id: number) => { - navigate(`/accounts/${activeAccountId}/transactions/${id}`); - }; - useEffect(() => { - if (activeAccountId) { - fetchTransactions(); - } - }, [activeAccountId]); - - useEffect(() => { - const userDetails = JSON.parse(sessionStorage.getItem('userDetails')); - setCurrentEmailId(userDetails?.email) - }, []); - - - function formatTimestamp(isoString, options = { timeZone: 'local' }) { - // Parse the ISO string to a Date object - const date = new Date(isoString); - - if (isNaN(date)) { - return 'Invalid date'; - } - - // Return formatted date string - if (options.timeZone === 'UTC') { - return date.toUTCString(); // e.g. "Mon, 04 Aug 2025 13:46:46 GMT" - } else { - return date.toLocaleString(); // e.g. "8/4/2025, 9:46:46 AM" (based on local time zone) - } - } - - function getStatusColorClass(status) { - return statusColorMap[status] || "bg-gray-100 text-gray-600"; - } - - function handleDownloadFormSubmit(e) { - e.preventDefault(); - - const formData = new FormData(e.target); - const docType = formData.get('download-doc-type'); - const includeCertificate = formData.get('download-doc-type-include-cert'); - - downloadDocumentUtils(activeAccountId, showMenu.transactionId as string, docType as string, includeCertificate as string | null) - } + fetchTransactions(); + }, [activeAccountId, page, size, filter]); return ( - <> - -
- {/* Header */} - {location.pathname !== '/transactions' && -
-

Recent Transactions

- View all -
} - - {/* Scrollable Table Container */} -
- - {/* Sticky Header */} - - - - - - - - - - - {loading ? ( - - - - - - ) : transactions.length > 0 ? ( - <> - {/* Scrollable Body */} - - {transactions.map((tx, idx) => ( - handleTransactionClick(tx.id)} - > - - - - - - - - - - - - - - - ))} - - ) : ( - - - - - - )} -
TransactionStatusSigned ByLast ChangeExpire After (Days)Actions
- -
-
handleTransactionClick(tx.id)} - > - {tx?.subject || "Untitled"} -
-
- - {tx.status} - - -
- {tx.signedBy ? ( -
-
- {tx.signedByInitials} -
- - {tx.signedBy} - -
- ) : ( - <> - - - - )} -
-
- - {formatTimestamp(tx.lastModifiedAt)} - - - - {tx.expireAfter ?? "-"} - - - -
-
{ - // if (!e.currentTarget.contains(e.relatedTarget)) { - // setShowMenu(null); - // } - // }} - - onClick={() => - setShowMenu({ - open: true, - transactionId: tx.id - }) - } - > - -

- Download -

- - - {/* {showMenu === idx && ( -
- - -
- )} */} - -
- {/* */} -
- -
-
- Record Not Found -
-
+
+ {/* Table Header */} + {location.pathname !== "/transactions" && ( +
+

Recent Transactions

+ navigate("/transactions")} + className="text-[var(--primary)] text-sm hover:underline cursor-pointer font-medium" + > + View all +
- -
- - - - - - - {showMenu.open && ( - )} + {/* Table */} +
+ + + + + + + + + + + + {loading ? ( + + + + + + ) : transactions.length > 0 ? ( + + {transactions.map((tx, idx) => ( + + + + + + + + + ))} + + ) : ( + + + + + + )} +
TransactionStatusCreated ByLast ModifiedExpire (Days)Actions
+ +
navigate(`/accounts/${activeAccountId}/transactions/${tx.id}`)} + className="px-6 py-3 text-gray-800 font-medium cursor-pointer truncate max-w-xs" + > + {tx.subject || "Untitled"} + + + {tx.status} + + + {tx.createdBy?.name || "-"} + {formatDate(tx.lastModifiedAt)}{tx.expireAfter ?? "-"} + +
+
+ No Records +

No transactions found

+
+
+
+ {/* Pagination */} + {transactions.length > 0 && ( +
+
+ Page {page + 1} of {totalPages} +
+
+ + + +
+
+ )} + {/* Download Modal */} + {showMenu.open && ( + + )} +
+ ); +}; - - - ) -} - -export default TransactionDataTable \ No newline at end of file +export default TransactionDataTable; diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index beeb675..ff0c635 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -6,7 +6,7 @@ import axios from "axios"; import { BsEye } from "react-icons/bs"; import { FiEyeOff } from "react-icons/fi"; -// import api from "../../config/api"; +import { TokenStorage } from "../../config/api"; interface FormDataState { email: string; @@ -16,11 +16,9 @@ interface FormDataState { lastName: string; } - const RegisterForm = ({ onSwitchToLogin }) => { - const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false }); - const [formData, setForm] = useState({ + const [formData, setForm] = useState({ email: "", password: "", confirmPassword: "", @@ -37,136 +35,79 @@ const RegisterForm = ({ onSwitchToLogin }) => { confirmPassword: "" }); const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate() + const navigate = useNavigate(); - const validateFirstName = (name) => { + const validateFirstName = (name: string) => { const trimmedName = name.trim(); - - if (!trimmedName) { - return "First name is required"; - } - - if (trimmedName.length < 2) { - return "First name must be at least 2 characters long"; - } - - if (trimmedName.length > 50) { - return "First name is too long"; - } - + if (!trimmedName) return "First name is required"; + if (trimmedName.length < 2) return "First name must be at least 2 characters"; + if (trimmedName.length > 50) return "First name is too long"; const nameRegex = /^[a-zA-Z\s'-]+$/; - if (!nameRegex.test(trimmedName)) { - return "First name can only contain letters, spaces, hyphens, and apostrophes"; - } - + if (!nameRegex.test(trimmedName)) return "First name can only contain letters, spaces, hyphens, and apostrophes"; return ""; }; - const validateLastName = (name) => { + const validateLastName = (name: string) => { const trimmedName = name.trim(); - - if (!trimmedName) { - return "Last name is required"; - } - - if (trimmedName.length < 2) { - return "Last name must be at least 2 characters long"; - } - - if (trimmedName.length > 50) { - return "Last name is too long"; - } - + if (!trimmedName) return "Last name is required"; + if (trimmedName.length < 2) return "Last name must be at least 2 characters"; + if (trimmedName.length > 50) return "Last name is too long"; const nameRegex = /^[a-zA-Z\s'-]+$/; - if (!nameRegex.test(trimmedName)) { - return "Last name can only contain letters, spaces, hyphens, and apostrophes"; - } - + if (!nameRegex.test(trimmedName)) return "Last name can only contain letters, spaces, hyphens, and apostrophes"; return ""; }; - const validateEmail = (email) => { + const validateEmail = (email: string) => { const trimmedEmail = email.trim(); + if (!trimmedEmail) return "Email is required"; + if (trimmedEmail.length > 120) return "Email is too long"; - if (!trimmedEmail) { - return "Email address is required"; - } + // Ensure email starts with a letter or number + const emailRegex = /^[a-zA-Z0-9][a-zA-Z0-9._-]*@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - if (trimmedEmail.length > 254) { - return "Email address is too long"; - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(trimmedEmail)) { - return "Please enter a valid email address"; + return "Invalid email address."; } - return ""; }; - const validatePassword = (password) => { - if (!password) { - return "Password is required"; - } - if (password.length < 8) { - return "Password must be at least 8 characters long"; - } + const validatePassword = (password: string) => { + if (!password) return "Password is required"; + if (password.length < 8) return "Password must be at least 8 characters long"; + if (password.length > 60) return "Password is too long"; + if (/\s/.test(password)) return "Password cannot contain spaces"; - if (password.length > 128) { - return "Password is too long"; - } const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); - const hasNumbers = /\d/.test(password); - // const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); + const hasNumber = /\d/.test(password); + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); - // if (!hasUpperCase) { - // return "Password must contain at least one uppercase letter"; - // } - - // if (!hasLowerCase) { - // return "Password must contain at least one lowercase letter"; - // } - - // if (!hasNumbers) { - // return "Password must contain at least one number"; - // } - - // if (!hasSpecialChar) { - // return "Password must contain at least one special character"; - // } + if (!hasUpperCase) return "Password must contain at least one uppercase letter"; + if (!hasLowerCase) return "Password must contain at least one lowercase letter"; + if (!hasNumber) return "Password must contain at least one number"; + if (!hasSpecialChar) return "Password must contain at least one special character"; return ""; }; - const validateConfirmPassword = (confirmPassword, password) => { - if (!confirmPassword) { - return "Please confirm your password"; - } - - if (confirmPassword !== password) { - return "Passwords do not match"; - } - + const validateConfirmPassword = (confirmPassword: string, password: string) => { + if (!confirmPassword) return "Please confirm your password"; + if (confirmPassword !== password) return "Passwords do not match"; return ""; }; - const handleInputChange = (field, value) => { + const handleInputChange = (field: keyof FormDataState, value: string) => { setForm({ ...formData, [field]: value }); // Clear error when user starts typing - if (errors[field]) { - setErrors({ ...errors, [field]: "" }); - } + if (errors[field]) setErrors({ ...errors, [field]: "" }); - // If confirming password, also check if it matches + // If password changes, check confirm password if (field === "password" && formData.confirmPassword && errors.confirmPassword) { const confirmError = validateConfirmPassword(formData.confirmPassword, value); - if (!confirmError) { - setErrors({ ...errors, [field]: "", confirmPassword: "" }); - } + if (!confirmError) setErrors({ ...errors, password: "", confirmPassword: "" }); } }; @@ -177,68 +118,40 @@ const RegisterForm = ({ onSwitchToLogin }) => { const passwordError = validatePassword(formData.password); const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password); - const newErrors = { - firstName: firstNameError, - lastName: lastNameError, - email: emailError, - password: passwordError, - confirmPassword: confirmPasswordError, - }; - + const newErrors = { firstName: firstNameError, lastName: lastNameError, email: emailError, password: passwordError, confirmPassword: confirmPasswordError }; setErrors(newErrors); - // If any errors exist, stop - if (Object.values(newErrors).some((error) => error !== "")) { - return; - } + if (Object.values(newErrors).some(e => e !== "")) return; setIsLoading(true); - const trimmedData = { - firstName: formData.firstName.trim(), - lastName: formData.lastName.trim(), - email: formData.email.trim(), - password: formData.password, - confirmPassword: formData.confirmPassword, - }; - try { const res = await axios.post( `${import.meta.env.VITE_BASE_URI}/api/v1/auth/signup`, { - firstName: trimmedData.firstName, - lastName: trimmedData.lastName, - email: trimmedData.email, - password: trimmedData.password, + firstName: formData.firstName.trim(), + lastName: formData.lastName.trim(), + email: formData.email.trim(), + password: formData.password, }, - { - withCredentials: true, - headers: { - "ngrok-skip-browser-warning": "69420", - "Access-Control-Allow-Origin": "*", - }, - } + { withCredentials: true } ); if (res.status === 201) { + TokenStorage.setTokens(res.data.accessToken, res.data.refreshToken); navigate("/"); } } catch (err: any) { if (err.response?.status === 409) { - formErrorRef.current = { message: 'User Already Exists. Please try logging.', error: true } + formErrorRef.current = { message: 'Email already exists. Try logging in.', error: true }; } else { - formErrorRef.current = { message: 'Something went wrong. Please refresh and try again.', error: true } + formErrorRef.current = { message: 'Something went wrong. Please refresh and try again.', error: true }; } } finally { setIsLoading(false); } }; - const handleGoogleLogin = () => { - // Your Google OAuth logic here - console.log("Google signup clicked"); - }; - return (
@@ -257,9 +170,7 @@ const RegisterForm = ({ onSwitchToLogin }) => {
- +
{ value={formData.firstName} onChange={(e) => handleInputChange("firstName", e.target.value)} placeholder="First name" - className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName - ? "border-red-500 focus:ring-red-500" - : "border-gray-300 focus:ring-blue-500" - }`} + className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName ? "border-red-500 focus:ring-red-500" : "border-gray-300 focus:ring-blue-500"}`} disabled={isLoading} /> - {errors.firstName && ( -

{errors.firstName}

- )} + {errors.firstName &&

{errors.firstName}

}
{ value={formData.lastName} onChange={(e) => handleInputChange("lastName", e.target.value)} placeholder="Last name" - className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName - ? "border-red-500 focus:ring-red-500" - : "border-gray-300 focus:ring-blue-500" - }`} + className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName ? "border-red-500 focus:ring-red-500" : "border-gray-300 focus:ring-blue-500"}`} disabled={isLoading} /> - {errors.lastName && ( -

{errors.lastName}

- )} + {errors.lastName &&

{errors.lastName}

}
- + handleInputChange("email", e.target.value)} placeholder="Enter your email" - className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email - ? "border-red-500 focus:ring-red-500" - : "border-gray-300 focus:ring-blue-500" - }`} + className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email ? "border-red-500 focus:ring-red-500" : "border-gray-300 focus:ring-blue-500"}`} disabled={isLoading} /> - {errors.email && ( -

{errors.email}

- )} + {errors.email &&

{errors.email}

}
- +
handleInputChange("password", e.target.value)} placeholder="Enter your password" - className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password - ? "border-red-500 focus:ring-red-500" - : "border-gray-300 focus:ring-blue-500" - }`} + className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password ? "border-red-500 focus:ring-red-500" : "border-gray-300 focus:ring-blue-500"}`} disabled={isLoading} />
- {errors.password && ( -

{errors.password}

- )} + {errors.password &&

{errors.password}

}
- +
handleInputChange("confirmPassword", e.target.value)} placeholder="Confirm your password" - className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.confirmPassword - ? "border-red-500 focus:ring-red-500" - : "border-gray-300 focus:ring-blue-500" - }`} + className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.confirmPassword ? "border-red-500 focus:ring-red-500" : "border-gray-300 focus:ring-blue-500"}`} disabled={isLoading} />
- {errors.confirmPassword && ( -

{errors.confirmPassword}

- )} + {errors.confirmPassword &&

{errors.confirmPassword}

}
-

{formErrorRef.current.message}

+

{formErrorRef.current.message}

@@ -403,4 +280,4 @@ const RegisterForm = ({ onSwitchToLogin }) => { ); }; -export default RegisterForm +export default RegisterForm; diff --git a/src/components/client/ClientAuditModal.tsx b/src/components/client/ClientAuditModal.tsx new file mode 100644 index 0000000..8a31ee0 --- /dev/null +++ b/src/components/client/ClientAuditModal.tsx @@ -0,0 +1,175 @@ +import axios from "axios"; +import React, { useEffect, useState } from "react"; +import { FiX, FiFileText, FiEye, FiSend, FiCheckCircle } from "react-icons/fi"; +import { MdAccessTime, MdCancel } from "react-icons/md"; + +interface ClientAuditModalProps { + open: boolean; + onClose: () => void; + recipientId?: string; +} + +const ClientAuditModal: React.FC = ({ open, onClose, recipientId }) => { + if (!open) return null; + + const [auditStatus, setAuditStatus] = useState([]); + const [details, setDetails] = useState(null); + + // const getAuditDetails = async () => { + // try { + // // fake API for demo + // const res = await ai("/api/v1/sign/" + recipientId + "/audit"); + // const data = await res.json(); + // setAuditStatus(data.events || []); + // setDetails(data); + // } catch (error) { + // console.error(error); + // } + // }; + + const getAuditDetails = async () => { + // setLoading(true) + try { + let res = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}/audit`, { + withCredentials: true, + headers: { + "ngrok-skip-browser-warning": "69420", + "Access-Control-Allow-Origin": "*", + }, + }) + // let res = await api.get(`/api/v1/accounts/${accountId}/transactions/${transactionId}/audit?lang=hi`) + if (res?.status === 200) { + // console.log(res?.data?.events) + // let auditResponse = res?.data?.events + setAuditStatus(res.data.events || []); + setDetails(res.data); + } + } catch (error) { + console.log(error) + } finally { + // setLoading(false) + } + } + + useEffect(() => { + if (open) getAuditDetails(); + }, [open]); + + const getActionIcon = (action: string) => { + const icons: Record = { + CREATED: , + SENT: , + INVITED: , + VIEWED: , + SIGNED: , + CANCELLED: , + EXPIRED: , + }; + return icons[action] || ; + }; + + return ( +
+
+ + {/* Header */} +
+

Transaction History

+ +
+ +
+ {/* Envelope/Transaction Details */} + {details && ( +
+

Transaction Details

+
+ +
+

+ Subject: + {details.subject} +

+

+ Transaction ID: + {details.transactionId} +

+

+ Created: + {new Date(details.createAt).toLocaleString()} +

+

+ Sent: + {new Date(details.sentAt).toLocaleString()} +

+
+ +
+ +
+

+ Sender: + {details.sender?.name} ({details.sender?.email}) +

+
+ +
+

Recipients:

+
    + {details.recipients?.map((r: any) => ( +
  • {r.name} ({r.email})
  • + ))} +
+
+ +
+

Documents:

+
    + {details.documents?.map((d: any) => ( +
  • {d.name}
  • + ))} +
+
+
+
+
+ + + )} + + {/* Activity History */} +
+

Activity History

+
+ {auditStatus.map((item, idx) => ( +
+ {getActionIcon(item.action.toUpperCase())} +
+

{item.message}

+

+ {new Date(item.timestamp).toLocaleString()} | {item.status} +

+
+
+ ))} +
+
+ +
+ + {/* Footer */} +
+ Showing {auditStatus.length} events + +
+
+
+ ); +}; + +export default ClientAuditModal; diff --git a/src/components/client/ClientDownloadBtn.tsx b/src/components/client/ClientDownloadBtn.tsx new file mode 100644 index 0000000..0c6f9cd --- /dev/null +++ b/src/components/client/ClientDownloadBtn.tsx @@ -0,0 +1,14 @@ +import { FaDownload } from 'react-icons/fa' + +const ClientDownloadBtn = () => { + return ( + <> + + + ) +} + +export default ClientDownloadBtn \ No newline at end of file diff --git a/src/components/client/SignButton.tsx b/src/components/client/SignButton.tsx index 6bd4473..79c00ec 100644 --- a/src/components/client/SignButton.tsx +++ b/src/components/client/SignButton.tsx @@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from "react"; import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle, FaAngleLeft, FaAngleRight } from "react-icons/fa"; import { BsThreeDotsVertical } from "react-icons/bs"; import ClientDownloadDocument from "./ClientDownloadDocument"; +import { AiOutlineAudit } from "react-icons/ai"; interface Props { transparentTags?: boolean; @@ -10,6 +11,9 @@ interface Props { onDownloadClick?: () => void; onDeclineClick?: () => void; onReassignClick?: () => void; + onAuditHistoryClick?: () => void; + showDecAndReassign?: boolean; + transactionId: string; } export default function SignButton({ @@ -19,6 +23,9 @@ export default function SignButton({ onDownloadClick, onDeclineClick, onReassignClick, + onAuditHistoryClick, + showDecAndReassign, + transactionId }: Props) { const [menuOpen, setMenuOpen] = useState(false); const menuRef = useRef(null); @@ -41,8 +48,8 @@ export default function SignButton({ return ( <> -
- {(!transparentTags && !hideTags) ? ( + {showDecAndReassign &&
+ {(!transparentTags && !hideTags) &&
{/* Sign Button */} {/* */} +
  • + +
  • )}
    - ) : ( -

    Signed Successfully

    - )} -
    - - - - - + } +
    } + {!showDecAndReassign && +
    + +
    + } {showMenuDownload.open && ( { const [requiredFilled, setRequiredFilled] = useState(false); + const [horizontalFormLoader, setHorizontalFormLoader] = useState<{ requiredFilled: number, totalRequired: number | null }>({ requiredFilled: 0, totalRequired: null }); const [stamp, setStamp] = useState({ open: false, blob: "" }); @@ -177,6 +177,7 @@ const SignDocument = () => { }); const [declineOpen, setDeclineOpen] = useState(false); const [reassignOpen, setReassignOpen] = useState(false); + const [auditHistoryOpen, setAuditHistoryOpen] = useState(false); const [rightSideNavToggle, setRightSideNavToggle] = useState(RightSideNav.PAGES); @@ -246,6 +247,21 @@ const SignDocument = () => { fetchData(); }, []) + // Ping Request - START + useEffect(() => { + const intervalId = setInterval(() => { + axios.get( + `${import.meta.env.VITE_BASE_URI}/api/v1/session/ping?sessionId=${recipientId}`, + { + headers: { "ngrok-skip-browser-warning": "69420" }, + } + ).catch(err => console.error("Ping failed:", err)); + }, 60000); // first call happens after 1 min + + return () => clearInterval(intervalId); // cleanup on unmount + }, [recipientId]); + // Ping Request - END + useEffect(() => { if (!focusElement) { if (droppedItems && droppedItems.length) { @@ -356,14 +372,14 @@ const SignDocument = () => { setIsFields(false) } - if (res?.data?.status === "COMPLETED" || res?.data?.status === "DECLINED") { - setShowCompletionScreen(true) + if (res?.data?.status === "COMPLETED" || res?.data?.status === "DECLINED" || res?.data?.details.type == "CC") { + // setShowCompletionScreen(true) + setTransparentTags(true) + setHideTags(true) + } dispatch(hideLoader()); - // setTimeout(() => { - // }, 600); - } }) @@ -372,21 +388,6 @@ const SignDocument = () => { }); } - const handleToastError = (msg: string) => { - toast.dismiss(); - toast.error(msg, { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - const handleFormSubmit = async () => { const submitData: TagsSubmitDataObj = { tags: [] @@ -402,13 +403,13 @@ const SignDocument = () => { } if (signInitialsCount !== Object.keys(signConfObj).length) { - handleToastError("Please ensure all signature and initials are signed") + // handleToastError("Please ensure all signature and initials are signed") return; } for (const tag of droppedItems) { if ("required" in tag && tag.required && ((tag.type !== "SIGNATURE" && tag.type !== "INITIALS") && (tag.value === undefined || tag.value === null || tag.value === "" || !("value" in tag)))) { - handleToastError(`${tag.label} is required`) + // handleToastError(`${tag.label} is required`) return; } @@ -444,8 +445,10 @@ const SignDocument = () => { dispatch(hideLoader()); setTransparentTags(true) setShowConfetti(true) - setShowCompletionScreen(true) + // setShowCompletionScreen(true) setSelectedTag(null) + // navigate("/transaction-complete") + window.location.href = `/transaction-complete` } } catch (e) { dispatch(hideLoader()); @@ -464,6 +467,11 @@ const SignDocument = () => { return; } + if (requiredFilled && focusElement.required == true) { + handleFormSubmit(); + return; + } + // If FocusElement is present updating in Based on Next and Prev const currFocusElemIndx = focusElement.focusElementIndx; if (currFocusElemIndx === undefined) return; @@ -485,19 +493,6 @@ const SignDocument = () => { } } - const fetchSignatureData = async (signatureId: string) => { - try { - const response = await axios.get(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}signatures/${signatureId}`, { - headers: { "ngrok-skip-browser-warning": "69420" } - }); - if (response.status === 200) { - return response.data; - } - } catch (error) { - } - return null; - } - const handlePrefillValues = () => { const prefilledTags = ["NAME", "EMAIL", "TEXT", "COMPANY", "TEMPORAL"] @@ -547,17 +542,24 @@ const SignDocument = () => { if (droppedItems.length > 0) { handlePrefillValues(); } - }, [droppedItems.length]) + }, [droppedItems]) useEffect(() => { const allRequiredFields = droppedItems.filter((item, idx) => { if (!item.required) return false - if ((item.required && item.value) || idx in signConfObj) return false + if ((item.required && item.value && item.type != "SIGNATURE" && item.type != "INITIALS") || (idx in signConfObj && saved.save)) return false return true }) + const totalRequiredFields = droppedItems.reduce((acc, obj) => { + if (obj.required == true) return acc + 1; + return acc; + }, 0) + + setHorizontalFormLoader({ requiredFilled: (totalRequiredFields - allRequiredFields.length), totalRequired: totalRequiredFields }) setRequiredFilled(allRequiredFields.length ? false : true) + }, [droppedItems, signConfObj]) @@ -653,7 +655,6 @@ const SignDocument = () => { if (setTo !== isFields) setIsFields(setTo); } - const handleOnConsent = () => { setSignerConsent((prev) => { return { ...prev, consent: true } @@ -662,15 +663,17 @@ const SignDocument = () => { const handleDeclineConfirm = async (reason: string, comments: string) => { try { - await axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/decline/${recipientId}`, { reason: reason + ", " + comments }); + await axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}/decline`, { reason: reason + ", " + comments }); setSignerConsent((prev) => ({ ...prev, decline: true })); setRecSignStatus("DECLINED"); - setShowCompletionScreen(true); + // setShowCompletionScreen(true); + // navigate("/transaction-declined") + window.location.href = `/transaction-declined` } catch (e) { } } const handleReassignConfirm = (data: { firstName: string; lastName: string; email: string; message: string }) => { - axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/reassign/${recipientId}`, { + axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/sign/${recipientId}/reassign`, { name: data.firstName ? data.firstName + " " + data.lastName : data.lastName, email: data.email, message: data.message, @@ -678,7 +681,9 @@ const SignDocument = () => { }) .then((res) => { if (res.status === 202) { - setShowCompletionScreen(true); + // setShowCompletionScreen(true); + // navigate("/transaction-reassigned") + window.location.href = `/transaction-reassigned` } }) .catch((err) => { @@ -698,20 +703,6 @@ const SignDocument = () => { }; } - // Utility to check if element is out of viewport - function isOutOfViewport(el: HTMLElement) { - const rect = el.getBoundingClientRect(); - const windowHeight = window.innerHeight; - const windowWidth = window.innerWidth; - - return ( - rect.right < 0 || - rect.bottom < 0 || - rect.left > windowWidth || - rect.top > windowHeight - ); - } - function printDocConDiv() { var printContents = document.getElementById(divId).innerHTML; var originalContents = document.body.innerHTML; @@ -755,31 +746,32 @@ const SignDocument = () => { }; }, []); - - if (showCompletionScreen) { - if (recSignStatus === "COMPLETED") { - return - } else if (recSignStatus === "DECLINED") { - return - } else { - return - } - } + // if (showCompletionScreen) { + // if (recSignStatus === "COMPLETED") { + // return + // } else if (recSignStatus === "DECLINED") { + // return + // } else { + // return + // } + // } return (
    - {!showCompletionScreen && <> - {/* */} - {/* {showCompletionScreen && } */} -
    ) => handleFormSubmit(e)} - > + + {/* */} + {/* {showCompletionScreen && } */} +
    ) => handleFormSubmit(e)} + > - {/* Nav Two - Options - START */} + {/* Nav Two - Options - START */} +
    +
    - + {/* Right Side - STARTS */}
    @@ -816,9 +808,9 @@ const SignDocument = () => { {/* PRINT _ START */} - + className="text-white mr-2 cursor-pointer" /> */} {/* PRINT _ END */} @@ -842,21 +834,18 @@ const SignDocument = () => {

    Signed Successfully

    } */} - {recipient?.type !== "CC" ? setDeclineOpen(true)} onReassignClick={() => setReassignOpen(true)} - /> : } + onAuditHistoryClick={() => setAuditHistoryOpen(true)} + showDecAndReassign={recipient?.type !== "CC"} + transactionId={"1"} + /> - - <> + {!transparentTags && recipient?.type !== "CC" && <>

    By Signing I agree Sign Made Easy Terms & Conditions.

    {/*
    @@ -912,11 +901,10 @@ const SignDocument = () => { {/* FIT - END */}
    - + } - - {requiredFilled && <> + {requiredFilled && !transparentTags && recipient?.type !== "CC" && <>

    All Set to Wrap This Up?

    @@ -932,194 +920,217 @@ const SignDocument = () => {
    {/* Right Side - ENDS */}
    - {/* Nav Two - Options - End */} - {/* Document Container Start */} -
    + {/* Horizontal Loader */} + {horizontalFormLoader.totalRequired !== null && !transparentTags && ( +
    +
    +
    + )} +
    - {/* Left Side pages start */} - {/*
    */} -
    + {/* Nav Two - Options - End */} -
    - {provideTags && recipient?.type !== "CC" &&
    handleSetIsFields(true)} - className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300'}`} - > - -

    Tags

    -
    } + {/* Document Container Start */} +
    -
    handleSetIsFields(provideTags ? false : !isFields)} - > - -

    Pages

    -
    + {/* Left Side pages start */} + {/*
    */} +
    - {/*

    Hi! {recipient && recipient.firstName} {recipient && recipient.lastName}

    */} + + {!transparentTags &&
    + {provideTags && recipient?.type !== "CC" &&
    handleSetIsFields(true)} + className={`flex items-center gap-1 cursor-pointer text-xs w-full justify-center py-1.5 rounded-2xl ${isFields ? 'bg-[var(--primary)] text-white' : ''} transition-all duration-300'}`} + > + +

    Tags

    +
    } + +
    handleSetIsFields(provideTags ? false : !isFields)} + > + +

    Pages

    - {isFields && recipientId && provideTags && recipient?.type !== "CC" && } + {/*

    Hi! {recipient && recipient.firstName} {recipient && recipient.lastName}

    */} +
    } - {!isFields && } + {!transparentTags && isFields && recipientId && provideTags && recipient?.type !== "CC" && } -
    - {/* Pages - END */} + {!isFields && } - {/* Main container */} -
    - { - selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => { - return ( -
    -
    -

    {doc.name}

    -
    +
    + {/* Pages - END */} -
    - { - doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => { - const key = `${doc.documentId}_${page.index}`; - // const tagJsonData = [...droppedItems]; - const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index) - let scale: { x: number | null, y: number | null } = { x: null, y: null } - return ( -
    handleDrop(e, doc.documentId, doc.documentId, page.index)} - onDragOver={(e) => handleDragOver(e)} - className='drop-page-in shadow-md relative' - > + {/* Main container */} +
    + { + selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => { + return ( +
    +
    +

    {doc.name}

    +
    - { - setPageScales((prev) => ({ - ...prev, - [page.index]: { - x: imgElem.clientWidth / page.width, - y: imgElem.clientHeight / page.height, - }, - })); - }} - width={page.width} - height={page.height} - src={page.src} +
    + { + doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => { + const key = `${doc.documentId}_${page.index}`; + // const tagJsonData = [...droppedItems]; + const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index) + let scale: { x: number | null, y: number | null } = { x: null, y: null } + return ( +
    handleDrop(e, doc.documentId, doc.documentId, page.index)} + onDragOver={(e) => handleDragOver(e)} + className='drop-page-in shadow-md relative' + > + + { + setPageScales((prev) => ({ + ...prev, + [page.index]: { + x: imgElem.clientWidth / page.width, + y: imgElem.clientHeight / page.height, + }, + })); + }} + width={page.width} + height={page.height} + src={page.src} + /> + + {!hideTags && <> + + } - {!hideTags && <> - - } - - {/* Side details of documents Pages and document Name - START */} - < p className='hidden sm:block absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-50px] text-slate-600' > - page {indx + 1} of {doc.pages && doc.pages.length} -

    - {/* Side details of documents Pages and document Name - END */} + {/* Side details of documents Pages and document Name - START */} + < p className='hidden sm:block absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-50px] text-slate-600' > + page {indx + 1} of {doc.pages && doc.pages.length} +

    + {/* Side details of documents Pages and document Name - END */} - {focusElement !== null && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index && -
    handleTagNavClick("N")} - > - {/* NEXT Button - START */} - - - - + {!transparentTags && focusElement !== null && focusElement.documentId == doc.documentId && focusElement.pageIndex == page.index && +
    handleTagNavClick("N")} + > - {/* NEXT Button - END */} -
    } -
    - ) - }) - } + + + {/* */} -
    + + {requiredFilled && focusElement.required == true ? "Submit" : focusElement.type.toLowerCase()} + + + + {/* NEXT Button - END */} +
    } +
    + ) + }) + }
    - ) - }) - } - - {(navigationTagPos === NavigationTagPosEnum.ABOVE || navigationTagPos === NavigationTagPosEnum.BELOW) && -
    scrollToElement(focusElement?.navigationOrder)} - className={`cursor-pointer fixed ${navigationTagPos === NavigationTagPosEnum.ABOVE ? "top-[45px]" : "bottom-[45px] rotate-[180deg]"} right-[8%] flex flex-col items-center`}> - {/* Arrow pointing up */} -
    - - {/* Label box */} -
    - Next
    -
    } - -
    - - {provideTags && selectedTag && selectedTag !== null && "key" in selectedTag && "index" in selectedTag && -
    - {/* Right Side Attributes Section if providedTags - START */} - - {/* Right Side Attributes Section if providedTags - END */} -
    + ) + }) } -
    - {/* Document Container End */} + + {(navigationTagPos === NavigationTagPosEnum.ABOVE || navigationTagPos === NavigationTagPosEnum.BELOW) && +
    scrollToElement(focusElement?.navigationOrder)} + className={`cursor-pointer fixed ${navigationTagPos === NavigationTagPosEnum.ABOVE ? "top-[45px]" : "bottom-[45px]"} right-[8%] flex flex-col items-center`}> + {/* Arrow pointing up */} + {navigationTagPos === NavigationTagPosEnum.ABOVE &&
    } + + {/* Label box */} +
    + {focusElement?.type.toLowerCase()} +
    + {navigationTagPos === NavigationTagPosEnum.BELOW &&
    } +
    } + +
    + + {provideTags && selectedTag && selectedTag !== null && "key" in selectedTag && "index" in selectedTag && +
    + {/* Right Side Attributes Section if providedTags - START */} + + {/* Right Side Attributes Section if providedTags - END */} +
    + }
    - } + {/* Document Container End */} + +
    {/* {showCompletionScreen && recSignStatus === "COMPLETED" && } {showCompletionScreen && recSignStatus === "DECLINED" && } */} @@ -1144,7 +1155,8 @@ const SignDocument = () => { {globalLoader.isLoading && } { - selectedDocuments !== null && !showCompletionScreen && !signerConsent.consent && handleOnConsent()} // onDecline={() => console.log("User declined")} /> @@ -1162,6 +1174,13 @@ const SignDocument = () => { onConfirm={({ firstName, lastName, email, message }) => handleReassignConfirm({ firstName, lastName, email, message })} /> + setAuditHistoryOpen(false)} + recipientId={recipientId} + // onConfirm={({ firstName, lastName, email, message }) => handleReassignConfirm({ firstName, lastName, email, message })} + /> +
    ) } diff --git a/src/components/client/TagsCon.tsx b/src/components/client/TagsCon.tsx index fa33a73..c01e666 100644 --- a/src/components/client/TagsCon.tsx +++ b/src/components/client/TagsCon.tsx @@ -301,6 +301,8 @@ const TagsCon: React.FC = ({ } } + console.log(droppedItems) + return ( <> {currDocTags.map((item, index) => { @@ -317,21 +319,21 @@ const TagsCon: React.FC = ({ // onPointerDown={(e) => handleTagMouseDown(e, key, index)} className="absolute rounded dropped-item text-sm" // onClick={() => handleTagClick(key, index)} - // style={{ - // left: `${item.dimensions.left}px`, - // top: `${item.dimensions.top}px`, - // width: `${item.dimensions.width}px`, - // height: `${item.dimensions.height}px`, - // border: `${((!hideTags && focusElement) && !transparentTags && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) ? '2px dashed #1c398e' : 'none'}`, - // // display: `${hideTags ? 'none' : 'block'}` - // }} - style={{ - left: item.dimensions.left * scale.x, - top: item.dimensions.top * scale.y, - width: item.dimensions.width * scale.x, - height: item.dimensions.height * scale.y, + left: `${item.dimensions.left}px`, + top: `${item.dimensions.top}px`, + width: `${item.dimensions.width}px`, + height: `${item.dimensions.height}px`, + border: `${((!hideTags && focusElement) && !transparentTags && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) ? '2px dashed #1c398e' : 'none'}`, + // display: `${hideTags ? 'none' : 'block'}` }} + + // style={{ + // left: item.dimensions.left * scale.x, + // top: item.dimensions.top * scale.y, + // width: item.dimensions.width * scale.x, + // height: item.dimensions.height * scale.y, + // }} > {item.required &&
    = ({
    {

    = ({

    {

    = ({ }} // contentEditable={true} - className="outline-none whitespace-nowrap w-full max-h-max" + className={`outline-none whitespace-nowrap w-full max-h-max text-[${item.color}] text-[${item.textSize}px] ${item.italic ? "italic": ""} ${item.bold ? "font-semibold": ""} ${item.underline ? "underline": ""}`} > {item.value ?? ""}

    @@ -614,20 +616,22 @@ const TagsCon: React.FC = ({ > {/* */} { - (!transparentTags && !hideTags && !(saved.save && (index in signConfObj && signConfObj[index].accepted))) ? - <> - - - : + (!transparentTags && !hideTags && !(saved.save && (index in signConfObj && signConfObj[index].accepted))) ? ( + + ) : saved.saveType === "SELECT" ? ( + <>{saved.fullName} + ) : ( src={`data:image/png;base64,${saved.signature}`} + alt="signature" /> + ) } +
    } {/* SIGNATURE FIELD - END */} diff --git a/src/components/dashboard/Loader.tsx b/src/components/dashboard/Loader.tsx index 1920bd8..786ede3 100644 --- a/src/components/dashboard/Loader.tsx +++ b/src/components/dashboard/Loader.tsx @@ -18,6 +18,8 @@ const Loader = () => { {/* Loader */}
    + + {/* */}

    {loaderData.message != undefined ? `${loaderData.message}` : 'Loading ...'} diff --git a/src/components/dashboard/SendForSign.tsx b/src/components/dashboard/SendForSign.tsx index 4606871..fe4a877 100644 --- a/src/components/dashboard/SendForSign.tsx +++ b/src/components/dashboard/SendForSign.tsx @@ -110,7 +110,7 @@ const SendForSign: React.FC = ({ showSideBar }) => {

    -
    handleStartSectionClick(SignTypeEnum.multipleSign)} className={` flex items-center justify-center @@ -128,7 +128,7 @@ const SendForSign: React.FC = ({ showSideBar }) => { ) : ( )} -
    +
    */} {/* {showSideBar && (
    = ({ showSideBar, setShowSideBar, isMobile const [showLogoutModal, setShowLogoutModal] = useState(false); const handleLogoutConfirm = async () => { - try { - await axios.post(`${import.meta.env.VITE_BASE_URI}/api/v1/auth/logout`, - {}, - { - withCredentials: true, - headers: { "ngrok-skip-browser-warning": "69420" } - } - ); + + api.get(`${import.meta.env.VITE_BASE_URI}/api/v1/auth/logout`).then((res) => { sessionStorage.clear(); + localStorage.clear(); // Redirect to login or refresh the page after logout + // window.location.href = '/login'; + }).catch((err) => { + sessionStorage.clear(); + localStorage.clear(); window.location.href = '/login'; - } catch (err) { - console.error('Logout failed:', err); - } + }); + }; return ( @@ -59,8 +58,8 @@ const SideBar: React.FC = ({ showSideBar, setShowSideBar, isMobile
    = ({ showSideBar, setShowSideBar, isMobile {/* Menu */}

    - +

    SME

    @@ -91,13 +90,13 @@ const SideBar: React.FC = ({ showSideBar, setShowSideBar, isMobile onClick={() => setShowSideBar(!showSideBar)} className={` absolute top-[95%] translate-y-[-60%] - ${isMobile - ? (showSideBar ? 'right-2' : 'right-2') + ${isMobile + ? (showSideBar ? 'right-2' : 'right-2') : 'right-0 top-[100%] translate-y-[-100%]' } bg-[var(--dashBoardBg)] p-1 - ${isMobile - ? 'rounded-md' + ${isMobile + ? 'rounded-md' : (showSideBar ? 'rounded-tl-sm rounded-bl-sm' : 'rounded-tl-sm rounded-bl-sm') } cursor-pointer transition-colors diff --git a/src/components/home/Cards.tsx b/src/components/home/Cards.tsx index c333c03..11bdd1f 100644 --- a/src/components/home/Cards.tsx +++ b/src/components/home/Cards.tsx @@ -10,6 +10,7 @@ import { FaFilter, FaCalendarAlt } from 'react-icons/fa'; +import { useNavigate } from 'react-router-dom'; // Define a type for card data interface CardData { @@ -97,7 +98,9 @@ const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year']; const accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3']; const Cards: React.FC = ({ type = 'dashboard', onOpenModal, onDateRangeChange, cards, isLoading }) => { + const navigate = useNavigate(); const [selectedFilter, setSelectedFilter] = useState('Last 7 Days'); + // const [isLoading, setIsLoading] = useState(true); // useEffect(() => { @@ -205,6 +208,7 @@ const Cards: React.FC = ({ type = 'dashboard', onOpenModal, onDateRa {cards.map((card, index) => (
    navigate(card.navigateTo)} className={`flex items-center justify-between bg-white px-4 py-3.5 transition-all duration-300 cursor-pointer ${index > 0 ? "border-l-1 border-gray-200" : ""}`} style={{ animationDelay: `${index * 100}ms`, diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx index d6fc766..75560c4 100644 --- a/src/components/home/Home.tsx +++ b/src/components/home/Home.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, JSX } from 'react'; import AccountNav from '../accounts/AccountNav'; import Cards from './Cards'; import StartSection from './StartSection'; @@ -18,6 +18,7 @@ interface CardData { textColor: string; percentage?: number; trend?: 'up' | 'down' | 'stable'; + navigateTo: string; } const Home = () => { @@ -26,38 +27,42 @@ const Home = () => { { title: "Action Required", count: data.actionNeeded || 0, - icon: , - iconBg: "bg-[#F3F9FF]", + icon: , + iconBg: "bg-sky-100 border-sky-200", textColor: "text-sky-600", gradient: "bg-white", shadowColor: "shadow-sky-100", + navigateTo: "/transactions/action_required" }, { title: "Waiting for Response", count: data.waitingForOthers || 0, icon: , - iconBg: "bg-[#FFF9EB]", + iconBg: "bg-yellow-100 border-yellow-200", textColor: "text-yellow-600", gradient: "bg-white", shadowColor: "shadow-yellow-100", + navigateTo: "/transactions/waiting_for_response" }, { title: "Expiring Soon", count: data.expiring || 0, icon: , - iconBg: "bg-[#FFF8F8]", + iconBg: "bg-red-100 border-red-200", textColor: "text-red-600", gradient: "bg-white", shadowColor: "shadow-red-100", + navigateTo: "/transactions/expiring_soon" }, { title: "Completed", count: data.completed || 0, icon: , - iconBg: "bg-[#F1FFEF]", + iconBg: "bg-green-100 border-green-200", textColor: "text-green-600", gradient: "bg-white", shadowColor: "shadow-green-100", + navigateTo: "/transactions/completed" }, ]; }; diff --git a/src/components/home/StartNow.tsx b/src/components/home/StartNow.tsx index ad42a55..3c5fd97 100644 --- a/src/components/home/StartNow.tsx +++ b/src/components/home/StartNow.tsx @@ -32,7 +32,7 @@ const StartNow = () => { const subject = filesToUpload[0].name ? filesToUpload[0].name : "Untitled" - const transactionRes = await api.post(`/api/v1/accounts/${userData.account?.id}/transactions`, { subject }); + const transactionRes = await api.post(`/api/v1/accounts/${userData.account?.id}/transactions`, { subject: "Sign Document - " + subject }); const transactionId = transactionRes.data.id const formData = new FormData(); diff --git a/src/components/process/CompletionScreen/TransactionSentSuccess.tsx b/src/components/process/CompletionScreen/TransactionSentSuccess.tsx index 8b2cc33..99f31be 100644 --- a/src/components/process/CompletionScreen/TransactionSentSuccess.tsx +++ b/src/components/process/CompletionScreen/TransactionSentSuccess.tsx @@ -82,7 +82,7 @@ const TransactionSentSuccess = () => { className="flex items-center text-gray-600 hover:text-[var(--primary)] transition-colors duration-200" > {/* */} - + {/*
    */} {/*

    diff --git a/src/components/process/processStepOne/DocumentCard.tsx b/src/components/process/processStepOne/DocumentCard.tsx index ac0d19f..0e9755e 100644 --- a/src/components/process/processStepOne/DocumentCard.tsx +++ b/src/components/process/processStepOne/DocumentCard.tsx @@ -40,7 +40,7 @@ const DocumentCard: React.FC = ({ id, name, isCompleted, tran let lastUpdate = 0; const delay = 1000; - const docProgSSE = new EventSource(`http://10.10.1.99:8083/api/v1/progress/${id}`); + const docProgSSE = new EventSource(`${import.meta.env.VITE_SSE_URI}/api/v1/progress/${id}`); docProgSSE.addEventListener('progress', (event) => { const jsonData = JSON.parse(event.data); diff --git a/src/components/process/processStepOne/ProcessStepOne.tsx b/src/components/process/processStepOne/ProcessStepOne.tsx index c61f676..07c5d05 100644 --- a/src/components/process/processStepOne/ProcessStepOne.tsx +++ b/src/components/process/processStepOne/ProcessStepOne.tsx @@ -337,7 +337,7 @@ const ProcessStepOne = () => { (async () => { try { await api.put(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" }) - if(accountId && transactionId) signSessionRedirect(accountId, transactionId, recipientId); + if (accountId && transactionId) signSessionRedirect(accountId, transactionId, recipientId); dispatch(hideLoader()); } catch (err) { console.log("Error Transaction Sent", err.message); @@ -367,7 +367,13 @@ const ProcessStepOne = () => { const handleClose = () => { dispatch(resetDocuments()) - navigate('/') + + if (window.location.href.includes("template") || window.location.href.includes("useTemplate")) { + navigate('/templates') + } else { + navigate('/') + } + } useEffect(() => { diff --git a/src/components/process/processStepOne/RecipientCards.tsx b/src/components/process/processStepOne/RecipientCards.tsx index 5749848..e9e0320 100644 --- a/src/components/process/processStepOne/RecipientCards.tsx +++ b/src/components/process/processStepOne/RecipientCards.tsx @@ -160,7 +160,8 @@ const RecipientCards = ({ placeholder={placeholder} onFocus={() => handleFieldFocus(identifier, fieldName)} onBlur={(e) => handleFieldBlur(recipient, fieldName, e.target.value)} - onChange={(e) => handleFieldChange(recipient, fieldName, e.target.value)} + // onChange={(e) => handleFieldChange(recipient, fieldName, e.target.value)} + onInput={(e) => handleFieldChange(recipient, fieldName, (e.target as HTMLInputElement).value)} onKeyPress={(e) => { if (e.key === "Enter") { e.currentTarget.blur(); @@ -450,10 +451,16 @@ const RecipientCards = ({
    {/* Role Name and Permissions Row */}
    - {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES &&
    -
    - {renderEditableField(recipient, "roleName", "Role name")} -
    + {
    + {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? +
    + {renderEditableField(recipient, "roleName", "Role name")} +
    + : +
    + {recipient.roleName} +
    + }
    } {/* Permissions - Mobile: Below role, Desktop: Right side */} diff --git a/src/components/process/processStepTwo/DocumentContainer.tsx b/src/components/process/processStepTwo/DocumentContainer.tsx index d4f372f..5c788f5 100644 --- a/src/components/process/processStepTwo/DocumentContainer.tsx +++ b/src/components/process/processStepTwo/DocumentContainer.tsx @@ -191,7 +191,7 @@ const DocumentContainer: React.FC = ({ transactionSaving } } - updatedData[recipientId] = updateTags + updatedData[recipientId] = updateTags.filter((tag, indx) => !tag.isDeleted); } const requests = Object.entries(updatedData) @@ -459,12 +459,6 @@ const DocumentContainer: React.FC = ({ transactionSaving } - const handleLazyLoad = (docId: string, pageId: string) => { - api.get(`http://10.10.1.99:8083/api/v1/${accountId}/transactions/${transactionId}/documents/${docId}/pages/${pageId}`).then(res => { - console.log(res) - }) - } - return ( <> {/* Center Doc Container - START */} diff --git a/src/components/process/processStepTwo/FieldList.tsx b/src/components/process/processStepTwo/FieldList.tsx index 92f2d37..c3ca6ff 100644 --- a/src/components/process/processStepTwo/FieldList.tsx +++ b/src/components/process/processStepTwo/FieldList.tsx @@ -7,6 +7,7 @@ import { FaQuestionCircle } from 'react-icons/fa'; import { TagsProperties, TagsTypes } from '../../../utlity/types'; import { v4 as uuidv4 } from "uuid"; import axios from 'axios'; +import api from '../../../config/api'; const handlePointerDown = (event: React.DragEvent, label: string, recipientId: number, recipientIndx: number, tagsProperties: TagsProperties) => { const dragElemRect = event.currentTarget.getBoundingClientRect(); @@ -119,8 +120,8 @@ const FieldList: React.FC = ({ color, recipientId, recipientIndx } useEffect(() => { - axios - .get(`${import.meta.env.VITE_BASE_URI}/api/v1/accounts/${accountId}/tags`, { + api + .get(`/api/v1/accounts/${accountId}/tags`, { headers: { "ngrok-skip-browser-warning": "69420", } diff --git a/src/components/process/processStepTwo/NavTwo.tsx b/src/components/process/processStepTwo/NavTwo.tsx index 03df70b..5718da8 100644 --- a/src/components/process/processStepTwo/NavTwo.tsx +++ b/src/components/process/processStepTwo/NavTwo.tsx @@ -149,8 +149,32 @@ const NavTwo: React.FC = ({ const [nav2RecToggle, setNav2RecToggle] = useState(false); const [nav2DocToggle, setNav2DocToggle] = useState(false); + + const transactionSendValidation = () => { + + const transactionSendValidationObj = { + oneSignerIsRequiredError: true + } + + // One Signer is required - Validation + for (const recipient of selectedRecipients) { + if (recipient.type == recipientType.SIGNER || recipient.type == recipientType.INPERSONSIGNER) { + transactionSendValidationObj.oneSignerIsRequiredError = false; break + }; + } + + if (!transactionSendValidationObj.oneSignerIsRequiredError) return false; + return transactionSendValidationObj; + } + function handleSend(isSignSend: boolean = false) { // dispatch(showLoader("Loading ...")); + + const transactionSendValidationObj = transactionSendValidation() + if(transactionSendValidationObj.oneSignerIsRequiredError) { + return + } + if (isSend === null) { setIsSend(false); } @@ -181,6 +205,7 @@ const NavTwo: React.FC = ({ }, 1000) } + // const handleSetIsFields = (setTo: boolean) => { // if (setTo !== undefined && setTo !== isFields) { // setIsFields(setTo) diff --git a/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx b/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx index 7430c5c..bd17bd1 100644 --- a/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx +++ b/src/components/process/processStepTwo/rightSideBar/RightSideBar.tsx @@ -83,16 +83,14 @@ export enum DROPDOWNTOGGLE { } const defaultStyles: StylesInterface = { - font: "", + font: "sans-serif", italic: false, bold: false, underline: false, color: "#000000", - textSize: 0 + textSize: 14 }; - - const RightSideBar: React.FC = ( { droppedItems, diff --git a/src/components/setting/Signature/DrawSignatureCanvas.tsx b/src/components/setting/Signature/DrawSignatureCanvas.tsx index 25b4247..bd5028e 100644 --- a/src/components/setting/Signature/DrawSignatureCanvas.tsx +++ b/src/components/setting/Signature/DrawSignatureCanvas.tsx @@ -109,7 +109,7 @@ const DrawSignatureCanvas: React.FC = ({ onSignatureChange, initialSignat const handleMouseMove = (e: React.MouseEvent, type: "SIGNATURE" | "INITIAL") => { if (!isDrawing) return; setIsDrawingType((prev) => { - return {...prev, signatureDrawn: type === "SIGNATURE" ? true : prev.signatureDrawn, initialDrawn: type === "INITIAL" ? true : prev.initialDrawn } + return { ...prev, signatureDrawn: type === "SIGNATURE" ? true : prev.signatureDrawn, initialDrawn: type === "INITIAL" ? true : prev.initialDrawn } }) const rect = e.currentTarget.getBoundingClientRect(); const point = { x: e.clientX - rect.left, y: e.clientY - rect.top }; @@ -147,7 +147,7 @@ const DrawSignatureCanvas: React.FC = ({ onSignatureChange, initialSignat const handleMouseUp = () => { setIsDrawing(false); - + if (currentPath.length === 0 || !activeCanvas) return; if (activeCanvas === 'full') { @@ -206,6 +206,8 @@ const DrawSignatureCanvas: React.FC = ({ onSignatureChange, initialSignat ctx2?.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height); } + setIsDrawingType({ signatureDrawn: false, initialDrawn: false }) + setPaths1([]); setPaths2([]); setInitialImage1(null); @@ -241,11 +243,12 @@ const DrawSignatureCanvas: React.FC = ({ onSignatureChange, initialSignat {/* Controls */}
    -
    +
    diff --git a/src/components/templates/Templates.tsx b/src/components/templates/Templates.tsx index c941b81..aac445f 100644 --- a/src/components/templates/Templates.tsx +++ b/src/components/templates/Templates.tsx @@ -4,11 +4,13 @@ import { Bounce, toast } from "react-toastify"; import api from "../../config/api"; import { IoMdAdd } from "react-icons/io"; import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { FaSpinner } from "react-icons/fa"; import LoaderUi from "../../helper/LoaderUi"; import TemplateTable from "./TemplateTable"; +import { resetRecipients } from "../../store/slices/selectedRecipientsSlice"; +import { resetDocuments } from "../../store/slices/documentSlice"; @@ -25,13 +27,14 @@ const Templates = () => { ); const navigate = useNavigate(); + const dispatch = useDispatch(); const filters = ["All", "My Templates", "Shared"]; - - const handleAddTemplate = async () => { try { + await dispatch(resetRecipients()) + await dispatch(resetDocuments()) const res = await api.post(`/api/v1/accounts/${activeAccountId}/templates`); navigate(`/process-step-one/${activeAccountId}/template/${res.data.id}`) } catch (error) { diff --git a/src/components/transactions/AuditModal.tsx b/src/components/transactions/AuditModal.tsx index c29fe16..fb5aa73 100644 --- a/src/components/transactions/AuditModal.tsx +++ b/src/components/transactions/AuditModal.tsx @@ -49,7 +49,7 @@ const AuditModal: React.FC = ({ closeModal }) => { const getAuditDetails = async () => { // setLoading(true) try { - let res = await api.get(`/api/v1/accounts/${accountId}/transactions/${transactionId}/audit?lang=${userDetails.language || ''}`) + let res = await api.get(`/api/v1/accounts/${accountId}/transactions/${transactionId}/audit/events?lang=${userDetails.language || ''}`) // let res = await api.get(`/api/v1/accounts/${accountId}/transactions/${transactionId}/audit?lang=hi`) if (res?.status === 200) { // console.log(res?.data?.events) diff --git a/src/components/transactions/TransactionView.tsx b/src/components/transactions/TransactionView.tsx index dbc5004..406c650 100644 --- a/src/components/transactions/TransactionView.tsx +++ b/src/components/transactions/TransactionView.tsx @@ -216,7 +216,7 @@ const TransactionView = () => { {transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && ( diff --git a/src/helper/LoaderUi.tsx b/src/helper/LoaderUi.tsx index d0f7730..7033217 100644 --- a/src/helper/LoaderUi.tsx +++ b/src/helper/LoaderUi.tsx @@ -15,6 +15,7 @@ const LoaderUi = ({ classStyle = '' }: StyleProps) => { {/* Loader box */}
    + {/* */}

    Loading...!