Compare commits

...

7 Commits

Author SHA1 Message Date
=bhagwanSingh aa9bd93bce merge 05 Sep 2025-09-05 12:16:39 +05:30
=bhagwanSingh cdbada638b Merge branch 2025-09-01 15:52:17 +05:30
=bhagwanSingh 3bb2991f72 Added Settings in Nav 2025-09-01 15:42:42 +05:30
JaiPandey425 9ebd3f66bd res 2025-09-01 14:19:27 +05:30
=bhagwanSingh e23b132d53 Meeting Changes | Hot Keys Shortcut Update 2025-09-01 14:15:24 +05:30
JaiPandey425 4f238f4a6f respo mob 2025-09-01 14:05:38 +05:30
JaiPandey425 2eaa1ed3e1 step-1-form-edit-ui-input-field 2025-08-29 15:27:42 +05:30
38 changed files with 2920 additions and 2012 deletions

8
.env
View File

@ -1,3 +1,7 @@
VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app" # VITE_BASE_URI="https://wired-terminally-muskrat.ngrok-free.app"
# VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app"
# VITE_BASE_URI="https://python-amused-adder.ngrok-free.app" # VITE_BASE_URI="https://python-amused-adder.ngrok-free.app"
# VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app" VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
# VITE_BASE_URI="http://192.168.2.176:8083"

View File

@ -22,6 +22,7 @@ import Templates from "./components/templates/Templates";
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen"; import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
import JoyrideController from "./utlity/JoyRideTour"; import JoyrideController from "./utlity/JoyRideTour";
import Contacts from "./components/contacts/Contacts"; import Contacts from "./components/contacts/Contacts";
import TransactionSentSuccess from "./components/process/CompletionScreen/TransactionSentSuccess";
const App = () => { const App = () => {
return ( return (
@ -84,10 +85,16 @@ const App = () => {
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/document-completion-screen/:accountId/:transactionId" element={ {/* <Route path="/document-completion-screen/:accountId/:transactionId" element={
<ProtectedRoute> <ProtectedRoute>
<DocumentCompletionScreen /> <DocumentCompletionScreen />
</ProtectedRoute> </ProtectedRoute>
} /> */}
<Route path="/transaction-sent/:accountId/:transactionId" element={
<ProtectedRoute>
<TransactionSentSuccess />
</ProtectedRoute>
} /> } />
{/* Links work wothout Login */} {/* Links work wothout Login */}

BIN
src/assets/SMELogo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -2,6 +2,9 @@ import React, { useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa"; import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa";
import api from '../config/api'; import api from '../config/api';
import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
import { useSelector } from 'react-redux';
import { RootState } from '../store/store';
interface AccountNavProps { interface AccountNavProps {
accountName?: string | null; accountName?: string | null;
@ -20,6 +23,8 @@ interface Account {
interface UserData { interface UserData {
id: number; id: number;
name: string; name: string;
firstName: string;
lastName: string;
email: string; email: string;
language: string; language: string;
account: Account; account: Account;
@ -50,6 +55,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null'); const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}'); const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
const userDetailsAsync = useSelector((state: RootState) => state.user)
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [accountsOpen, setAccountsOpen] = useState(false); const [accountsOpen, setAccountsOpen] = useState(false);
const [loadingAccounts, setLoadingAccounts] = useState(false); const [loadingAccounts, setLoadingAccounts] = useState(false);
@ -57,9 +64,13 @@ const CommonNav: React.FC<AccountNavProps> = ({
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const [initial, setInitial] = useState<string>("");
const [user, setUser] = useState<UserData>({ const [user, setUser] = useState<UserData>({
id: 0, id: 0,
name: "", name: "",
firstName: "",
lastName: "",
email: "", email: "",
language: "", language: "",
account: { account: {
@ -69,6 +80,19 @@ const CommonNav: React.FC<AccountNavProps> = ({
}, },
}); });
const handleSettingsClick = () => {
setOpen(false);
navigate("/s/settings#profile-info");
}
useEffect(() => {
if(initial !== "") return
console.log(userDetailsAsync, 'line 89')
let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0])
initialN = initialN+""+(userDetailsAsync.lastName && userDetailsAsync.lastName[0])
setInitial(initialN)
}, [userDetailsAsync])
const getAccountDetails = async () => { const getAccountDetails = async () => {
try { try {
let res = await api.get('/api/user/accounts'); let res = await api.get('/api/user/accounts');
@ -117,6 +141,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
setUser({ setUser({
id: account.userId, id: account.userId,
name: account.name, name: account.name,
firstName: account.firstName,
lastName: account.lastName,
email: account.userEmail, email: account.userEmail,
language: account.language, // set it if available language: account.language, // set it if available
account: { account: {
@ -126,6 +152,12 @@ const CommonNav: React.FC<AccountNavProps> = ({
}, },
}); });
if (account.firstName && account.lastName) {
setInitial((account?.firstName?.slice(0, 1)).toUpperCase() + "" + (account?.lastName?.slice(0, 1)).toUpperCase())
} else {
setInitial((account?.name?.slice(0, 1)).toUpperCase())
}
sessionStorage.setItem('userDetails', JSON.stringify({ sessionStorage.setItem('userDetails', JSON.stringify({
id: account.userId, id: account.userId,
name: account.name, name: account.name,
@ -153,6 +185,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
const userData = { const userData = {
id: item?.id, id: item?.id,
name: item?.firstName + " " + item?.lastName, name: item?.firstName + " " + item?.lastName,
firstName: item?.firstName,
lastName: item?.lastName,
email: item?.email, email: item?.email,
language: item?.language, language: item?.language,
account: { account: {
@ -163,6 +197,12 @@ const CommonNav: React.FC<AccountNavProps> = ({
}; };
setUser(userData); setUser(userData);
if (userData.firstName && userData.lastName) {
setInitial((userData?.firstName?.slice(0, 1)).toUpperCase() + "" + (userData?.lastName?.slice(0, 1)).toUpperCase())
} else {
setInitial((userData?.name?.slice(0, 1)).toUpperCase())
}
sessionStorage.setItem('userDetails', JSON.stringify(userData)); sessionStorage.setItem('userDetails', JSON.stringify(userData));
sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id)); sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id));
@ -334,7 +374,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md"> <div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
{user ? ( {user ? (
<span className="w-full h-full object-cover p-1.5 font-bold"> <span className="w-full h-full object-cover p-1.5 font-bold">
{(user?.name?.slice(0, 2)).toUpperCase()} {initial}
</span> </span>
) : ( ) : (
<FaUser className="text-lg" /> <FaUser className="text-lg" />
@ -349,7 +389,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold shadow-md"> <div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
{user ? ( {user ? (
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold"> <span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold">
{(user?.name?.slice(0, 2)).toUpperCase()} {initial}
</span> </span>
) : ( ) : (
<FaUser className="text-lg" /> <FaUser className="text-lg" />
@ -365,6 +405,18 @@ const CommonNav: React.FC<AccountNavProps> = ({
</div> </div>
</div> </div>
<div className="border-b border-gray-100">
<button
onClick={handleSettingsClick}
className="w-full flex items-center justify-between px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer"
>
<div className="flex items-center">
<IoSettings className="mr-2.5 text-lg text-gray-500" />
<span>Settings</span>
</div>
{/* <FaChevronDown className={`text-gray-400 transition-transform ${accountsOpen ? 'rotate-180' : ''}`} /> */}
</button>
</div>
<div className="border-b border-gray-100"> <div className="border-b border-gray-100">
<button <button
onClick={handleAccountsClick} onClick={handleAccountsClick}
@ -437,20 +489,5 @@ const CommonNav: React.FC<AccountNavProps> = ({
</div> </div>
</header> </header>
); );
// const ShimmerRow = () => (
// <div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
// <div className="flex items-center space-x-3">
// <div className="w-8 h-8 bg-gray-200 rounded-full"></div>
// <div className="flex-1 space-y-2">
// <div className="h-4 bg-gray-200 rounded w-3/4"></div>
// <div className="h-3 bg-gray-200 rounded w-1/2"></div>
// </div>
// <div className="w-16 h-6 bg-gray-200 rounded"></div>
// </div>
// </div>
// );
// };
} }
export default CommonNav; export default CommonNav;

View File

@ -1,15 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FaCheck } from "react-icons/fa";
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi"; import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import { FaDatabase } from "react-icons/fa"; import { FaDatabase } from "react-icons/fa";
import AccountNav from "./AccountNav";
import api from "../../config/api"; import api from "../../config/api";
import { AccountData, AccountStatus, UserRole } from "../../utlity/types"; import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Cards from "../home/Cards";
import AddAccountModal from "./AddAccountModal"; import AddAccountModal from "./AddAccountModal";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import Loader from "../../helper/Loader";
// import { UserRole, AccountStatus, AccountData } from "../../types/account"; // import { UserRole, AccountStatus, AccountData } from "../../types/account";
@ -36,15 +32,9 @@ const getStatusLabel = (
const Accounts = () => { const Accounts = () => {
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]); const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [accounts, setAccounts] = useState<string[]>([]);
const [openMenuId, setOpenMenuId] = useState<string | null>(null); const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const handleOpenModal = () => {
setIsModalOpen(true);
};
const navigate = useNavigate(); const navigate = useNavigate();
const fetchAccounts = async () => { const fetchAccounts = async () => {

View File

@ -1,14 +1,11 @@
// import OAuthGoogle from "./OAuthGoogle"; // import OAuthGoogle from "./OAuthGoogle";
import axios from "axios"; import axios from "axios";
import { useState } from "react"; import { useRef, useState } from "react";
// import { FcGoogle } from "react-icons/fc"; // import { FcGoogle } from "react-icons/fc";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import OAuthGoogle from "./OAuthGoogle"; import OAuthGoogle from "./OAuthGoogle";
import { Bounce, toast } from "react-toastify";
// import Cookies from "js-cookie"; // import Cookies from "js-cookie";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../store/store";
import { BsEye } from "react-icons/bs"; import { BsEye } from "react-icons/bs";
import { FiEyeOff } from "react-icons/fi"; import { FiEyeOff } from "react-icons/fi";
// import api from "../../config/api"; // import api from "../../config/api";
@ -27,6 +24,9 @@ const LoginForm = ({ onSwitchToRegister }) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState({ email: "", password: "" }); const [errors, setErrors] = useState({ email: "", password: "" });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
const navigate = useNavigate() const navigate = useNavigate()
const validateEmail = (email) => { const validateEmail = (email) => {
@ -66,15 +66,6 @@ const LoginForm = ({ onSwitchToRegister }) => {
if (!password) { if (!password) {
return "Password is required"; return "Password is required";
} }
// if (password.length < 6) {
// return "Password must be at least 6 characters long";
// }
// if (password.length > 128) {
// return "Password is too long";
// }
return ""; return "";
}; };
@ -98,49 +89,6 @@ const LoginForm = ({ onSwitchToRegister }) => {
} }
}; };
const handleNextClick = async () => {
const emailError = validateEmail(formData.email);
if (emailError) {
setErrors({ ...errors, email: emailError });
return;
}
setIsLoading(true);
try {
const res = await axios.get(
`${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`,
{
headers: {
"ngrok-skip-browser-warning": "69420",
}
}
);
if (res.status === 200) {
toast.dismiss();
setCacheKey(res.data);
setNextClick(true);
}
} catch (err: any) {
toast.dismiss();
toast.error("User Does Not Exist", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
theme: "light",
transition: Bounce,
});
} finally {
setIsLoading(false);
}
};
const handleFormLoginClick = async () => { const handleFormLoginClick = async () => {
const emailError = validateEmail(formData.email); const emailError = validateEmail(formData.email);
const passwordError = validatePassword(formData.password); const passwordError = validatePassword(formData.password);
@ -167,57 +115,16 @@ const LoginForm = ({ onSwitchToRegister }) => {
); );
if (res.status === 200) { if (res.status === 200) {
toast.dismiss(); navigate("/");
formErrorRef.current = { message: "Login Successfully", error: false }
// Example if you want cookies / redux:
// Cookies.set("authorization", res.headers.authorization, {
// path: "/",
// secure: true,
// sameSite: "Lax",
// });
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
navigate("/"); // Redirect after 1 second
toast.success("Login Successful", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
} }
} catch (err) { } catch (err) {
toast.dismiss();
if (err.response?.status === 400 || err.response?.status === 401) { if (err.response?.status === 400 || err.response?.status === 401) {
toast.error("Credentials Not Match", { formErrorRef.current = { message: "The email or password you entered is invalid.", error: true }
position: "top-center", } else if(err.response?.status === 404) {
autoClose: 5000, formErrorRef.current = { message: "Email not found. Please sign up to continue.", error: true }
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
} else { } else {
toast.error("Some Error Occurred", { formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true }
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
} }
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -305,18 +212,9 @@ const LoginForm = ({ onSwitchToRegister }) => {
</div> </div>
{/* )} */} {/* )} */}
{/* {!nextClick ? ( <p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
<button
onClick={handleNextClick}
disabled={isLoading}
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white"
}`}
>
{isLoading ? "Verifying..." : "Next"}
</button>
) : ( */}
<button <button
onClick={handleFormLoginClick} onClick={handleFormLoginClick}
disabled={isLoading} disabled={isLoading}

View File

@ -1,15 +1,9 @@
// import { GoogleSvg, SalesforceSvg } from "../svgs/Svg" // import { GoogleSvg, SalesforceSvg } from "../svgs/Svg"
import OAuthGoogle from "./OAuthGoogle" import OAuthGoogle from "./OAuthGoogle"
import { useEffect, useState } from "react"; import { useRef, useState } from "react";
import { Link, Navigate, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import axios from "axios"; import axios from "axios";
// import Cookies from "js-cookie";
import { Bounce, toast } from "react-toastify";
import { updateUser, UserState } from "../../store/slices/userSlice";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../store/store";
import { BsEye } from "react-icons/bs"; import { BsEye } from "react-icons/bs";
import { FiEyeOff } from "react-icons/fi"; import { FiEyeOff } from "react-icons/fi";
// import api from "../../config/api"; // import api from "../../config/api";
@ -23,7 +17,9 @@ interface FormDataState {
} }
const RegisterForm = ({ onSwitchToLogin }) => { const RegisterForm = ({ onSwitchToLogin }) => {
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
const [formData, setForm] = useState({ const [formData, setForm] = useState({
email: "", email: "",
password: "", password: "",
@ -175,92 +171,68 @@ const RegisterForm = ({ onSwitchToLogin }) => {
}; };
const handleFormRegisterClick = async () => { const handleFormRegisterClick = async () => {
const firstNameError = validateFirstName(formData.firstName); const firstNameError = validateFirstName(formData.firstName);
const lastNameError = validateLastName(formData.lastName); const lastNameError = validateLastName(formData.lastName);
const emailError = validateEmail(formData.email); const emailError = validateEmail(formData.email);
const passwordError = validatePassword(formData.password); const passwordError = validatePassword(formData.password);
const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password); const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password);
const newErrors = { const newErrors = {
firstName: firstNameError, firstName: firstNameError,
lastName: lastNameError, lastName: lastNameError,
email: emailError, email: emailError,
password: passwordError, password: passwordError,
confirmPassword: confirmPasswordError, confirmPassword: confirmPasswordError,
}; };
setErrors(newErrors); setErrors(newErrors);
// If any errors exist, stop // If any errors exist, stop
if (Object.values(newErrors).some((error) => error !== "")) { if (Object.values(newErrors).some((error) => error !== "")) {
return; return;
} }
setIsLoading(true); setIsLoading(true);
const trimmedData = { const trimmedData = {
firstName: formData.firstName.trim(), firstName: formData.firstName.trim(),
lastName: formData.lastName.trim(), lastName: formData.lastName.trim(),
email: formData.email.trim(), email: formData.email.trim(),
password: formData.password, password: formData.password,
confirmPassword: formData.confirmPassword, confirmPassword: formData.confirmPassword,
}; };
try { try {
const res = await axios.post( const res = await axios.post(
`${import.meta.env.VITE_BASE_URI}/api/auth/signup`, `${import.meta.env.VITE_BASE_URI}/api/auth/signup`,
{ {
firstName: trimmedData.firstName, firstName: trimmedData.firstName,
lastName: trimmedData.lastName, lastName: trimmedData.lastName,
email: trimmedData.email, email: trimmedData.email,
password: trimmedData.password, password: trimmedData.password,
}, },
{ {
withCredentials: true, withCredentials: true,
headers: { headers: {
"ngrok-skip-browser-warning": "69420", "ngrok-skip-browser-warning": "69420",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
}, },
} }
); );
if (res.status === 201) {
toast.dismiss();
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
// dispatch(updateUser({ ...res.data, authorized: true }));
navigate("/");
}
} catch (err: any) {
toast.dismiss();
if (err.response?.status === 409) {
toast.error("User Already Exists", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
theme: "light",
transition: Bounce,
});
} else {
toast.error("Some Error Occurred", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
theme: "light",
transition: Bounce,
});
}
} finally {
setIsLoading(false);
}
};
if (res.status === 201) {
navigate("/");
}
} catch (err: any) {
if (err.response?.status === 409) {
formErrorRef.current = { message: 'User Already Exists. Please try logging.', error: true }
} else {
formErrorRef.current = { message: 'Something went wrong. Please refresh and try again.', error: true }
}
} finally {
setIsLoading(false);
}
};
const handleGoogleLogin = () => { const handleGoogleLogin = () => {
// Your Google OAuth logic here // Your Google OAuth logic here
@ -296,8 +268,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
onChange={(e) => handleInputChange("firstName", e.target.value)} onChange={(e) => handleInputChange("firstName", e.target.value)}
placeholder="First name" placeholder="First name"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName 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-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500" : "border-gray-300 focus:ring-blue-500"
}`} }`}
disabled={isLoading} disabled={isLoading}
/> />
@ -312,8 +284,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
onChange={(e) => handleInputChange("lastName", e.target.value)} onChange={(e) => handleInputChange("lastName", e.target.value)}
placeholder="Last name" placeholder="Last name"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName 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-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500" : "border-gray-300 focus:ring-blue-500"
}`} }`}
disabled={isLoading} disabled={isLoading}
/> />
@ -334,8 +306,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
onChange={(e) => handleInputChange("email", e.target.value)} onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="Enter your email" 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 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-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500" : "border-gray-300 focus:ring-blue-500"
}`} }`}
disabled={isLoading} disabled={isLoading}
/> />
@ -355,8 +327,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
onChange={(e) => handleInputChange("password", e.target.value)} onChange={(e) => handleInputChange("password", e.target.value)}
placeholder="Enter your password" 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 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-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500" : "border-gray-300 focus:ring-blue-500"
}`} }`}
disabled={isLoading} disabled={isLoading}
/> />
@ -385,8 +357,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
onChange={(e) => handleInputChange("confirmPassword", e.target.value)} onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
placeholder="Confirm your password" 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 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-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500" : "border-gray-300 focus:ring-blue-500"
}`} }`}
disabled={isLoading} disabled={isLoading}
/> />
@ -404,12 +376,14 @@ const RegisterForm = ({ onSwitchToLogin }) => {
)} )}
</div> </div>
<p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
<button <button
onClick={handleFormRegisterClick} onClick={handleFormRegisterClick}
disabled={isLoading} disabled={isLoading}
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading className={`w-full mt-2 py-2 rounded-lg font-medium transition-colors ${isLoading
? "bg-gray-400 cursor-not-allowed" ? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white" : "bg-blue-500 hover:bg-blue-600 text-white"
}`} }`}
> >
{isLoading ? "Creating Account..." : "Create Account"} {isLoading ? "Creating Account..." : "Create Account"}

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import "../styles/style.css"; // import "../styles/style.css";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import LoginForm from "./LoginForm"; import LoginForm from "./LoginForm";
import RegisterForm from "./RegisterForm"; import RegisterForm from "./RegisterForm";
@ -37,7 +37,7 @@ function UserAuthUI() {
return ( return (
<div className="max-h-screen bg-white flex"> <div className="max-h-screen bg-white flex">
{/* Left Side - Branding */} {/* Left Side - Branding */}
<div className="hidden lg:flex lg:w-1/2 bg-[var(--primary)] text-white p-12"> <div className="hidden lg:flex lg:w-1/2 bg-[var(--primary)] text-white p-12 lg:h-[100vh]">
<div className="flex flex-col items-center justify-center text-center w-full h-full gap-6"> <div className="flex flex-col items-center justify-center text-center w-full h-full gap-6">
{/* Logos */} {/* Logos */}
<div className="flex flex-col items-center gap-2 space-y-3"> <div className="flex flex-col items-center gap-2 space-y-3">

View File

@ -0,0 +1,77 @@
import { useState } from "react";
import { FaSignature, FaCheckCircle } from "react-icons/fa";
import { MdOutlinePrivacyTip } from "react-icons/md";
interface ConsentScreenProps {
onConsent: () => void;
onDecline?: () => void;
}
export default function ConsentScreen({ onConsent, onDecline }: ConsentScreenProps) {
const [accepted, setAccepted] = useState(false);
console.log("USER ACCEPTED CALLED")
return (
<>
<div className="absolute top-0 left-0 w-[100vw] h-[100vh] bg-slate-100 opacity-50 backdrop-filter backdrop-blur-2xl"></div>
<div className="absolute top-0 left-0 w-full h-screen flex items-center justify-center p-6">
<div className="bg-white shadow-xl rounded-2xl p-8 max-w-lg w-full border border-gray-200">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<FaSignature className="text-blue-600 text-3xl" />
<h2 className="text-2xl font-semibold text-gray-800">
Consent for E-Signature
</h2>
</div>
{/* Info */}
<div className="flex items-start gap-3 mb-6 text-gray-600">
<MdOutlinePrivacyTip className="text-xl text-blue-500 mt-1" />
<p className="text-sm leading-relaxed">
By providing your electronic signature, you agree that it is the legal
equivalent of your manual signature on this document. Please review the
agreement carefully before proceeding.
</p>
</div>
{/* Checkbox */}
<label className="flex items-center gap-3 mb-6 cursor-pointer select-none">
<input
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
className="w-5 h-5 accent-blue-600 border-gray-300 rounded"
/>
<span className="text-sm text-gray-700">
I agree to sign electronically
</span>
</label>
{/* Buttons */}
<div className="flex gap-4 flex-row-reverse">
<button
className={`flex-1 cursor-pointer flex items-center justify-center gap-2 rounded-xl py-2 font-medium transition-colors
${accepted ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-300 text-gray-500 cursor-not-allowed"}
`}
disabled={!accepted}
onClick={onConsent}
>
<FaCheckCircle /> Continue
</button>
{/* {onDecline && (
<button
className="flex-1 border border-red-500 text-red-500 hover:bg-red-50 rounded-xl py-2 font-medium transition-colors"
onClick={onDecline}
>
Decline
</button>
)} */}
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,49 @@
import { FaTimesCircle } from "react-icons/fa";
interface DeclineModalProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
}
export default function DeclineModal({ open, onClose, onConfirm }: DeclineModalProps) {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6 relative">
{/* Header */}
<div className="flex items-center gap-3 mb-4">
<FaTimesCircle className="text-red-500 text-2xl" />
<h2 className="text-lg font-semibold text-gray-800">
Confirm Decline
</h2>
</div>
{/* Message */}
<p className="text-sm text-gray-600 mb-6">
Are you sure you want to decline this document? This action cannot be undone.
</p>
{/* Buttons */}
<div className="flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 rounded-md border border-gray-300 text-gray-600 hover:bg-gray-100"
>
Cancel
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
className="px-4 py-2 rounded-md bg-red-500 text-white hover:bg-red-600"
>
Decline
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,108 @@
import { useState } from "react";
import { FaUserEdit } from "react-icons/fa";
interface ReassignModalProps {
open: boolean;
onClose: () => void;
onConfirm: (data: { firstName: string; lastName: string; email: string }) => void;
}
export default function ReassignModal({ open, onClose, onConfirm }: ReassignModalProps) {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [errors, setErrors] = useState<{ lastName?: string; email?: string }>({});
if (!open) return null;
const validate = () => {
const newErrors: { lastName?: string; email?: string } = {};
if (!lastName.trim()) newErrors.lastName = "Last name is required.";
if (!email.trim()) newErrors.email = "Email is required.";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) newErrors.email = "Enter a valid email.";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = () => {
if (!validate()) return;
onConfirm({ firstName, lastName, email });
onClose();
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div className="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative">
{/* Header */}
<div className="flex items-center gap-3 mb-4">
<FaUserEdit className="text-blue-500 text-2xl" />
<h2 className="text-lg font-semibold text-gray-800">Reassign Document</h2>
</div>
{/* Message */}
<p className="text-sm text-gray-600 mb-4">
Please enter the recipient details to reassign this document.
</p>
{/* Form Fields */}
<div className="space-y-4">
<div className="flex flex-col lg:flex-row lg:gap-4">
<div className="flex-1">
<input
type="text"
placeholder="First Name (optional)"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
</div>
<div className="flex-1">
<input
type="text"
placeholder="Last Name *"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className={`w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 ${
errors.lastName ? "border-red-500 focus:ring-red-400" : "focus:ring-blue-400"
}`}
/>
{errors.lastName && (
<p className="text-red-500 text-xs mt-1">{errors.lastName}</p>
)}
</div>
</div>
<div>
<input
type="email"
placeholder="Email *"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 ${
errors.email ? "border-red-500 focus:ring-red-400" : "focus:ring-blue-400"
}`}
/>
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
</div>
</div>
{/* Buttons */}
<div className="flex justify-end gap-3 mt-6">
<button
onClick={onClose}
className="px-4 py-2 rounded-md border border-gray-300 text-gray-600 hover:bg-gray-100 text-sm"
>
Cancel
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 rounded-md bg-blue-500 text-white hover:bg-blue-600 text-sm"
>
Reassign
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,112 @@
import { useState, useRef, useEffect } from "react";
import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle } from "react-icons/fa";
import { BsThreeDotsVertical } from "react-icons/bs";
interface Props {
transparentTags?: boolean;
hideTags?: boolean;
handleFormSubmit: () => void;
onDownloadClick?: () => void;
onDeclineClick?: () => void;
onReassignClick?: () => void;
}
export default function SignButton({
transparentTags,
hideTags,
handleFormSubmit,
onDownloadClick,
onDeclineClick,
onReassignClick,
}: Props) {
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
// Close menu on outside click
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className="relative inline-block" ref={menuRef}>
{(!transparentTags && !hideTags) ? (
<div className="flex items-center">
{/* Sign Button */}
<button
onClick={handleFormSubmit}
className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-md bg-[var(--primary)] hover:opacity-90 transition text-white outline-none"
>
<span id="sign-client-side-btn" className="tracking-wide">
Sign
</span>
<FaSignature className="text-md" />
</button>
{/* Three Dots Dropdown Trigger */}
<button
onClick={() => setMenuOpen((prev) => !prev)}
className="ml-2 p-2 rounded-full hover:bg-gray-100 focus:outline-none transition"
>
<BsThreeDotsVertical className="text-gray-600 text-lg cursor-pointer" />
</button>
{/* Dropdown Menu */}
{menuOpen && (
<div
tabIndex={0}
onBlur={() => setMenuOpen(false)} // close on blur
className="absolute right-0 top-10 w-56 bg-white border border-gray-200 rounded-lg shadow-lg z-10"
>
<ul className="py-1 text-sm text-gray-700">
<li>
<button
onClick={() => {
onDownloadClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaDownload className="text-blue-500" />
<span>Download Document</span>
</button>
</li>
<li>
<button
onClick={() => {
onReassignClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
>
<FaUserEdit className="text-yellow-500" />
<span>Reassign</span>
</button>
</li>
<li>
<button
onClick={() => {
onDeclineClick?.();
setMenuOpen(false);
}}
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100 text-red-500 border-t border-gray-200"
>
<FaTimesCircle />
<span>Decline</span>
</button>
</li>
</ul>
</div>
)}
</div>
) : (
<p className="text-xs text-green-600 font-medium">Signed Successfully</p>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,806 @@
import React, { useState } from 'react'
import { DroppedItem, TagsTypes } from '../../utlity/types';
import { RiDragMoveFill } from 'react-icons/ri';
import { IoMdClose } from 'react-icons/io';
import maskUtils from '../../utlity/maskUtils';
import { FaFileAlt, FaSignature, FaStamp } from 'react-icons/fa';
import { TbSignature } from 'react-icons/tb';
import { BsFileArrowDownFill } from 'react-icons/bs';
import { handleGroupBorderPosition } from '../../utlity/tagGroupConUtils';
import api from '../../config/api';
import { SaveType, SignConfTypeObj, StampTypeObj } from './SignDocument';
import dayjs from 'dayjs';
import typeToIconMap from '../../helper/iconMap';
interface TagsConProps {
currDocTags: DroppedItem[]
pageIdkey: string;
provideTags: boolean
documentId: string;
pageIndex: number;
hideTags: boolean,
transparentTags: boolean;
focusElement: DroppedItem | null;
selectedTag: { key: string; index: number } | null;
droppedItems: DroppedItem[]
saved: SaveType;
signConfObj: SignConfTypeObj | {};
setStamp: React.Dispatch<React.SetStateAction<StampTypeObj>>;
setSignConfObj: React.Dispatch<React.SetStateAction<SignConfTypeObj | {}>>;
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>,
setSignTagId: React.Dispatch<React.SetStateAction<number>>;
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItem[]>>;
setSelectedTag: React.Dispatch<React.SetStateAction<{ key: string; index: number } | null>>
setFocusElement: React.Dispatch<React.SetStateAction<DroppedItem | null>>
}
const TagsCon: React.FC<TagsConProps> = ({
currDocTags,
pageIdkey,
provideTags,
documentId,
pageIndex,
hideTags,
transparentTags,
focusElement,
selectedTag,
droppedItems,
saved,
signConfObj,
setStamp,
setSignConfObj,
setOpenSign,
setSignTagId,
setDroppedItems,
setSelectedTag,
setFocusElement
}) => {
const [dateValues, setDateValues] = useState<{ [key: string]: string }>({});
const [isDateOpen, setIsDateOpen] = useState<string | null>(null);
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
setSelectedTag({ key, index })
event.preventDefault();
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
let targetElement = event?.currentTarget;
if (!targetContainer || !targetElement) return;
const containerRect = targetContainer.getBoundingClientRect();
const elementRect = targetElement.getBoundingClientRect();
// Calculate the initial offset (where the mouse was clicked inside the element)
const offsetX = event.clientX - elementRect.left;
const offsetY = event.clientY - elementRect.top;
const conScrollWidth = targetContainer?.scrollWidth;
const conScrollHeight = targetContainer?.scrollHeight;
let isGroupAttr = event.currentTarget.hasAttribute("group");
let groupId = null;
if (isGroupAttr) {
groupId = event.currentTarget.getAttribute("group");
}
let groupElements: NodeListOf<HTMLElement> | null = null
if (isGroupAttr && groupId) {
groupElements = document.querySelectorAll(`div[group="${groupId}"]`);
}
const handleMouseMove = (event: MouseEvent) => {
// Get new X and Y positions relative to the container
let left = event.clientX - containerRect.left - offsetX;
let top = event.clientY - containerRect.top - offsetY;
// **Restrict movement within container bounds**
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
if (groupElements != null && groupElements.length) handleGroupBorderPosition(groupElements, containerRect, offsetX, offsetY);
// Apply the movement to the element
setDroppedItems(
droppedItems.map((item, i) =>
i === index ? { ...item, dimensions: { ...item.dimensions, left, top }, update: true } : item
)
);
};
const handleMouseUp = () => {
document.removeEventListener("pointermove", handleMouseMove);
document.removeEventListener("pointerup", handleMouseUp);
};
// Add event listeners
document.addEventListener("pointermove", handleMouseMove);
document.addEventListener("pointerup", handleMouseUp);
};
const handleRemoveTag = (key: string, targetIndex: number) => {
const tag: DroppedItem = droppedItems[targetIndex];
const recipientId = tag.recipientId;
(async () => {
if ("id" in tag) {
try {
const response = await api.delete(`/api/sign/tags?recipientId=${recipientId}`, { data: [{ tagId: tag.id }] })
if (response.status !== 200) throw "Tag Deletion error"
} catch (err) {
return;
}
}
setSelectedTag(null);
// Check if this item has a group for special group handling
if ('group' in tag && tag.group) {
const groupId = tag.group;
// Find all items in the same group
const groupItems = droppedItems[key]
.map((item, idx) => ({ item, index: idx }))
.filter(({ item }) => 'group' in item && item.group === groupId && !item.isDeleted);
// Sort group items by their top position to identify the parent (top-most)
groupItems.sort((a, b) => a.item.dimensions.top - b.item.dimensions.top);
// Check if the item being removed is the parent (top-most item)
const isParentItem = groupItems.length > 0 && groupItems[0].index === targetIndex;
if (isParentItem && groupItems.length > 1) {
// If removing the parent and there are child items, mark all items in the group as deleted
// Get indices of all group items to mark as deleted
const indicesToDelete = groupItems.map(({ index }) => index);
setDroppedItems(prevState => ({
...prevState,
[key]: prevState[key].map((item, index) =>
indicesToDelete.includes(index) ? { ...item, isDeleted: true } : item
)
}));
// Remove group border elements after state update
// setTimeout(() => {
// removeGroupBorders(groupId);
// }, 100);
} else {
// If removing a child item or the only item in group, just mark this item as deleted
setDroppedItems(prevState => ({
...prevState,
[key]: prevState[key].map((item, index) =>
index === targetIndex ? { ...item, isDeleted: true } : item
)
}));
// Check if this was the last item in the group after state update
// setTimeout(() => {
// const remainingGroupItems = droppedItems[key].filter(item =>
// 'group' in item && item.group === groupId && !item.isDeleted
// );
// if (remainingGroupItems.length === 0) {
// // Remove group borders if no items left
// removeGroupBorders(groupId);
// } else {
// // Update group borders for remaining items
// handleIGroupBorderPosition(groupId);
// }
// }, 100);
}
} else {
// Handle non-group item removal (your existing logic)
setDroppedItems(prevState => prevState.filter((item, idx) => {
return idx !== targetIndex;
}));
}
})()
};
const handleTagUpdate = (updateIndx: number, updateValue: string) => {
if (droppedItems && droppedItems.length) {
const updatedTag = droppedItems.map((val, indx) => {
if (indx === updateIndx) {
return {
...val,
value: updateValue
}
}
return val;
})
setDroppedItems(updatedTag);
}
// setTagJson((prev) => {
// const newTagJson = [...prev];
// if (newTagJson[updateIndx]) {
// newTagJson[updateIndx].label = updateValue;
// }
// return newTagJson;
// });
}
const handleSignClick = (index: number, id: number) => {
setSignTagId(id)
if (saved.save) {
if (index in signConfObj) {
setOpenSign(true);
return
};
setSignConfObj({
...signConfObj,
[index]: {
accepted: true,
index: index
}
})
return
}
setOpenSign(true);
const newObj = {
[index]: {
"accepted": false,
index: index
}
}
setSignConfObj({ ...newObj })
}
const handleStampClick = (index: number) => {
setStamp({
open: true,
blob: ""
})
}
const handleSignAcknowledgeClick = (index: number) => {
if (saved.save && signConfObj === null) {
setSignConfObj([...signConfObj, index])
}
}
return (
<>
{currDocTags.map((item, index) => {
if (item.pageIndex !== pageIndex || item.documentId != documentId) return <></>;
return (
<div
tabIndex={index+1}
onFocus={() => {
setSelectedTag({ key: pageIdkey, index })
// setFocusElement({ ...item, focusElementIndx: index })
}
}
key={index}
// 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'}`
}}
>
{provideTags && !transparentTags && !hideTags && <div
onPointerDown={(e) => handleTagMouseDown(e, pageIdkey, index)}
// className="absolute top-[-15px] left-[-15px] w-4 h-4 bg-blue-600 rounded-tr cursor-move z-30"
title="Drag"
>
<RiDragMoveFill
className="absolute top-[-15px] left-[-15px] w-4 h-4 bg-slate-100 text-[var(--primary)] cursor-move z-30"
/>
</div>}
<>
{selectedTag?.key && selectedTag.key === pageIdkey && selectedTag.index === index &&
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm' >
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
{/* <p className='w-2.5 h-2.5 rounded'
style={{ "background": "#2d2d2d" }}
>
</p> */}
{/* <p
className=''>{IconComponent}
</p> */}
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
{provideTags && <IoMdClose onClick={(e) => {
e.stopPropagation();
handleRemoveTag(pageIdkey, index)
}} className='cursor-pointer' />}
</div>
</div>}
</>
{/* TEXT FIELD - START */}
{
item.type === "TEXT" &&
<div
id={item.id}
tabIndex={1}
className={`max-h-max text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
// style={{ height: item.dimensions.height + "px" }}
>
{/* <input
name={item.id}
className="w-full px-2 outline-none"
>
</input> */}
{
!item.multiLine && !item.maskType && <p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onBlur={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
>{item.value ?? ""}</p>
}
{
item.multiLine && !item.maskType && <p
suppressContentEditableWarning
onBlur={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
contentEditable={true}
className="w-full outline-none max-h-max"
>{item.value ?? ""}</p>
}
{
"maskType" in item && "maskCharacter" in item && item.maskType && <p
// onBlur={(e) => {
// const val = e.currentTarget.textContent;
// handleTagUpdate(index, val as string);
// }}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
// onFocus={() => {
// }}
onBlur={(e) => {
let val = e.target.textContent;
handleTagUpdate(index, val as string);
if (typeof item.maskType === "string" && typeof val === "string" && item.maskCharacter) {
val = val.replace(/ /g, "");
e.target.textContent = maskUtils(item.maskType, val, item.maskCharacter)
}
}}
contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
></p>
}
</div>
}
{/* TEXT FIELD - END */}
{/* EMAIL FIELD - START */}
{
item.type === "EMAIL" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onInput={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const val = e.currentTarget.textContent?.trim() || "";
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (val && !emailRegex.test(val)) {
alert("Please enter a valid email address");
handleTagUpdate(index, "" as string)
e.currentTarget.textContent = "";
e.currentTarget.focus(); // Optional: return focus if invalid
}
e.preventDefault()
};
}}
onBlur={(e) => {
const val = e.currentTarget.textContent?.trim() || "";
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (val && !emailRegex.test(val)) {
alert("Please enter a valid email address");
handleTagUpdate(index, "" as string)
e.currentTarget.textContent = "";
e.currentTarget.focus(); // Optional: return focus if invalid
}
}}
// contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
>
{item.value ?? ""}
</p>
}
</div>
}
{/* EMAIL FIELD - END */}
{/* NAME FIELD - START */}
{
item.type === "NAME" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onInput={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
};
}}
onBlur={(e) => {
}}
// contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
>
{item.value ?? ""}
</p>
}
</div>
}
{/* NAME FIELD - END */}
{/* NUMBER FIELD - START */}
{
item.type === "NUMBER" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<input
onInput={(e) => {
const val = e.currentTarget.value;
handleTagUpdate(index, val as string);
}}
type="number"
className="outline-none h-full max-w-max"
/>
}
</div>
}
{/* NUMBER FIELD - END */}
{/* COMPANY FIELD - START */}
{
item.type === "COMPANY" &&
<div
id={item.id}
tabIndex={1}
className={`h-full text-[14px] font-medium w-full border-slate-100 rounded ${(transparentTags || hideTags) ? "pointer-events-nones" : "bg-slate-200 shadow-sm opacity-80"} flex justify-start items-end`}
>
{
<p
// name={item.id}
// style={{ height: item.dimensions.height + "px" }}
onInput={(e) => {
const val = e.currentTarget.textContent;
handleTagUpdate(index, val as string);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
};
}}
onBlur={(e) => {
}}
contentEditable={true}
className="outline-none whitespace-nowrap w-full max-h-max"
></p>
}
</div>
}
{/* COMPANY FIELD - END */}
{/* CHECKBOX FIELD - START */}
{
item.type === "CHECKBOX" &&
<div
id={item.id}
className={`h-full w-full rounded flex justify-center items-center ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
<input
name={item.id}
// style={{ height: item.dimensions.height + "px" }}
className="w-full px-2 outline-none"
type="checkbox" />
</div>
}
{/* CHECKBOX FIELD - END */}
{/* SIGNATURE FIELD - START */}
{
item.type === "SIGNATURE" &&
<div
tabIndex={1}
id={item.id}
onClick={() => handleSignClick(index, item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none bg-transparent" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{/* <input type="checkbox" className="" /> */}
{
(!transparentTags && !hideTags && !(saved.save && (index in signConfObj && signConfObj[index].accepted))) ?
<>
<FaSignature className="text-lg text-slate-600" />
</>
:
<img
style={{
width: `${item.dimensions.width}px`,
height: `${item.dimensions.height}px`,
}}
// src={saved.signature as string} />
src={`data:image/png;base64,${saved.signature}`}
/>
}
</div>
}
{/* SIGNATURE FIELD - END */}
{/* INITIALS FIELD - START */}
{
item.type === "INITIALS" &&
<div
tabIndex={1}
id={item.id}
onClick={() => handleSignClick(index, item.id)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{((!transparentTags && !hideTags) && !(saved.save && (index in signConfObj) && signConfObj[index].accepted)) ?
<TbSignature className="text-lg text-slate-600" />
:
<img
style={{
width: `${item.dimensions.width}px`,
height: `${item.dimensions.height}px`,
}}
className="w-[100%] h-full"
// src={saved.initial as string}
src={`data:image/png;base64,${saved.initial}`}
/>
}
</div>
}
{/* INITIALS FIELD - END */}
{/* STAMP FIELD - START */}
{
item.type === "STAMP" &&
<div
tabIndex={1}
id={item.id}
onClick={() => handleStampClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{!transparentTags && !hideTags ?
<FaStamp className="text-lg text-slate-600" />
:
<img className="w-[100%] h-full" src={saved.initial as string} />
}
</div>
}
{/* STAMP FIELD - END */}
{/* IMAGE FIELD - START */}
{
item.type === "FILE" &&
<div
tabIndex={1}
id={item.id}
// onClick={() => handleSignClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{!transparentTags && !hideTags ?
<FaFileAlt className="text-lg text-slate-600" />
:
<img className="w-[100%] h-full" src={saved.initial as string} />
}
</div>
}
{/* IMAGE FIELD - END */}
{/* SELECT FIELD - START */}
{
item.type === "SELECT" &&
<div
id={item.id}
tabIndex={1}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100"}`}
>
<select
name={item.id}
className="outline-none"
onInput={(e) => {
const val = e.currentTarget.value;
handleTagUpdate(index, val as string);
}}
>
<option value="">Select</option>
{item.options && item.options.map((obj, indx) => {
return (
<option
key={indx}
value={obj.value}
>
{obj.label}
</option>
)
})
}
</select>
</div>
}
{/* SELECT FIELD - END */}
{/* TEMPORAL FIELD - START */}
{
item.type === "TEMPORAL" &&
<div
id={item.id}
tabIndex={1}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100"}`}
>
<p
style={{ opacity: `${item.id === isDateOpen ? "0" : "100"}` }}
className="w-full h-full rounded pl-1 pt-1 whitespace-nowrap">
{(item.id && item.id in dateValues) ? dateValues[item.id] : item.value ? item.value : item.dateFormat?.toLowerCase()}
</p>
<input
name={item.id}
onFocus={(e) => {
e.target.showPicker?.()
if (item.id) setIsDateOpen(item.id)
}}
onBlur={(e) => {
let val = e.target.value;
// Adding dummy time in case of time as it giving Invalid date without that
if (item.dateFormat?.toLowerCase() === "time") val = "09/02/2025 " + val
if (!item.id) return;
setIsDateOpen(null);
setDateValues({
...dateValues,
[item.id as string]: dayjs(val).format(item.format)
})
}}
onInput={(e) => {
const val = e.currentTarget.value;
handleTagUpdate(index, val as string);
}}
style={{ opacity: `${item.id === isDateOpen ? "100" : "0"}` }}
className="w-full px-2 outline-none absolute"
onClick={() => {
}}
type={`${item.dateFormat?.toLowerCase() === "date" ? "date" : (item.dateFormat?.toLowerCase() === "time" ? "time" : "datetime-local")}`}
/>
</div>
}
{/* TEMPORAL FIELD - END */}
{/* Attachment Field - START */}
{
item.type.toLowerCase() === TagsTypes.ATTACHMENT &&
<div
tabIndex={1}
id={item.id}
// onClick={() => handleStampClick(index)}
className={`flex justify-center items-center h-full w-full rounded cursor-pointer ${transparentTags || hideTags ? "pointer-events-none" : "bg-slate-200 shadow-sm border-slate-100 opacity-80"}`}
// style={{ height: item.dimensions.height + "px" }}
>
{!transparentTags && !hideTags ?
(
<p className="text-lg text-slate-600">
{typeToIconMap.ATTACHMENT}
</p>
)
:
(
<p className="text-lg text-slate-600">
{typeToIconMap.ATTACHMENT}
</p>
)
}
</div>
}
{/* Attachment Field - END */}
{/* Focus Show Element */}
{
(!transparentTags && !hideTags) && (focusElement && focusElement.documentId === item.documentId && focusElement.focusElementIndx === index) &&
<>
{/* <div
className="absolute left-0 z-50 bg-blue-900 text-xs text-slate-100 px-2 py-[2px] rounded-sm whitespace-nowrap"
style={{ top: -23 + "px" }}
>
<p>{focusElement.label}</p>
</div> */}
<div
className="absolute left-[-10px] z-50 flex max-w-max"
style={{ left: -(item.dimensions.left + 20) + "px", top: ((item.dimensions.height / 2) - 18) + "px" }}
>
{/* <FaArrowRight className="text-[var(--primary)] text-xl" />
*/}
<BsFileArrowDownFill className="text-blue-900 text-4xl rotate-270" />
</div>
</>
}
</div>
)
})}
</>
)
}
export default TagsCon

View File

@ -11,11 +11,9 @@ import { clearTransaction, TRANSACTION_STATUS_ENUMS } from '../../store/slices/t
import { resetSelDoc } from '../../store/slices/selectedDocsSlice' import { resetSelDoc } from '../../store/slices/selectedDocsSlice'
import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice' import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice'
import { hideLoader, showLoader } from '../../store/slices/loaderSlice' import { hideLoader, showLoader } from '../../store/slices/loaderSlice'
import { Bounce, toast } from 'react-toastify'
import SignModal from '../modal/SignModal' import SignModal from '../modal/SignModal'
import confetti from 'canvas-confetti' import confetti from 'canvas-confetti'
import { useWindowSize } from 'react-use'
import { FaCheckCircle } from 'react-icons/fa' import { FaCheckCircle } from 'react-icons/fa'
import { IoClose } from 'react-icons/io5' import { IoClose } from 'react-icons/io5'
import CommonNav from '../CommonNav' import CommonNav from '../CommonNav'
@ -39,13 +37,6 @@ function Dashboard() {
// Check if cookie already exists - Redirect to Dashboard // Check if cookie already exists - Redirect to Dashboard
useLayoutEffect(() => { useLayoutEffect(() => {
// if (!user.authorized) {
// window.location.href = '/login';
// return;
// }
// console.log("OUT MIC CHECK", transactionStatus, TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS, TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED);
// Clear all if transaction success
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_FAILURE) { if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_FAILURE) {
dispatch(resetSelDoc()) dispatch(resetSelDoc())
@ -54,22 +45,6 @@ function Dashboard() {
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) { if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) {
setShowConfetti(true) setShowConfetti(true)
// setTimeout(() => {
// setShowConfetti(false)
// dispatch(hideLoader())
// toast.dismiss();
// toast.info("Transaction Send Successfully!", {
// position: "bottom-right",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
// }, 1000)
} }
return return

View File

@ -1,6 +1,8 @@
import { LuSignature } from "react-icons/lu"; import { LuSignature } from "react-icons/lu";
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';
import SMELogo from "../../assets/SMELogo2.png";
import { FaFileSignature } from "react-icons/fa";
const Loader = () => { const Loader = () => {
@ -16,7 +18,7 @@ const Loader = () => {
{/* Loader */} {/* Loader */}
<div className="absolute z-50 w-screen h-screen flex items-center justify-center backdrop-blur-sm bg-opacity-50 bg-black/30"> <div className="absolute z-50 w-screen h-screen flex items-center justify-center backdrop-blur-sm bg-opacity-50 bg-black/30">
<div className='bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg'> <div className='bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg'>
<LuSignature className="text-4xl font-bold animate-text" /> <FaFileSignature className="text-4xl font-bold animate-text" />
<p> <p>
{loaderData.message != undefined ? `${loaderData.message}` : 'Loading ...'} {loaderData.message != undefined ? `${loaderData.message}` : 'Loading ...'}
</p> </p>

View File

@ -1,13 +1,9 @@
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { IoMdArrowDropdown } from "react-icons/io";
import { IoIosSend } from "react-icons/io"; import { IoIosSend } from "react-icons/io";
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { setSignType, SignType } from '../../store/slices/selectedDocsSlice';
import { RootState } from '../../store/store'; import { RootState } from '../../store/store';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Bounce, toast } from 'react-toastify';
import api from '../../config/api'; import api from '../../config/api';
import { StartCard } from '../home/StartSection';
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice'; import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice';
import { SignTypeEnum } from '../../utlity/types'; import { SignTypeEnum } from '../../utlity/types';
// import { IoMdArrowDropright } from "react-icons/io"; // import { IoMdArrowDropright } from "react-icons/io";
@ -120,7 +116,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
px-2 py-2.5 px-2 py-2.5
overflow-hidden overflow-hidden
transition-all duration-300 ease-in-out w-full transition-all duration-300 ease-in-out w-full
${showSideBar ? "rounded-l" : "rounded"} rounded
`} `}
style={{ whiteSpace: "nowrap" }} style={{ whiteSpace: "nowrap" }}
> >
@ -131,7 +127,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
)} )}
</div> </div>
{showSideBar && ( {/* {showSideBar && (
<div <div
className="rounded-r btn px-2 py-2 bg-[var(--sidebarDropDownBg)] transition-all duration-300 ease-in-out flex items-center justify-center" className="rounded-r btn px-2 py-2 bg-[var(--sidebarDropDownBg)] transition-all duration-300 ease-in-out flex items-center justify-center"
style={{ opacity: showSideBar ? 1 : 0 }} style={{ opacity: showSideBar ? 1 : 0 }}
@ -142,7 +138,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
}`} }`}
/> />
</div> </div>
)} )} */}
</div> </div>

View File

@ -33,9 +33,10 @@ import confetti from 'canvas-confetti'
import { IoClose } from "react-icons/io5"; import { IoClose } from "react-icons/io5";
import { IoMdClose, IoMdHome } from "react-icons/io"; import { IoMdClose, IoMdHome } from "react-icons/io";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import NavTwo from '../processStepTwo/NavTwo';
type Signer = { export type Signer = {
id: number; id: number;
name: string; name: string;
email: string; email: string;
@ -44,7 +45,7 @@ type Signer = {
}; };
// Signers List Component // Signers List Component
const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => { export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
return ( return (
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 "> // <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 ">
<div className="p-2 "> <div className="p-2 ">
@ -198,6 +199,7 @@ const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
// </div> // </div>
); );
}; };
const DocumentSubmissionOptions = () => { const DocumentSubmissionOptions = () => {
const location = useLocation(); const location = useLocation();
const [status, setStatus] = useState(null); const [status, setStatus] = useState(null);
@ -224,18 +226,6 @@ const DocumentSubmissionOptions = () => {
}, []) }, [])
useEffect(() => { useEffect(() => {
// toast.dismiss();
// toast.success("Your document has been successfully sent for signature.", {
// position: "top-center",
// autoClose: 1500,
// hideProgressBar: false,
// closeOnClick: false,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
if (showConfetti) { if (showConfetti) {
const duration = 3000; const duration = 3000;
@ -314,25 +304,6 @@ const DocumentSubmissionOptions = () => {
} }
}, [status]); }, [status]);
// useEffect(() => {
// if (status === 'SENT') {
// // Step 1: Push a dummy state to trap the user
// window.history.pushState(null, '', window.location.href);
// // Step 2: Intercept back button
// const handlePopState = (event) => {
// // Push the same dummy state again to block back
// window.history.pushState(null, '', window.location.href);
// };
// window.addEventListener('popstate', handlePopState);
// return () => {
// window.removeEventListener('popstate', handlePopState);
// };
// }
// }, [status]);
const [selectedOption, setSelectedOption] = useState('success'); const [selectedOption, setSelectedOption] = useState('success');
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [previewData, setPreviewData] = useState({ const [previewData, setPreviewData] = useState({
@ -440,95 +411,6 @@ const DocumentSubmissionOptions = () => {
</div> </div>
<div className={`max-w-4xl mx-auto transition-all bg-white rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}> <div className={`max-w-4xl mx-auto transition-all bg-white rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
{/* Header */}
{/* <div className="grid grid-cols-1 lg:grid-cols-2 gap-3 max-w-5xl mx-auto">
<div className="space-y-6 ">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 min-h-[81vh] ">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
Choose an action after a signer completes the document.
</h3>
<div className="space-y-3">
{options.map((option) => {
const Icon = option.icon;
const isSelected = selectedOption === option.id;
return (
<div
key={option.id}
onClick={() => setSelectedOption(option.id)}
className={`
relative cursor-pointer p-4 rounded-xl border-2 transition-all duration-200 group
${isSelected
? 'border-[var(--primary)] bg-blue-50 shadow-md scale-[1.02]'
: option.color + ' border-2'
}
hover:shadow-md hover:scale-[1.01] transform
`}
>
<div className="flex items-start space-x-4">
<div className={`
w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200
${isSelected
? 'bg-[var(--primary)] text-white shadow-lg'
: 'bg-white ' + option.iconColor + ' group-hover:scale-110'
}
`}>
<Icon className="text-lg" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<h4 className={`font-semibold transition-colors duration-200 ${isSelected ? 'text-[var(--primary)]' : 'text-gray-800'
}`}>
{option.title}
</h4>
{option.isPremium && (
<span className="text-purple-500 text-sm">★</span>
)}
</div>
<p className="text-sm text-gray-600 mt-1">
{option.description}
</p>
</div>
</div>
{isSelected && (
<div className="absolute top-4 right-4">
<div className="w-6 h-6 bg-[var(--primary)] rounded-full flex items-center justify-center">
<FaCheck className="text-white text-xs" />
</div>
</div>
)}
</div>
);
})}
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden sticky top-6">
<div className="bg-gradient-to-r from-gray-50 to-blue-50 px-6 py-2 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-800">Live Preview</h3>
<p className="text-sm text-gray-500">See how your users will experience the completion</p>
</div>
<div className="p-3 min-h-[70vh] flex justify-center">
<div className="w-full transition-all duration-500 transform">
{getPreviewContent()}
</div>
</div>
</div>
</div>
</div> */}
<div className="text-center pt-1 max-h-[25vh]"> <div className="text-center pt-1 max-h-[25vh]">
<div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg "> <div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg ">
<FaCheck className="text-white text-4xl" /> <FaCheck className="text-white text-4xl" />

View File

@ -0,0 +1,51 @@
import { FaCheck, FaClock, FaUsers } from "react-icons/fa";
import { Signer } from "./DocumentCompletionScreen";
export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
return (
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 ">
<div className="p-4 bg-slate-100 rounded-sm shadow-sm">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-[var(--primary)] flex items-center gap-2 ">
{/* <FaUsers className="text-blue-600" /> */}
Recipients
</h3>
<span className="text-sm text-gray-600">
{signers.filter(s => s.completed).length} of {signers.length} signed
</span>
</div>
<div className="max-h-[24vh] overflow-y-auto no-scrollbar">
{signers.map((signer) => (
<div key={signer.id} className="text-sm flex items-center gap-4 rounded-lg">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
{/* {signer.name.split(" ").length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.name[0].toUpperCase()} */}
{signer.firstName && signer.firstName[0].toUpperCase()}
{signer.lastName && signer.lastName[0].toUpperCase()}
</div>
<div className="flex-1">
<div className="font-medium text-gray-800">{signer.name}</div>
<div className="text-xs text-gray-600">{signer.email}</div>
<div className="text-xs text-gray-500">Order: {signer.routingOrder}</div>
</div>
<div className="flex items-center gap-2">
{signer.completed ? (
<div className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs">
<FaCheck className="w-3 h-3" />
Signed
</div>
) : (
<div className="flex items-center gap-1 px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-xs">
<FaClock className="w-3 h-3" />
Pending
</div>
)}
</div>
</div>
))}
</div>
</div>
// </div>
);
};

View File

@ -0,0 +1,119 @@
import React from "react";
import { FaCheck, FaClock, FaEnvelope, FaLock } from "react-icons/fa";
type Signer = {
id: number;
name: string;
firstName: string;
email: string;
routingOrder: number;
completed: boolean;
};
export const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
const groupByRoutingOrder = (data: Signer[]) => {
const grouped: Record<number, Signer[]> = {};
data.forEach((signer) => {
if (!grouped[signer.routingOrder]) {
grouped[signer.routingOrder] = [];
}
grouped[signer.routingOrder].push(signer);
});
return grouped;
};
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0].toUpperCase())
.join("");
};
const grouped = groupByRoutingOrder(signers);
const sortedOrders = Object.keys(grouped)
.map(Number)
.sort((a, b) => a - b);
const currentProcessingOrder =
sortedOrders.find((order) =>
grouped[order].some((signer) => !signer.completed)
) || -1;
return (
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100">
<div className="py-3">
<h3 className="text-lg font-semibold text-gray-800 mb-1 text-center ">
Signing Flow</h3>
<div className="overflow-x-auto overflow-y-auto h-[18vh] no-scrollbar flex justify-center items-center">
<div className="flex justify-center items-center gap-2 min-w-max">
{/* Mail icon */}
<div className="flex flex-col items-center">
<div className="bg-blue-100 p-4 rounded-full">
<FaEnvelope className="text-blue-600 text-2xl" />
</div>
<p className="text-sm text-gray-600 mt-2">Sent</p>
</div>
{/* Dotted line */}
<div className="flex items-center mb-3">
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
</div>
{/* Signers by routing order */}
<div className="flex items-center justify-center space-x-6">
{sortedOrders.map((order, index) => (
<React.Fragment key={order}>
<div className="flex flex-col items-center space-y-4 mb-4">
<div className="text-sm font-medium text-gray-600 mb-2">
Step {order}
</div>
<div className="flex flex-col items-center space-y-3">
{grouped[order].map((signer) => {
let bgColor = "";
let icon = null;
if (signer.completed) {
bgColor = "bg-[var(--primary)]";
icon = <FaCheck className="text-green-600" />;
} else if (order === currentProcessingOrder) {
bgColor = "bg-yellow-500";
icon = <FaClock className="text-yellow-800" />;
} else {
bgColor = "bg-gray-400";
icon = <FaLock className="text-gray-500" />;
}
return (
<div key={signer.id} className="relative">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`}
title={signer.name}
>
{getInitials(signer.name)}
<span className="absolute bottom-[-4px] right-[-8px] p-1 rounded-full bg-white shadow">
{icon}
</span>
</div>
<p className="mt-1 text-xs text-gray-600 text-center max-w-16 truncate">
{signer.name.split(" ")[0] || signer.firstName.split(0, 1)}
</p>
</div>
);
})}
</div>
</div>
{index < sortedOrders.length - 1 && (
<div className="flex items-center">
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
</div>
)}
</React.Fragment>
))}
</div>
</div>
</div>
</div>
// </div>
);
};

View File

@ -0,0 +1,153 @@
import { useEffect, useState } from 'react'
import DocumentContainer from '../processStepTwo/DocumentContainer'
import { Link, useParams } from 'react-router-dom'
import { IoMdHome } from 'react-icons/io'
import { FaCheck } from 'react-icons/fa'
import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from '../../../store/store'
import { fetchRecipients, recipientType } from '../../../store/slices/selectedRecipientsSlice'
import { fetchDocuments } from '../../../store/slices/documentSlice'
import { SigningOrderDiagram } from './SigningOrderDiagram'
import { SignersList } from './SignerList'
import { IoCheckmarkDoneCircle } from 'react-icons/io5'
const TransactionSentSuccess = () => {
const [isVisible, setIsVisible] = useState(false);
const [isSignNow, setIsSignNow] = useState(false);
const dispatch = useDispatch<AppDispatch>();
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
const userDetails = useSelector((state: RootState) => state.user);
const [selfUserSign, setSelfUserSign] = useState({ sign: false, recipientId: 0 });
if (!accountId || !transactionId) {
window.location.href = "/";
return null;
}
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
// const [transactionData, setTransactionData] = useState({})
useEffect(() => {
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
setIsVisible(true)
}, [])
useEffect(() => {
if (selectedRecipients.length === 0) return;
let minRoutingOrder: number = Infinity;
let userEmailPresent = {
present: false,
routingOrder: 0,
recipientId: 0,
type: recipientType.CC
};
// Getting Minimum RoutingOrder
for (let i = 0; i < selectedRecipients.length; i++) {
const recipient = selectedRecipients[i]
if (recipient.routingOrder < minRoutingOrder) {
minRoutingOrder = recipient.routingOrder;
}
if (recipient.email === userDetails.email) {
userEmailPresent.present = true;
userEmailPresent.routingOrder = recipient.routingOrder;
userEmailPresent.recipientId = Number(recipient.id);
userEmailPresent.type = recipient.type;
}
}
if (userEmailPresent.present && userEmailPresent.routingOrder === minRoutingOrder && userEmailPresent.type !== recipientType.CC && userEmailPresent.type !== recipientType.INPERSONSIGNER) {
setSelfUserSign({ sign: true, recipientId: userEmailPresent.recipientId })
}
}, [selectedRecipients])
return (
<>
<div className='w-full'>
<div className="flex items-center space-x-4 bg-white w-full py-2 px-5 shadow">
<Link to={'/'} className="flex items-center space-x-2 text-gray-600 hover:text-[var(--primary)] transition-colors duration-200 group">
<IoMdHome
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
/>
</Link>
<div className="h-8 w-px bg-gray-300"></div>
<h1 className="text-md text-gray-800">🎉 Awesome! The document’s been sent for signature. 🎉</h1>
</div>
<div className='flex flex-col p-4 justify-center items-center w-full'>
<div className='mx-auto max-w-max'>
<div className='flex items-center gap-4 mb-3 ml-0 bg-slate-100 p-4 rounded-md'>
<div className="w-12 h-12 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center shadow-sm">
<FaCheck className="text-white text-2xl" />
</div>
<div>
<p className='text-2xl font-medium text-[var(--primary)]'>Your Document is send for signature</p>
<p className='text-sm text-slate-600'>All signers will be notified via email</p>
</div>
</div>
<div className='flex gap-4'>
<div className='pb-4 bg-slate-100'>
<div className='w-full overflow-auto h-[calc(100vh-200px)]'>
<div className='py-2 bg-white pl-4 pr-4 shadow-sm rounded-sm border-1 border-slate-200 sticky top-0 w-full z-1'>
<p className='text-sm font-medium'>QuickPreview</p>
</div>
{/* <NavTwo isPreview={true} aId={accountId} tId={transactionId} /> */}
<div className='pl-4 pr-4'>
<DocumentContainer isPreview={true} aId={accountId} tId={transactionId} />
</div>
</div>
</div>
{/* Right Side */}
<div className={`transition-all rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
{/* <div className="text-center pt-1 max-h-[25vh]">
<div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg ">
<FaCheck className="text-white text-4xl" />
</div>
</div> */}
{/* <div className='text-center py-1'>
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
</div> */}
<div className='max-h-[30vh]'>
<SignersList signers={selectedRecipients} />
</div>
<div className='mt-4 flex flex-col gap-2'>
{selfUserSign.sign && <Link to={`/sign/${selfUserSign.recipientId}`} className={`p-2 text-center rounded-md bg-[var(--primary)] text-white hover:shadow-sm transition-all duration-150`}>Sign Now</Link>}
<Link to={"/"} className={`p-2 text-center rounded-md bg-slate-200 hover:shadow-sm transition-all duration-150`}>Home</Link>
</div>
{/* <div className='border border-b border-gray-50'></div> */}
{/* <div className='min-h-[30vh]'>
<SigningOrderDiagram signers={selectedRecipients} />
</div> */}
{/* <div className='border border-b border-gray-50'></div> */}
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default TransactionSentSuccess

View File

@ -0,0 +1,12 @@
import React from 'react'
import NavTwo from '../processStepTwo/NavTwo'
const FinalPreview = () => {
return (
<>
</>
)
}
export default FinalPreview

View File

@ -0,0 +1,114 @@
import { useState } from "react";
import { FiLock } from "react-icons/fi";
export default function AdvancedOptions({setShowAdvancedOptions}) {
const [autoReminders, setAutoReminders] = useState(false);
const [expireDays, setExpireDays] = useState(120);
const [alertDays, setAlertDays] = useState(0);
return (
<div className="w-full h-screen bg-white flex flex-col">
{/* Header */}
<div className="flex justify-between items-center border-b px-6 py-4">
<h2 className="text-xl font-semibold">Advanced Options</h2>
<button
onClick={() => setShowAdvancedOptions(false)}
className="text-gray-500 hover:text-black">✕</button>
</div>
{/* Content */}
<div className="flex flex-1 overflow-hidden">
{/* Sidebar */}
<div className="w-60 border-r px-6 py-4 space-y-4 text-sm font-medium text-gray-700">
<div className="cursor-pointer hover:text-black">Recipient Privileges</div>
<div className="cursor-pointer hover:text-black">Reminders</div>
<div className="cursor-pointer hover:text-black">Expiration</div>
</div>
{/* Main Content */}
<div className="flex-1 px-8 py-6 overflow-y-auto space-y-10">
{/* Recipient Privileges */}
<div>
<h3 className="text-lg font-semibold mb-3">Recipient Privileges</h3>
<p className="text-sm text-gray-600 mb-4">Give recipients options for how they sign.</p>
<div className="flex flex-col gap-2">
{/* <label className="flex items-center gap-2 text-gray-700">
<input type="checkbox" checked readOnly className="accent-purple-600" />
Recipients can sign on paper
</label> */}
<label className="flex items-center gap-2 text-gray-400">
<input type="checkbox" disabled className="accent-purple-600" />
Recipients can change signing responsibility or assign a delegate
</label>
</div>
<FiLock className="text-gray-400 mt-2" />
</div>
{/* Reminders */}
<div>
<h3 className="text-lg font-semibold mb-3">Reminders</h3>
<p className="text-sm text-gray-600 mb-4">
Follow up with automatic reminders. Signers will receive emails until they sign or decline the envelope.
</p>
<div>
<button
onClick={() => setAutoReminders(!autoReminders)}
className={`relative w-14 h-7 flex items-center rounded-full p-1 transition-colors duration-200 ${autoReminders ? "bg-purple-600" : "bg-gray-300"
}`}
>
<span
className={`bg-white w-5 h-5 rounded-full shadow-md transform transition-transform duration-200 ${autoReminders ? "translate-x-7" : "translate-x-0"
}`}
/>
</button>
<span className="ml-3 text-sm">Turn on auto reminders</span>
</div>
</div>
{/* Expiration */}
<div>
<h3 className="text-lg font-semibold mb-3">Expiration</h3>
<p className="text-sm text-gray-600 mb-4">
By default, envelopes expire after 120 days. Recipients can no longer view or sign an envelope after it expires.
</p>
<div className="flex items-center gap-4 mb-3">
<select className="border rounded px-3 py-2 text-sm">
<option>Custom days</option>
</select>
<input
type="number"
value={expireDays}
onChange={(e) => setExpireDays(Number(e.target.value))}
className="border rounded px-3 py-2 text-sm w-20"
/>
</div>
<p className="text-sm text-gray-600 mb-6">
Envelopes will expire on: <span className="font-medium">1/3/2026</span>
</p>
<div className="flex items-center gap-4">
<select className="border rounded px-3 py-2 text-sm">
<option>Custom days</option>
</select>
<input
type="number"
value={alertDays}
onChange={(e) => setAlertDays(Number(e.target.value))}
className="border rounded px-3 py-2 text-sm w-20"
/>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="border-t p-4 flex justify-end">
<button className="bg-purple-600 text-white px-6 py-2 rounded hover:bg-purple-700 transition">
Save
</button>
</div>
</div>
);
}

View File

@ -18,6 +18,7 @@ import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from ".
import { SignTypeEnum } from "../../../utlity/types"; import { SignTypeEnum } from "../../../utlity/types";
import { RiPencilFill } from "react-icons/ri"; import { RiPencilFill } from "react-icons/ri";
import SelfSignWarnModal from "./SelfSignWarnModal"; import SelfSignWarnModal from "./SelfSignWarnModal";
import AdvancedOptions from "./AdvancedOptions";
const ProcessStepOne = () => { const ProcessStepOne = () => {
interface EmailDtls { interface EmailDtls {
@ -25,10 +26,29 @@ const ProcessStepOne = () => {
message: string; message: string;
} }
interface FormSubmitInt {
documents: boolean,
recipients: boolean,
subject: boolean,
recipientEmailAndLastName: boolean,
duplicateEmailSameRoutingOrder: boolean
}
const FormSubmitIntitialState = {
documents: false,
recipients: false,
subject: false,
recipientEmailAndLastName: false,
duplicateEmailSameRoutingOrder: false
}
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const signType = useParams<{ signType: string }>().signType; const signType = useParams<{ signType: string }>().signType;
const templateId = useParams<{ templateId: string }>().templateId; const templateId = useParams<{ templateId: string }>().templateId;
const [submitError, setSubmitError] = useState<FormSubmitInt>({ ...FormSubmitIntitialState })
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(false)
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>(); let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -102,6 +122,12 @@ const ProcessStepOne = () => {
dispatch(hideLoader()); dispatch(hideLoader());
}, []) }, [])
useEffect(() => {
if (submitError.duplicateEmailSameRoutingOrder) setSubmitError(prev => ({ ...prev, duplicateEmailSameRoutingOrder: false }))
if (submitError.documents) setSubmitError(prev => ({ ...prev, documents: false }))
if (submitError.recipients) setSubmitError(prev => ({ ...prev, recipients: false }))
}, [selectedRecipients])
const getMinRoutingOrder = () => { const getMinRoutingOrder = () => {
return selectedRecipients.reduce((min, recipient) => { return selectedRecipients.reduce((min, recipient) => {
@ -112,11 +138,20 @@ const ProcessStepOne = () => {
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const handleNextClick = async (saveAndClose: boolean = false) => { const handleNextClick = async (saveAndClose: boolean = false) => {
let formError = { let formError = { ...FormSubmitIntitialState }
documents: false,
recipients: false, const recipientsData: { [key: string | number]: string[] } = {}
subject: false, for (const recipient of selectedRecipients) {
recipientEmailAndLastName: false if (recipientsData[recipient.routingOrder] && recipientsData[recipient.routingOrder].includes(recipient.email)) {
formError.duplicateEmailSameRoutingOrder = true;
break;
}
if (recipientsData[recipient.routingOrder]) {
recipientsData[recipient.routingOrder] = [...recipientsData[recipient.routingOrder], recipient.email]
} else {
recipientsData[recipient.routingOrder] = [recipient.email]
}
} }
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) { if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
@ -143,29 +178,12 @@ const ProcessStepOne = () => {
if (SignTypeEnum.selfSign === signType) formError.recipients = false; if (SignTypeEnum.selfSign === signType) formError.recipients = false;
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName) { if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName || formError.duplicateEmailSameRoutingOrder) {
toast.dismiss(); setSubmitError(formError)
toast.error(`${formError.documents ? "Add Document" : ""}
${(formError.documents && formError.recipients) ? " | " : ""}
${formError.recipients ? "Add Recipient" : ""}
${formError.recipientEmailAndLastName ? "All recipients must have both Email and Last Name." : ""}
${(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; return;
} }
} }
try { try {
dispatch(showLoader("Processing Documents...")); dispatch(showLoader("Processing Documents..."));
@ -188,12 +206,7 @@ const ProcessStepOne = () => {
dispatch(showLoader("Processing Recipients...")); dispatch(showLoader("Processing Recipients..."));
let minRoutingOrder = getMinRoutingOrder(); let minRoutingOrder = getMinRoutingOrder();
// selectedRecipients.forEach((recipient) => {
// if (recipient.routingOrder ?? 1 < minRoutingOrder) {
// minRoutingOrder = recipient.routingOrder ?? 1;
// }
// });
const recipientsData = selectedRecipients.map((recipient, index) => ({ const recipientsData = selectedRecipients.map((recipient, index) => ({
id: "id" in recipient ? recipient.id : null, id: "id" in recipient ? recipient.id : null,
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1, routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
@ -267,7 +280,7 @@ const ProcessStepOne = () => {
} }
} else { } else {
if(!accountId || !transactionId) return; if (!accountId || !transactionId) return;
dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId })); dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId }));
} }
dispatch(showLoader("Processing Message...")); dispatch(showLoader("Processing Message..."));
@ -303,8 +316,6 @@ const ProcessStepOne = () => {
navigate(`/process-step-two/${accountId}/${transactionId}`); navigate(`/process-step-two/${accountId}/${transactionId}`);
} }
} }
// }, 1000);
// }
} catch (error) { } catch (error) {
dispatch(hideLoader()); dispatch(hideLoader());
@ -340,55 +351,82 @@ const ProcessStepOne = () => {
const globalLoader = useSelector((state: RootState) => state.loader) const globalLoader = useSelector((state: RootState) => state.loader)
if (showAdvancedOptions) {
return (
<AdvancedOptions setShowAdvancedOptions={setShowAdvancedOptions} />
)
}
return ( return (
<> <>
<div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}> <div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}>
{/* Top Navigation - Fixed */} {/* Top Navigation - Fixed */}
<div className='fixed top-0 left-0 right-0 w-full flex items-center justify-between bg-white shadow z-50 px-4 py-3'> <div className='fixed top-0 left-0 right-0 w-full bg-white shadow z-50 px-4 py-1.5 lg:py-3'>
<div className='flex items-center gap-2'> <div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 flex-wrap'>
<IoMdClose
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 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 {/* Left Section */}
id="add-tags-btn-step-one" <div className='flex sm:flex-row items-start sm:items-center gap-2 sm:gap-4 w-full sm:w-auto'>
className='flex items-center gap-3'>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <button <IoMdClose
onClick={() => handleNextClick(true)} onClick={handleClose}
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer" className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition self-start sm:self-auto'
> />
Save & Close
</button>}
<button <p className="text-base font-normal text-gray-700 border-l pl-4 relative w-full sm:w-auto">
onClick={() => handleNextClick(false)} {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? (
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer" <>
> <input
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"} value={templateName}
</button> onChange={handleChange}
className="w-full sm:w-[250px] 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>
{/* Right Section */}
<div
id="add-tags-btn-step-one"
className='flex sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3 w-full sm:w-auto '>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<button
onClick={() => handleNextClick(true)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
>
Save & Close
</button>
)}
<button
onClick={() => setShowAdvancedOptions(true)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded text-slate-800 border shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
>
Advanced Options
</button>
<button
onClick={() => handleNextClick(false)}
className="text-sm w-full sm:w-auto px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
>
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
</button>
</div>
</div> </div>
</div> </div>
{/* Main Content - Scrollable */} {/* Main Content - Scrollable */}
<div className="h-screen pt-[80px] overflow-y-auto no-scrollbar"> <div className="h-screen pt-[80px] overflow-y-auto no-scrollbar">
<div className="max-w-4xl rounded-xl shadow-md mx-auto px-6 pb-10 bg-[var(--documentUploadBg)] mb-5"> <div className="max-w-4xl rounded-xl shadow-md mx-auto px-6 pb-10 bg-[var(--documentUploadBg)] mb-5">
@ -396,7 +434,10 @@ const ProcessStepOne = () => {
{/* Documents */} {/* Documents */}
<section> <section>
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2> <h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} /> <div id="documents_con" className={`${submitError.documents ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.documents && "At least one document is required."}</p>
</div>
</section> </section>
{/* Recipients */} {/* Recipients */}
@ -422,13 +463,26 @@ const ProcessStepOne = () => {
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) && (signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
<div className="flex flex-col gap-4 align-start"> <div className="flex flex-col gap-4 align-start">
<RecipientCards userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} editRecipient={editRecipient} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} editRecipientCurrentEmail={editRecipientCurrentEmail} /> <div id="recipient_cards_con" className={`${submitError.duplicateEmailSameRoutingOrder ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
<RecipientCards
userEmail={userData.email}
getMinRoutingOrder={getMinRoutingOrder}
transactionTemplateToggle={transactionTemplateToggle}
setEditRecipient={setEditRecipient}
setEditCase={setEditCase}
editRecipient={editRecipient}
setEditRecipientCurrentEmail={setEditRecipientCurrentEmail}
editRecipientCurrentEmail={editRecipientCurrentEmail}
/>
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.duplicateEmailSameRoutingOrder && "Each Signing order must have unique emails."}</p>
</div>
<div <div
ref={topRef} ref={topRef}
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''}`} className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''} ${submitError.recipients ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}
> >
<RecipientAddCard userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} /> <RecipientAddCard userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.recipients && "Each Signing order must have unique emails."}</p>
</div> </div>
</div> </div>
@ -440,16 +494,21 @@ const ProcessStepOne = () => {
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2> <h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
{/* Subject */} {/* Subject */}
<div className='flex flex-col mb-4'> <div className={`${submitError.subject ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""} flex flex-col mb-4`}>
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label> <label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
<input <input
id="subject"
type="text" type="text"
required required
value={emailDtls.subject} value={emailDtls.subject}
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })} onChange={(e) => {
setEmailDtls({ ...emailDtls, subject: e.target.value })
submitError.subject && setSubmitError(prev => ({ ...prev, subject: false }))
}}
placeholder="Email Subject" placeholder="Email Subject"
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]" className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/> />
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.subject && "Enter a subject."}</p>
</div> </div>
{/* Message */} {/* Message */}

View File

@ -363,27 +363,26 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<form <form
ref={formRef} ref={formRef}
onSubmit={handleRecipientAdd} onSubmit={handleRecipientAdd}
id="add-recipient-step-one" id="add-recipient-step-one"
className="w-full bg-white border border-gray-200 rounded-lg shadow-sm px-6 py-4 space-y-3 relative" className="w-full bg-white border border-gray-200 rounded-lg shadow-sm px-3 sm:px-4 lg:px-6 py-3 sm:py-4 space-y-3 sm:space-y-4 relative"
> >
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && ( {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="space-y-1"> <div className="space-y-2 sm:space-y-3">
{/* Top Row: Role Input + Settings */} {/* Role Name and Permissions Section - Responsive Layout */}
<div className="flex justify-between items-center"> <div className="flex flex-col lg:flex-row lg:justify-between lg:items-start gap-3 lg:gap-4">
{/* Role Name Input */} {/* Role Name Input */}
{/* Role Name Input */} <div className="w-full lg:w-64 xl:w-80">
<div className="w-64">
<input <input
type="text" type="text"
value={recipient.roleName} value={recipient.roleName}
placeholder="Role name" placeholder="Role name"
className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none transition 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)]'}`} ${errors.roleName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => { onChange={(e) => {
setRecipients({ ...recipient, roleName: e.target.value }); setRecipients({ ...recipient, roleName: e.target.value });
setErrors(prev => ({ ...prev, roleName: false })); setErrors(prev => ({ ...prev, roleName: false }));
@ -394,125 +393,76 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
)} )}
</div> </div>
{/* Permissions Section */}
{/* Settings + Permissions */} <div className="flex items-center justify-start lg:justify-end">
<div className="flex items-center"> <div className="flex sm:flex-row items-start sm:items-center gap-2 sm:gap-3">
<div className="flex justify-between w-full"> {/* Edit Permission */}
{/* Permission Status Text - Always Visible */} <div className="relative group">
<div className="max-h-10 opacity-100 me-3 overflow-hidden"> <label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<div className="text-sm text-gray-600 pt-0.5"> <input
{/* {recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Full permissions granted"} type="checkbox"
{recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "Edit permission granted"} checked={recipient.allowSenderToEdit}
{!recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Delete permission granted"} onChange={(e) =>
{!recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "No permissions granted"} */} setRecipients({ ...recipient, allowSenderToEdit: e.target.checked })
Permissions granted : }
</div> onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
className='accent-blue-500 w-3 h-3 sm:w-4 sm:h-4'
/>
<span className="whitespace-nowrap">Allow Edit</span>
</label>
</div> </div>
{/* Permission Controls - Always Visible */} {/* Delete Permission */}
<div className="max-w-80 opacity-100 overflow-visible"> <div className="relative group">
<div className="flex items-center space-x-3 pr-1.5"> <label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
{/* Edit Permission */} <input
<div className="relative group"> type="checkbox"
<label checked={recipient.allowSenderToDelete}
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 onChange={(e) =>
? 'bg-blue-100 border-blue-500 shadow-md' setRecipients({ ...recipient, allowSenderToDelete: e.target.checked })
: 'bg-white border-gray-300 hover:border-blue-400 text-gray-400' }
}`} onFocus={() => setOpenContactModal(true)}
> onBlur={handleContactModalBlur}
<input className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
type="checkbox" />
checked={recipient.allowSenderToEdit} <span className="whitespace-nowrap">Allow Delete</span>
onChange={(e) => </label>
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>
{/* 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'
}`}
>
<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>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Name Inputs */} {/* Name and Email Inputs - Responsive Grid */}
<div className="grid grid-cols-12 gap-4 mt-3"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 mt-3">
<div className="col-span-4"> {/* First Name */}
<div className="w-full">
<input <input
type="text" type="text"
onFocus={() => handleFocus("firstName")} onFocus={() => handleFocus("firstName")}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}
value={recipient.firstName} value={recipient.firstName}
placeholder="First Name" placeholder="First Name"
className={`w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition className={`w-full px-3 sm: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)]'}`} ${errors.firstName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => { onChange={(e) => {
setRecipients({ ...recipient, firstName: e.target.value }); setRecipients({ ...recipient, firstName: e.target.value });
// setErrors((prev) => ({ ...prev, firstName: false }));
}} }}
/> />
</div> </div>
{/* {errors.firstName && (
<p className="text-red-500 text-xs mt-1">First Name is required</p> {/* Last Name */}
)} */} <div className="w-full">
<div className="col-span-4">
<input <input
type="text" type="text"
onFocus={() => handleFocus("lastName")} onFocus={() => handleFocus("lastName")}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}
value={recipient.lastName} value={recipient.lastName}
placeholder="Last Name" placeholder="Last Name"
className={` w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition className={`w-full px-3 sm: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)]'}`} ${errors.lastName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => { onChange={(e) => {
setRecipients({ ...recipient, lastName: e.target.value }); setRecipients({ ...recipient, lastName: e.target.value });
setErrors((prev) => ({ ...prev, lastName: false })); setErrors((prev) => ({ ...prev, lastName: false }));
@ -522,15 +472,17 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
<p className="text-red-500 text-xs mt-1">Last Name is required</p> <p className="text-red-500 text-xs mt-1">Last Name is required</p>
)} )}
</div> </div>
<div className="col-span-4">
{/* Email - Full width on small screens, shares row on larger */}
<div className="w-full sm:col-span-2 lg:col-span-1">
<input <input
type="email" type="email"
onFocus={() => handleFocus("email")} onFocus={() => handleFocus("email")}
onBlur={handleContactModalBlur} onBlur={handleContactModalBlur}
value={recipient.email} value={recipient.email}
placeholder="Recipient Email" placeholder="Recipient Email"
className={`w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition className={`w-full px-3 sm: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)]'}`} ${errors.email || errors.emailFormat ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
onChange={(e) => { onChange={(e) => {
setRecipients({ ...recipient, email: e.target.value }); setRecipients({ ...recipient, email: e.target.value });
setErrors((prev) => ({ ...prev, email: false })); setErrors((prev) => ({ ...prev, email: false }));
@ -539,36 +491,41 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
{errors.email && ( {errors.email && (
<p className="text-red-500 text-xs mt-1">Email is required</p> <p className="text-red-500 text-xs mt-1">Email is required</p>
)} )}
{errors.emailFormat && (
<p className="text-red-500 text-xs mt-1">Email is not valid</p>
)}
</div> </div>
</div> </div>
{/* Host Fields for In-Person Signers */}
{recipient.type === recipientType.INPERSONSIGNER && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm: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-3 sm: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-3 sm: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>
)}
{recipient.type === recipientType.INPERSONSIGNER && <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> {/* Contact Modal - Responsive Positioning */}
<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 && ( {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"> <div className="absolute z-50 mt-1 w-full sm:w-2/3 lg:w-1/2 xl:w-2/3 max-h-[40vh] sm:max-h-[50vh] bg-white shadow-lg rounded-md p-2 sm:p-3 border border-gray-100 overflow-y-auto no-scrollbar">
<ContactModal <ContactModal
contactData={contactData} contactData={contactData}
inpFirstName={recipient.firstName} inpFirstName={recipient.firstName}
@ -583,36 +540,41 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
</div> </div>
)} )}
{/* Signing Options and Add Button - Responsive Layout */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
{/* Signing options + Add button in one line */} {/* Recipient Type Radio Options */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:flex-wrap gap-2 sm:gap-4 lg:gap-6">
<div className="flex flex-wrap gap-6">
{[ {[
{ label: "Signer", value: recipientType.SIGNER }, { label: "Signer", value: recipientType.SIGNER },
{ label: "In Person Signer", value: recipientType.INPERSONSIGNER }, { label: "In Person Signer", value: recipientType.INPERSONSIGNER },
{ label: "CC", value: recipientType.CC }, { label: "CC", value: recipientType.CC },
].map((option) => ( ].map((option) => (
<label key={option.value} className="flex items-center gap-2 text-gray-700 text-sm cursor-pointer"> <label key={option.value} className="flex items-center gap-2 text-gray-700 text-xs sm:text-sm cursor-pointer">
<input <input
type="radio" type="radio"
name="recipient-type" name="recipient-type"
value={option.value} value={option.value}
checked={recipient.type === option.value} checked={recipient.type === option.value}
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })} onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
className="accent-[var(--primary)] cursor-pointer" className="accent-[var(--primary)] cursor-pointer w-3 h-3 sm:w-4 sm:h-4"
/> />
{option.label} <span className="whitespace-nowrap">{option.label}</span>
</label> </label>
))} ))}
</div> </div>
{!showCard && <button {/* Add/Edit Button */}
type="submit" {!showCard && (
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" <button
> type="submit"
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"} className="inline-flex items-center justify-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-xs sm:text-sm font-semibold px-4 sm:px-5 py-2 rounded-md transition w-full sm:w-auto min-w-fit"
</button>} >
<FaPlus size={12} className="sm:w-3.5 sm:h-3.5" />
<span className="whitespace-nowrap">
{editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
</span>
</button>
)}
</div> </div>
</form> </form>
</div> </div>

View File

@ -15,6 +15,9 @@ import { IoCloseOutline } from "react-icons/io5";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { FiEdit } from "react-icons/fi"; import { FiEdit } from "react-icons/fi";
import { FaUsersLine } from "react-icons/fa6"; import { FaUsersLine } from "react-icons/fa6";
import { IoMdClose } from "react-icons/io";
import { BiSolidHelpCircle } from "react-icons/bi";
const getInitials = (name: string) => { const getInitials = (name: string) => {
if (!name) return ""; if (!name) return "";
@ -130,48 +133,28 @@ const RecipientCards = ({
) => { ) => {
const identifier = getRecipientIdentifier(recipient); const identifier = getRecipientIdentifier(recipient);
const currentValue = getCurrentValue(recipient, fieldName); const currentValue = getCurrentValue(recipient, fieldName);
const isEditing = editingField?.recipientId === identifier && editingField?.fieldName === fieldName;
const showValue = hasValue(recipient, fieldName);
const error = errors[identifier]?.[fieldName]; const error = errors[identifier]?.[fieldName];
const editable = isEditable(recipient); const editable = isEditable(recipient);
if (isEditing && editable) {
return (
<div className="relative">
<input
type={type}
value={currentValue || ""}
placeholder={placeholder}
autoFocus
onFocus={() => handleFieldFocus(identifier, fieldName)}
onBlur={(e) => handleFieldBlur(recipient, fieldName, e.target.value)}
onChange={(e) => handleFieldChange(recipient, fieldName, e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
e.currentTarget.blur();
}
}}
className={`w-full px-3 py-1 border ${error ? "border-red-500 placeholder-red-500 text-xs" : "border-gray-300"
} rounded-lg text-sm focus:outline-none focus:ring-0 focus:ring-gray-300 ${className}`}
/>
{/* {error && (
<span className="text-red-500 text-xs absolute left-0">{`${placeholder} is required`}</span>
)} */}
</div>
);
}
return ( return (
<span <div className="relative inline-block w-full sm:mx-1 lg:mx-3">
onClick={editable ? () => handleFieldFocus(identifier, fieldName) : undefined} <input
className={`${editable type={type}
? "cursor-pointer hover:bg-gray-100 rounded px-1 transition-colors" value={currentValue || ""}
: "cursor-not-allowed" placeholder={placeholder}
} ${showValue ? "text-gray-800" : "text-gray-400 italic"} ${className}`} onFocus={() => handleFieldFocus(identifier, fieldName)}
title={editable ? "Click to edit" : "Editing disabled"} onBlur={(e) => handleFieldBlur(recipient, fieldName, e.target.value)}
> onChange={(e) => handleFieldChange(recipient, fieldName, e.target.value)}
{showValue ? currentValue : placeholder} onKeyPress={(e) => {
</span> if (e.key === "Enter") {
e.currentTarget.blur();
}
}}
disabled={!editable}
className={`w-full px-2 sm:px-3 lg:px-4 py-1.5 sm:py-2 font-normal border focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition ${error ? "border-red-500 placeholder-red-500 text-xs" : "border-gray-300"} rounded-lg text-xs sm:text-sm ${className} ${!editable ? "bg-gray-100 cursor-not-allowed" : ""}`}
title={editable ? "Edit field" : "Editing disabled"}
/>
</div>
); );
}; };
@ -240,12 +223,12 @@ const RecipientCards = ({
{recipients.length > 0 ? ( {recipients.length > 0 ? (
<> <>
{recipients.length > 1 && ( {recipients.length > 1 && (
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-300"> <div className="flex sm:flex-row sm:items-center justify-between px-3 sm:px-4 py-2 border-b border-gray-300 gap-2">
<h3 className="text-sm font-semibold text-gray-700"> <h3 className="text-sm font-semibold text-gray-700">
Recipients ({recipients.length}) Recipients ({recipients.length})
</h3> </h3>
<label <label
className="flex items-center gap-2 cursor-pointer select-none" className="flex items-center gap-2 cursor-pointer select-none justify-center sm:justify-end"
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })} onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}
> >
<span className="text-xs font-medium text-gray-600">Signing Order</span> <span className="text-xs font-medium text-gray-600">Signing Order</span>
@ -254,30 +237,42 @@ const RecipientCards = ({
</div> </div>
)} )}
<div className="flex flex-col divide-y divide-gray-200"> <div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
{recipients.map((recipient, idx) => ( {recipients.map((recipient, idx) => (
<div <div
key={recipient.email + idx} key={recipient.email + idx}
className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${idx % 2 === 0 ? 'bg-gray-50' : 'bg-white'} ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
? "bg-neutral-100 opacity-40" ? "bg-neutral-100 opacity-40"
: "" : ""
}`} }`}
> >
<div className="flex items-center gap-3 min-w-0"> {/* Main Content Section */}
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
{/* Order Number */}
{recipients.length > 1 ? ( {recipients.length > 1 ? (
<div className="relative"> <div className="relative flex-shrink-0">
<input <input
type="number" type="number"
value={recipient.routingOrder || 1} value={recipient.routingOrder}
min={1} // min={1}
onChange={(e) => onChange={(e) =>
handleOrderChange( handleOrderChange(
recipient.id || recipient.email, recipient.id || recipient.uuid,
Number(e.target.value) 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" className="w-8 sm: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"
/> />
{idx === 0 && recipients.length > 1 && <p className="group">
<BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
{/* Tooltip */}
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
Signing order: Recipients receive emails one after another.
Same order means they all receive at once.
</span>
</p>}
</div> </div>
) : ( ) : (
<input <input
@ -287,144 +282,163 @@ const RecipientCards = ({
min={1} min={1}
onChange={(e) => onChange={(e) =>
handleOrderChange( handleOrderChange(
recipient.id || recipient.email, recipient.id || recipient.uuid,
Number(e.target.value) Number(e.target.value)
) )
} }
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]" className="w-6 sm:w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)] flex-shrink-0"
/> />
)} )}
<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">
{/* Avatar */}
{/* <div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)} {getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
</div> </div> */}
<div className="min-w-0">
<p className="font-semibold text-gray-800 truncate text-xs flex items-center gap-0.5"> {/* Form Fields Section */}
{renderEditableField(recipient, "firstName", "First Name")} <div className="min-w-0 flex-1">
{renderEditableField(recipient, "lastName", "Last Name")} {/* Role Name and Permissions Row */}
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && ( <div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
<span className="flex items-center gap-2"> {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <div className="flex-1 lg:w-1/2">
<span className="text-gray-400 ">| Permissions granted :</span> <div className="font-semibold text-gray-800 text-xs">
<FiEdit {renderEditableField(recipient, "roleName", "Role name")}
className={`cursor-pointer h-3 w-3 ${recipient.allowSenderToEdit
? "text-blue-500"
: "text-gray-400"
} hover:text-blue-800`}
onClick={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
}
/>
<ImBin
className={`cursor-pointer h-3 w-3 ${recipient.allowSenderToDelete
? "text-red-500"
: "text-gray-400"
} hover:text-red-700`}
onClick={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
/>
</span>
)}
</p>
<p className="text-xs text-gray-500 truncate flex items-center gap-1">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")} |
<span
onClick={
isEditable(recipient)
? () => handleFieldFocus(getRecipientIdentifier(recipient), "type")
: undefined
}
className={`${isEditable(recipient)
? "cursor-pointer hover:bg-gray-100"
: "cursor-not-allowed"
} rounded px-1 transition-colors`}
>
{recipient.type}
</span>
{(hasValue(recipient, "roleName") ||
transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) && (
<span>
{" | Role: "}
{renderEditableField(recipient, "roleName", "Role name")}
</span>
)}
{recipient.type === recipientType.INPERSONSIGNER && (
<>
<span className="flex items-center">
<span>{" | Host Name: "}</span>
{renderEditableField(recipient, "hostName", "Host Name")}
<span>{" | Host Email: "}</span>
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</span>
</>
)}
</p>
{editingField?.recipientId === getRecipientIdentifier(recipient) &&
editingField?.fieldName === "type" &&
isEditable(recipient) && (
<div className="flex items-center gap-2 mt-1">
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type === recipientType.SIGNER}
onChange={() =>
handleTypeChange(recipient, recipientType.SIGNER)
}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)]"
disabled={!isEditable(recipient)}
/>
<span className="text-xs">Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type === recipientType.INPERSONSIGNER}
onChange={() =>
handleTypeChange(recipient, recipientType.INPERSONSIGNER)
}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)]"
disabled={!isEditable(recipient)}
/>
<span className="text-xs">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type === recipientType.CC}
onChange={() =>
handleTypeChange(recipient, recipientType.CC)
}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)]"
disabled={!isEditable(recipient)}
/>
<span className="text-xs">CC</span>
</label>
</div> </div>
)} </div>}
{/* Permissions - Mobile: Below role, Desktop: Right side */}
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
<div className="flex ps-2.5 sm:flex-row items-start sm:items-center gap-2 sm:gap-4">
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToEdit}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToEdit: !recipient.allowSenderToEdit,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-blue-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Edit</span>
</label>
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
<input
type="checkbox"
checked={recipient.allowSenderToDelete}
onChange={() =>
dispatch(
updateRecipient({
...recipient,
allowSenderToDelete: !recipient.allowSenderToDelete,
uuid: getRecipientIdentifier(recipient),
})
)
}
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
/>
<span className="whitespace-nowrap">Allow Delete</span>
</label>
</div>
)}
</div>
</div>
{/* Name and Email Fields */}
<div className="mb-2 w-[92%]">
{/* Mobile: Stack vertically, Tablet+: Side by side */}
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "firstName", "First Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "lastName", "Last Name")}
</div>
</div>
</div>
<div className="w-full sm:flex-1">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
</div>
</div>
</div>
</div>
{/* Host Fields (for In-Person Signers) */}
{recipient.type === recipientType.INPERSONSIGNER && (
<div className="mb-2">
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2">
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostName", "Host Name")}
</div>
</div>
<div className="w-full sm:w-1/2">
<div className="font-semibold text-gray-800 text-xs">
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
</div>
</div>
</div>
</div>
)}
{/* Recipient Type Radio Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 my-1.5 mx-1 sm:mx-2">
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.SIGNER}
checked={recipient.type === recipientType.SIGNER}
onChange={() => handleTypeChange(recipient, recipientType.SIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.INPERSONSIGNER}
checked={recipient.type === recipientType.INPERSONSIGNER}
onChange={() => handleTypeChange(recipient, recipientType.INPERSONSIGNER)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm whitespace-nowrap">In Person Signer</span>
</label>
<label className="flex items-center gap-1">
<input
type="radio"
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
value={recipientType.CC}
checked={recipient.type === recipientType.CC}
onChange={() => handleTypeChange(recipient, recipientType.CC)}
onBlur={() => setEditingField(null)}
className="accent-[var(--primary)] w-3 h-3"
disabled={!isEditable(recipient)}
/>
<span className="text-xs sm:text-sm">CC</span>
</label>
</div>
</div> </div>
</div> </div>
{/* Action Buttons */}
{signType !== "self-sign" && ( {signType !== "self-sign" && (
<div className="flex justify-between gap-2 items-center"> <div className="lg:ml-2 absolute top-0 right-0 px-1.5 py-1.5 rounded-bl-sm border-l-1 border-b-1 border-slate-300 border-dashed">
<button <button
type="button" type="button"
title="Remove recipient" title="Remove recipient"
@ -435,9 +449,9 @@ const RecipientCards = ({
? false ? false
: !recipient.newOne && !recipient.allowSenderToDelete : !recipient.newOne && !recipient.allowSenderToDelete
} }
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50" className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
> >
<ImBin size={16} /> <ImBin size={14} className="sm:w-4 sm:h-4" />
</button> </button>
</div> </div>
)} )}
@ -446,24 +460,27 @@ const RecipientCards = ({
</div> </div>
</> </>
) : ( ) : (
<div className="p-6 text-center text-gray-500 font-medium text-sm"> <div className="p-4 sm:p-6 text-center text-gray-500 font-medium text-sm">
No Recipients Added No Recipients Added
</div> </div>
)} )}
{/* Responsive Modal */}
{signingOrderModal.open && ( {signingOrderModal.open && (
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center"> <div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center p-4">
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full overflow-auto"> <div className="flex-col relative justify-center items-center w-full max-w-xs sm:max-w-md md:max-w-lg lg:max-w-2xl xl:max-w-4xl max-h-[80vh] sm:max-h-[70vh] lg:max-h-[60vh] h-full overflow-auto">
<div className="p-2 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]"> <div className="p-3 sm:p-4 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
<p className="font-semibold">Signing Order</p> <p className="font-semibold text-sm sm:text-base">Signing Order</p>
<div <div
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })} onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
className="text-xl cursor-pointer hover:text-gray-200 transition-all" className="text-xl sm:text-2xl cursor-pointer hover:text-gray-200 transition-all p-1"
> >
<IoCloseOutline /> <IoCloseOutline />
</div> </div>
</div> </div>
<SigningOrderDiagram signers={recipients} /> <div className="bg-white rounded-b-lg">
<SigningOrderDiagram signers={recipients} />
</div>
</div> </div>
</div> </div>
)} )}

View File

@ -90,15 +90,26 @@ const SelectedDocuments = ({ transactionTemplateToggle }) => {
// Get proper Tailwind class names for upload card span // Get proper Tailwind class names for upload card span
const getUploadCardSpanClass = () => { const getUploadCardSpanClass = () => {
switch (docCount) { if (docCount === 0) {
case 0: return 'col-span-5'; // Full width when no docs // Upload card takes all columns only when no docs
case 1: return 'col-span-4'; // 4 cols for upload, 1 for doc return "col-span-1 sm:col-span-2 md:col-span-3 lg:col-span-4 xl:col-span-5";
case 2: return 'col-span-3'; // 3 cols for upload, 2 for docs }
case 3: return 'col-span-2'; // 2 cols for upload, 3 for docs
case 4: return 'col-span-1'; // 1 col for upload, 4 for docs switch (docCount) {
default: return 'col-span-1'; // 1 col for upload, rest for docs case 1:
} return "col-span-1 sm:col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4";
}; case 2:
return "col-span-1 sm:col-span-1 md:col-span-1 lg:col-span-2 xl:col-span-3";
case 3:
return "col-span-1 sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-2";
case 4:
return "col-span-1"; // balanced
default:
return "col-span-1";
}
};
// Calculate how many documents should be in the first row // Calculate how many documents should be in the first row
const getFirstRowDocCount = () => { const getFirstRowDocCount = () => {
@ -113,150 +124,153 @@ const SelectedDocuments = ({ transactionTemplateToggle }) => {
return ( return (
<> <>
{/* Upload Card First */} {/* Upload Card First */}
<div className='space-y-3'> <div className="space-y-3">
<div id='upload-doc-step-one' className="grid grid-cols-5 gap-2">
{/* Upload Document Card - Always First */}
<div <div
onClick={() => setModalOpen(true)} id="upload-doc-step-one"
className={`${getUploadCardSpanClass()} cursor-pointer h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md`} className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-5 gap-2"
> >
<FaPlus className="text-[var(--primary)] text-3xl mb-2" /> {/* Upload Document Card */}
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p> <div
onClick={() => setModalOpen(true)}
className={`${getUploadCardSpanClass()} cursor-pointer h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md`}
>
<FaPlus className="text-[var(--primary)] text-3xl mb-2" />
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p>
</div>
{/* First Row Documents */}
<ReactSortable
className="contents"
list={documents.slice(0, firstRowDocCount).map((doc) => ({ ...doc }))}
setList={(newState) => {
const updatedDocs = [...newState, ...documents.slice(firstRowDocCount)];
dispatch(setDocuments(updatedDocs));
}}
handle=".drag-handle"
animation={200}
ghostClass="sortable-ghost"
disabled={false}
>
{documents.slice(0, firstRowDocCount).map((doc) => (
<div key={doc.id} className="col-span-1 mx-auto">
<DocumentCard
id={doc.id}
name={doc.name}
isCompleted={
"isCompleted" in doc ||
("status" in doc && doc.status === "UPLOADED")
? true
: false
}
transactionTemplateToggle={transactionTemplateToggle}
/>
</div>
))}
</ReactSortable>
</div> </div>
{/* First Row Documents */} {/* Additional Rows */}
<ReactSortable {remainingDocs > 0 && (
className="contents" // Use 'contents' to make children participate in parent grid <ReactSortable
list={documents.slice(0, firstRowDocCount).map(doc => ({ ...doc }))} className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-5 gap-2"
setList={(newState) => { list={documents.slice(firstRowDocCount).map((doc) => ({ ...doc }))}
const updatedDocs = [ setList={(newState) => {
...newState, const updatedDocs = [
...documents.slice(firstRowDocCount) ...documents.slice(0, firstRowDocCount),
]; ...newState,
dispatch(setDocuments(updatedDocs)); ];
}} dispatch(setDocuments(updatedDocs));
handle=".drag-handle" }}
animation={200} handle=".drag-handle"
ghostClass="sortable-ghost" animation={200}
disabled={false} ghostClass="sortable-ghost"
> disabled={false}
{documents.slice(0, firstRowDocCount).map((doc) => ( >
<div key={doc.id} className="col-span-1"> {documents.slice(firstRowDocCount).map((doc) => (
<DocumentCard <div key={doc.id} className="col-span-1 mx-auto">
id={doc.id} <DocumentCard
name={doc.name} id={doc.id}
isCompleted={ name={doc.name}
("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED")) isCompleted={
? true "isCompleted" in doc ||
: false ("status" in doc && doc.status === "UPLOADED")
} ? true
transactionTemplateToggle={transactionTemplateToggle} : false
/> }
</div> transactionTemplateToggle={transactionTemplateToggle}
))} />
</ReactSortable> </div>
</div> ))}
</ReactSortable>
)}
{/* Additional Rows - Remaining Documents (5th, 6th, 7th, etc.) */} {/* Modal */}
{remainingDocs > 0 && ( {isModalOpen && (
<ReactSortable <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-2 sm:px-4">
className="grid grid-cols-5 gap-2" <div className="bg-white w-full h-full sm:max-w-5xl sm:h-[500px] rounded-xl shadow-lg flex flex-col sm:flex-row overflow-hidden relative">
list={documents.slice(firstRowDocCount).map(doc => ({ ...doc }))} {/* Close Button */}
setList={(newState) => { <button
const updatedDocs = [ className="absolute top-2 right-2 text-gray-500 hover:text-red-500 transition text-xl bg-white rounded-full p-2"
...documents.slice(0, firstRowDocCount), onClick={() => setModalOpen(false)}
...newState >
]; <IoMdClose />
dispatch(setDocuments(updatedDocs)); </button>
}}
handle=".drag-handle"
animation={200}
ghostClass="sortable-ghost"
disabled={false}
>
{documents.slice(firstRowDocCount).map((doc) => (
<div key={doc.id} className="col-span-1">
<DocumentCard
id={doc.id}
name={doc.name}
isCompleted={
("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED"))
? true
: false
}
transactionTemplateToggle={transactionTemplateToggle}
/>
</div>
))}
</ReactSortable>
)}
{/* Sidebar Tabs */}
{/* Modal */} <div className="w-full sm:w-1/4 bg-gray-100 p-4 flex flex-row sm:flex-col gap-2 border-b sm:border-b-0 sm:border-r border-gray-200 overflow-x-auto sm:overflow-hidden">
{isModalOpen && ( {tabs.map((tab) => (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-4"> <button
<div className="bg-white w-full max-w-5xl h-[500px] rounded-xl shadow-lg flex overflow-hidden relative"> key={tab.name}
{/* Close Button */} className={`flex items-center gap-2 p-3 rounded-md text-sm font-medium transition whitespace-nowrap ${activeTab === tab.name
<button ? "bg-white shadow text-[var(--primary)]"
className="absolute top-2 right-2 text-gray-500 hover:text-red-500 transition text-xl bg-white rounded-full p-2" : "hover:bg-gray-200 text-gray-600"
onClick={() => setModalOpen(false)} }`}
> onClick={() => setActiveTab(tab.name)}
<IoMdClose /> >
</button> {tab.icon} {tab.name}
{/* Sidebar Tabs */}
<div className="w-1/4 bg-gray-100 p-4 flex flex-col gap-2 border-r border-gray-200">
{tabs.map(tab => (
<button
key={tab.name}
className={`flex items-center gap-2 p-3 rounded-md text-sm font-medium transition ${activeTab === tab.name
? 'bg-white shadow text-[var(--primary)]'
: 'hover:bg-gray-200 text-gray-600'
}`}
onClick={() => setActiveTab(tab.name)}
>
{tab.icon} {tab.name}
</button>
))}
</div>
{/* Modal Content */}
<div className="w-3/4 p-6 overflow-y-auto">
{activeTab === 'Upload' && (
<div
onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
className="flex flex-col items-center justify-center h-full border-2 border-dashed border-gray-300 rounded-xl p-6 text-center cursor-pointer hover:bg-gray-50"
>
<FaCloudUploadAlt className="text-5xl text-[var(--primary)] mb-4" />
<p className="text-sm text-gray-500 mb-2">Drag & drop files here or</p>
<button className="px-5 py-2 text-sm bg-[var(--primary)] text-white rounded-md hover:scale-103 cursor-pointer transition-all">
Browse Files
</button> </button>
<input ))}
type="file" </div>
ref={fileInputRef}
className="hidden" {/* Modal Content */}
onChange={handleFileChange} <div className="w-full sm:w-3/4 p-4 sm:p-6 overflow-y-auto">
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt" {activeTab === "Upload" && (
multiple <div
/> onClick={() => fileInputRef.current?.click()}
</div> onDragOver={(e) => e.preventDefault()}
)} onDrop={handleDrop}
{activeTab !== 'Upload' && ( className="flex flex-col items-center justify-center h-full border-2 border-dashed border-gray-300 rounded-xl p-6 text-center cursor-pointer hover:bg-gray-50"
<div className="flex items-center justify-center h-full text-gray-400 text-sm"> >
<p>{activeTab} integration coming soon...</p> <FaCloudUploadAlt className="text-5xl text-[var(--primary)] mb-4" />
</div> <p className="text-sm text-gray-500 mb-2">
)} Drag & drop files here or
</p>
<button className="px-5 py-2 text-sm bg-[var(--primary)] text-white rounded-md hover:scale-103 cursor-pointer transition-all">
Browse Files
</button>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt"
multiple
/>
</div>
)}
{activeTab !== "Upload" && (
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
<p>{activeTab} integration coming soon...</p>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> )}
)}
</div> </div>
</> </>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { useEffect, useLayoutEffect, useState } from 'react' import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../../store/store' import { RootState } from '../../../store/store'
@ -35,17 +35,23 @@ export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
} }
interface DocumentContainerProps { interface DocumentContainerProps {
transactionSaving: boolean; transactionSaving?: boolean;
setTransactionSaving: React.Dispatch<React.SetStateAction<boolean>>; setTransactionSaving?: React.Dispatch<React.SetStateAction<boolean>>;
isSend: boolean | null; isSend?: boolean | null;
setIsSend: React.Dispatch<React.SetStateAction<boolean | null>>; setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>;
isPreview?: boolean;
aId?: string;
tId?: string;
} }
const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving, setTransactionSaving, isSend }) => { let copiedTags: DroppedItem[] = []
let selectedDoc: { [key: string]: string | null } = { key: null }
const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null }) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch(); const dispatch = useDispatch();
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
const accountId = params.accountId; let accountId = params.accountId;
let transactionId = params.transactionId let transactionId = params.transactionId
if (!transactionId) { if (!transactionId) {
transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
@ -57,8 +63,16 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
return null; return null;
} }
// If is preview is enable
if (isPreview && aId !== null && tId !== null) {
accountId = aId;
transactionId = tId;
}
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId) // const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
const navigate = useNavigate(); const navigate = useNavigate();
const childRef = useRef<any>(null);
const tagUndoRef = useRef<DroppedItem[] | []>([]);
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
@ -66,13 +80,11 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
const [droppedItems, setDroppedItems] = useState<DroppedItemsObj>({}); const [droppedItems, setDroppedItems] = useState<DroppedItemsObj>({});
interface HotKeysObj { interface HotKeysObj {
isActive: boolean; isActive: boolean;
key: string | null; key: string | null;
tagId?: string | null; tagId?: string | null;
hotKey?: string | null; hotKey?: string | null;
} }
@ -141,10 +153,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
if (selectedRecipients.length === 0) return; if (selectedRecipients.length === 0) return;
fetchTags(); fetchTags();
}, [selectedRecipients]); }, [selectedRecipients]);
// FETCHING TAG - END // FETCHING TAG - END
const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1); const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1);
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null); const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
@ -213,9 +223,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
}) })
) )
console.log(droppedItemsUpdateFalse, "Line 182") setDroppedItems(droppedItemsUpdateFalse);
// setDroppedItems(droppedItemsUpdateFalse);
} catch (error) { } catch (error) {
console.error("Error submitting recipient tags:", error); console.error("Error submitting recipient tags:", error);
} }
@ -235,7 +243,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
let tagsData = groupByRecipientId(updatedTags); let tagsData = groupByRecipientId(updatedTags);
await sendTagsToRecipients(tagsData, accountId, transactionId); await sendTagsToRecipients(tagsData, accountId, transactionId);
return // return
// SENDING Transaction - START // SENDING Transaction - START
if (!save) { if (!save) {
const res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" }) const res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" })
@ -343,12 +351,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
setTimeout(() => { setTimeout(() => {
const tagIndex = key in droppedItems ? droppedItems[key].length : 0; const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
console.log(tagIndex, "line 307", key); console.log(tagIndex, "line 307", key);
setSelectedTag({ key, tagIndex }) setSelectedTag({ key, tagIndex })
}, 0) }, 0)
}; };
// HANDLE DROP - END // HANDLE DROP - END
console.log(selectedTag, "Line 359")
useEffect(() => { useEffect(() => {
if (!hotKeysObj.isActive) return if (!hotKeysObj.isActive) return
@ -356,9 +365,24 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
switch (hotKeysObj.hotKey) { switch (hotKeysObj.hotKey) {
case "Ctrl+c": case "Ctrl+c":
// Handle Ctrl+C const tempCopy = { ...droppedItems[selectedTag?.key][selectedTag?.index] }
delete tempCopy.id;
copiedTags = [{ ...tempCopy, dimensions: { ...tempCopy.dimensions, top: tempCopy.dimensions.top + 10, left: tempCopy.dimensions.left + 10 } }]
console.log("from Copy copiedTags", copiedTags)
break; break;
case "Ctrl+v": case "Ctrl+v":
console.log(selectedDoc.key, "keyss")
let updatedKey = droppedItems[selectedDoc.key]
updatedKey.push(...copiedTags)
console.log("copiedTags", copiedTags)
console.log("updatedKey", updatedKey)
setDroppedItems(prevState => ({
...prevState,
[selectedDoc.key]: updatedKey
}));
// Handle Ctrl+V // Handle Ctrl+V
break; break;
case "Ctrl+x": case "Ctrl+x":
@ -372,12 +396,11 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
break; break;
case "Ctrl+a": case "Ctrl+a":
docCon?.querySelectorAll("[data-tag-id]").forEach(tag => { docCon?.querySelectorAll("[data-tag-id]").forEach(tag => {
tag.classList.add("border-2", "border-slate-800") tag.classList.add("border-2", "border-sky-700")
tag.setAttribute("all-select-group", "true") tag.setAttribute("all-select-group", "true")
docCon.addEventListener("click", () => { docCon.addEventListener("click", () => {
console.log("focus in", tag) tag.classList.remove("border-2", "border-sky-800")
tag.classList.remove("border-2", "border-slate-800")
tag.removeAttribute("all-select-group") tag.removeAttribute("all-select-group")
}) })
// docCon.addEventListener("focusout", () => { // docCon.addEventListener("focusout", () => {
@ -388,9 +411,15 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
}); });
break; break;
case "delete": case "delete":
docCon?.querySelectorAll("[all-select-group]").forEach((tag) => { const allselectedTags = docCon?.querySelectorAll("[all-select-group]") ?? []
console.log(tag, "TAGID") allselectedTags.forEach((tag) => {
const tagIndex = tag.getAttribute("data-tag-id") ?? "";
const key = tag.getAttribute("data-drop-key") ?? "";
childRef.current.handleRemoveTag(key, Number(tagIndex))
}) })
if (allselectedTags.length === 0) childRef.current.handleRemoveTag(selectedTag?.key, selectedTag?.index)
break; break;
default: default:
@ -399,8 +428,9 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
}, [hotKeysObj]) }, [hotKeysObj])
const handleDocumentFocus = (key: string) => { const handleDocumentFocus = (key: string) => {
const id = key.replace("_", ""); // const id = key.replace("_", "");
const targetCon = document.getElementById(id); const targetCon = document.getElementById('doc-con');
selectedDoc.key = key;
targetCon?.addEventListener("keydown", (event) => { targetCon?.addEventListener("keydown", (event) => {
event.preventDefault(); event.preventDefault();
@ -425,7 +455,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
setHotKeysObj({ setHotKeysObj({
isActive: true, isActive: true,
key: pressedKey, key: key,
tagId: null, tagId: null,
hotKey: hotKey hotKey: hotKey
}); });
@ -439,18 +469,18 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
return ( return (
<> <>
{/* Center Doc Container - START */} {/* Center Doc Container - START */}
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'> <div id="doc-con" className={`zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth`}>
{ {
selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => { selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => {
return ( return (
<div key={indxDoc} className='flex flex-col'> <div key={indxDoc} className='flex flex-col overflow-x-auto'>
<div <div
className='py-2 text-sm'> className='py-2 text-sm'>
<p className='text-center'>{doc.name}</p> <p className='text-center'>{doc.name}</p>
</div> </div>
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'> <div className={`flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%] ${isPreview ? "pointer-events-none" : ""}`}>
{ {
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => { doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
const key = `${doc.id}_${page.index}`; const key = `${doc.id}_${page.index}`;
@ -466,6 +496,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
> >
<DroppedItemsTagsCon <DroppedItemsTagsCon
ref={childRef}
droppedItems={droppedItems} droppedItems={droppedItems}
pageIdkey={key} pageIdkey={key}
selectedTag={selectedTag} selectedTag={selectedTag}
@ -475,6 +506,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
accountId={accountId} accountId={accountId}
transactionId={transactionId} transactionId={transactionId}
transactionTemplateToggle={transactionTemplateToggle} transactionTemplateToggle={transactionTemplateToggle}
tagUndoRef={tagUndoRef}
/> />
<img <img

View File

@ -8,7 +8,7 @@
// -- handleGroupBorderPosition // -- handleGroupBorderPosition
// -- handleAddGroupItemClicked // -- handleAddGroupItemClicked
import React, { useEffect, useRef } from 'react' import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { DroppedItemsObj } from './DocumentContainer'; import { DroppedItemsObj } from './DocumentContainer';
import typeToIconMap from '../../../helper/iconMap'; import typeToIconMap from '../../../helper/iconMap';
import { IoMdClose } from 'react-icons/io'; import { IoMdClose } from 'react-icons/io';
@ -16,6 +16,7 @@ import { IoAdd } from 'react-icons/io5';
import { FaQuestionCircle } from 'react-icons/fa'; import { FaQuestionCircle } from 'react-icons/fa';
import api from '../../../config/api'; import api from '../../../config/api';
import { Recipient } from '../../../store/slices/selectedRecipientsSlice'; import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
import { DroppedItem } from '../../../utlity/types';
interface DroppedItemsTagsConProps { interface DroppedItemsTagsConProps {
droppedItems: DroppedItemsObj; droppedItems: DroppedItemsObj;
@ -27,9 +28,11 @@ interface DroppedItemsTagsConProps {
accountId: string; accountId: string;
transactionId: string; transactionId: string;
transactionTemplateToggle: string; transactionTemplateToggle: string;
tagUndoRef: React.MutableRefObject<DroppedItem[]>;
} }
const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({ // 👶 Child component with forwardRef
const DroppedItemsTagsCon = forwardRef(({
droppedItems, droppedItems,
pageIdkey, pageIdkey,
selectedTag, selectedTag,
@ -38,19 +41,24 @@ const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({
setDroppedItems, setDroppedItems,
accountId, accountId,
transactionId, transactionId,
transactionTemplateToggle transactionTemplateToggle,
}) => { tagUndoRef
}: DroppedItemsTagsConProps, ref) => {
const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 }); const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 });
const handleTagClick = (key: string, index: number) => { const handleTagClick = (key: string, index: number) => {
console.log(index, "line 307", key);
setSelectedTag({ key, index }) setSelectedTag({ key, index })
} }
const handleRemoveTag = (key: string, targetIndex: number) => { const handleRemoveTag = (key: string, targetIndex: number) => {
if(key === undefined || targetIndex === undefined) return
const tag = droppedItems[key][targetIndex]; const tag = droppedItems[key][targetIndex];
const recipientId = tag.recipientId; const recipientId = tag.recipientId;
(async () => { (async () => {
if ("id" in tag) { if ("id" in tag) {
try { try {
@ -62,6 +70,8 @@ const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({
} }
} }
setSelectedTag(null); setSelectedTag(null);
// Check if this item has a group for special group handling // Check if this item has a group for special group handling
@ -136,6 +146,10 @@ const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({
})() })()
}; };
useImperativeHandle(ref, () => ({
handleRemoveTag
}));
// HANDLE TAG MOUSE DOWN - START // HANDLE TAG MOUSE DOWN - START
// Updated handleTagMouseDown function with proper group border positioning // Updated handleTagMouseDown function with proper group border positioning
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => { const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
@ -536,6 +550,7 @@ const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({
// key={index} // key={index}
draggable="false" draggable="false"
data-tag-id={index} data-tag-id={index}
data-drop-key={pageIdkey}
data-tag-selected={index === selectedTag?.index} data-tag-selected={index === selectedTag?.index}
className="absolute rounded dropped-item" className="absolute rounded dropped-item"
onClick={() => handleTagClick(pageIdkey, index)} onClick={() => handleTagClick(pageIdkey, index)}
@ -621,6 +636,6 @@ const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({
{/* Render only the dropped items for this specific docId and page - ENDS */} {/* Render only the dropped items for this specific docId and page - ENDS */}
</> </>
) )
} })
export default DroppedItemsTagsCon export default DroppedItemsTagsCon

View File

@ -12,14 +12,18 @@ import { fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE
import { fetchDocuments } from '../../../store/slices/documentSlice' import { fetchDocuments } from '../../../store/slices/documentSlice'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { Bounce, toast } from 'react-toastify' import { Bounce, toast } from 'react-toastify'
import { FiChevronDown } from 'react-icons/fi'
interface NavTwoProps { interface NavTwoProps {
transactionSaving: boolean; transactionSaving?: boolean;
// setTransactionSaving: React.Dispatch<React.SetStateAction<boolean>> // setTransactionSaving: React.Dispatch<React.SetStateAction<boolean>>
isFields: boolean; isFields?: boolean;
setIsFields: React.Dispatch<React.SetStateAction<boolean>> setIsFields?: React.Dispatch<React.SetStateAction<boolean>>
isSend: boolean | null; isSend?: boolean | null;
setIsSend: React.Dispatch<React.SetStateAction<boolean | null>> setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>
isPreview?: boolean
aId?: string | null
tId?: string | null
} }
@ -47,11 +51,11 @@ const fitObj = [
}, },
] ]
const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsFields, isSend, setIsSend }) => { const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, setIsFields, isSend, setIsSend, isPreview = false, aId = null, tId = null }) => {
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
const dispatch = useDispatch(); const dispatch = useDispatch();
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
const accountId = params.accountId; let accountId = params.accountId;
let transactionId = params.transactionId let transactionId = params.transactionId
if (!transactionId) { if (!transactionId) {
transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
@ -63,7 +67,12 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
return null; return null;
} }
// If is preview is enable
if (isPreview && aId !== null && tId !== null) {
accountId = aId;
transactionId = tId;
}
// const [transactionSaving, setTransactionSaving] = useState<boolean>(false); // const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
const [dotCount, setDotCount] = useState(0); const [dotCount, setDotCount] = useState(0);
@ -130,8 +139,8 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
if (isSignSend) { if (isSignSend) {
window.open(`./../../../sign/${selfUserSign.recipientId}`, "_self"); window.open(`./../../../sign/${selfUserSign.recipientId}`, "_self");
} }
else { else {
navigate(`/document-completion-screen/${accountId}/${transactionId}`, { replace: true }) navigate(`/transaction-sent/${accountId}/${transactionId}`, { replace: true })
} }
} else { } else {
navigate(`/templates`); navigate(`/templates`);
@ -234,12 +243,12 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
</div> </div>
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'> {!isPreview && <div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
<RiResetRightLine <RiResetRightLine
className={`${transactionSaving ? "animate-spin" : ""} transition duration-300`} className={`${transactionSaving ? "animate-spin" : ""} transition duration-300`}
/> />
<p className="tracking-wide transition-all">{transactionSaving ? `Saving ${dots}` : "Save"}</p> <p className="tracking-wide transition-all">{transactionSaving ? `Saving ${dots}` : "Save"}</p>
</div> </div>}
</div> </div>
@ -247,6 +256,32 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
{/* Right Side - STARTS */} {/* Right Side - STARTS */}
<div className='flex justify-center items-center'> <div className='flex justify-center items-center'>
<div className="relative inline-block text-left">
{/* Trigger */}
<button className="peer flex justify-center items-center bg-white px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 ">
<p className='tracking-wide mr-1'>Actions</p>
<MdKeyboardArrowDown />
</button>
{/* Menu */}
<div className="absolute right-0 mt-2 w-48 hidden peer-focus:block rounded-md bg-white shadow-sm ring-1 ring-black/5 z-10">
<div className="py-1">
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Save & Close
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Message
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Edit Recipients
</button>
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
Advanced Options
</button>
</div>
</div>
</div>
{/* Documnt Nav */} {/* Documnt Nav */}
<div tabIndex={1} onBlur={() => setNav2DocToggle(false)}> <div tabIndex={1} onBlur={() => setNav2DocToggle(false)}>
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'> <div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
@ -293,24 +328,26 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsField
</div> </div>
{/* Send */} {/* Send */}
<div {!isPreview && <>
id='send-sign-dash-btn' <div
onClick={() => handleSend()} id='send-sign-dash-btn'
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'> onClick={() => handleSend()}
<p className='tracking-wide text-white'> className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"} <p className='tracking-wide text-white'>
</p> {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"}
<FaSignature className='text-white text-md' /> </p>
</div> <FaSignature className='text-white text-md' />
{transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign && <div </div>
id='send-and-sign-dash-btn' {transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign && <div
onClick={() => handleSend(selfUserSign.sign)} id='send-and-sign-dash-btn'
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'> onClick={() => handleSend(selfUserSign.sign)}
<p className='tracking-wide text-white'> className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
Sign & Send <p className='tracking-wide text-white'>
</p> Sign & Send
<FaSignature className='text-white text-md' /> </p>
</div>} <FaSignature className='text-white text-md' />
</div>}
</>}

View File

@ -302,9 +302,7 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ droppedItems, selectedTag, s
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} /> <CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />
} }
{selectedTag.label && <LabelAttr label={selectedTag.label ?? ""} handlePropertyChange={handlePropertyChange} />
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
<ToolTipAttr tooltip={selectedTag.tooltip ?? ""} handlePropertyChange={handlePropertyChange} /> <ToolTipAttr tooltip={selectedTag.tooltip ?? ""} handlePropertyChange={handlePropertyChange} />
{ {

View File

@ -1,16 +1,15 @@
import React, { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import logoRecord from '../../assets/record.svg'; import logoRecord from '../../assets/record.svg';
import LoaderUi from '../../helper/LoaderUi'; import LoaderUi from '../../helper/LoaderUi';
import api from '../../config/api'; import api from '../../config/api';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { TemplateInterface } from '../../utlity/types';
const TemplateTable = () => { const TemplateTable = () => {
const loaction = useLocation()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [templates, setTemplates] = useState([]) const [templates, setTemplates] = useState<TemplateInterface[] | []>([])
const accountDetails = useSelector((state: any) => state.user.account); const accountDetails = useSelector((state: any) => state.user.account);
const accountId = accountDetails.id || ""; const accountId = accountDetails.id || "";
const [showMenu, setShowMenu] = useState(null); const [showMenu, setShowMenu] = useState(null);
@ -88,7 +87,7 @@ const TemplateTable = () => {
templateId: item.id, templateId: item.id,
}; };
let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload) let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload)
// /process-step-one/:accountId/:transactionId // /process-step-one/:accountId/:transactionId
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`); navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
} catch (error) { } catch (error) {
@ -139,7 +138,7 @@ const TemplateTable = () => {
<th className="px-6 py-4 text-center font-semibold">Last Updated</th> <th className="px-6 py-4 text-center font-semibold">Last Updated</th>
</> </>
)} )}
<th className="px-6 py-4 text-center font-semibold">Use</th> {/* <th className="px-6 py-4 text-center font-semibold">Use</th> */}
<th className="px-6 py-4 text-center font-semibold">Action</th> <th className="px-6 py-4 text-center font-semibold">Action</th>
</tr> </tr>
</thead> </thead>
@ -181,14 +180,14 @@ const TemplateTable = () => {
</> </>
)} )}
<td className="px-6 py-4 text-center"> {/* <td className="px-6 py-4 text-center">
<button <button
className="bg-[var(--lightGreyCol)] cursor-pointer border-gray-300 text-gray-600 hover:bg-gray-100 px-3 py-1 rounded-lg font-semibold text-xs transition-colors duration-150" className="cursor-pointer bg-[var(--primary)] border-gray-300 text-white hover:bg-gray-100 px-3 py-2 rounded-lg font-semibold text-xs transition-colors duration-150"
onClick={() => handleUseClick(item)} onClick={() => handleUseClick(item)}
> >
Use USE
</button> </button>
</td> </td> */}
<td className="px-6 py-4 text-center"> <td className="px-6 py-4 text-center">
<div <div
@ -201,7 +200,7 @@ const TemplateTable = () => {
}} }}
> >
<button <button
className="text-gray-400 hover:text-gray-600 transition-colors duration-150 p-1 rounded-md hover:bg-gray-100" className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
onClick={() => onClick={() =>
setShowMenu(showMenu === index ? null : index) setShowMenu(showMenu === index ? null : index)
} }
@ -210,10 +209,16 @@ const TemplateTable = () => {
</button> </button>
{showMenu === index && ( {showMenu === index && (
<div className="absolute -top-9 right-4 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[100px]"> <div className="absolute top-7.5 right-0 bg-white border border-gray-200 rounded-xl shadow-lg z-20 min-w-[140px] overflow-hidden animate-fade-in">
<button
onClick={() => handleUseClick(item)}
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
>
Use
</button>
<button <button
onMouseDown={() => handleEditClick(item.id)} onMouseDown={() => handleEditClick(item.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600" className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-yellow-50 hover:text-yellow-600 transition-colors"
> >
Edit Edit
</button> </button>
@ -221,12 +226,13 @@ const TemplateTable = () => {
onMouseDown={() => { onMouseDown={() => {
setShowMenu(null); setShowMenu(null);
}} }}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600" className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-red-50 hover:text-red-600 transition-colors"
> >
Delete Delete
</button> </button>
</div> </div>
)} )}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -87,6 +87,10 @@ const TransactionView = () => {
} }
} }
console.log(transactionDetails, "transactionDetails")
const fetchDocuments = async () => { const fetchDocuments = async () => {
try { try {
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents`) const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents`)
@ -155,13 +159,16 @@ const TransactionView = () => {
More More
</button> </button>
{/* <Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-two/${accountId}/${transactionId}`}> <Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-one/${accountId}/${transactionId}`}>
Edit Edit
</Link> */} </Link>
<button className='cursor-pointer border text-[var(--primary)] border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' onClick={handleOpenAuditModal}> <button className='cursor-pointer border text-[var(--primary)] border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' onClick={handleOpenAuditModal}>
Audit Audit
</button> </button>
</div> </div>
<hr className='my-6 border-slate-200' /> <hr className='my-6 border-slate-200' />
@ -203,7 +210,7 @@ const TransactionView = () => {
<p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p> <p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p>
</div> </div>
{recipient.email === user.email && recipient.status !== "COMPLETED" && ( {transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && (
<button <button
onClick={() => handleNavigateToSign(recipient.id)} onClick={() => handleNavigateToSign(recipient.id)}
className="text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold text-xs hover:bg-opacity-90 transition" className="text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold text-xs hover:bg-opacity-90 transition"

View File

@ -30,7 +30,7 @@ api.interceptors.response.use(
const originalRequest = error.config; const originalRequest = error.config;
// Prevent multiple refresh attempts // Prevent multiple refresh attempts
// if (error.response?.status === 401 || error.response?.status === 500) { // if (error.response?.status === 401 || error.response?.status === 500) {
if (error.response?.status === 401 || error.response?.status === 500) { if (error.response?.status === 401) {
originalRequest._retry = true; originalRequest._retry = true;
try { try {
if (!isRefreshing) { if (!isRefreshing) {

View File

@ -1,4 +1,7 @@
import { LuSignature } from "react-icons/lu"; import { LuSignature } from "react-icons/lu";
import SMELogo from "../assets/SMELogo2.png";
import { FaFileSignature } from "react-icons/fa";
interface StyleProps { interface StyleProps {
classStyle?: string; classStyle?: string;
@ -11,8 +14,8 @@ const LoaderUi = ({ classStyle = '' }: StyleProps) => {
<div className="absolute inset-0 bg-black/30 opacity-60"></div> <div className="absolute inset-0 bg-black/30 opacity-60"></div>
{/* Loader box */} {/* Loader box */}
<div className="relative z-10 bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg"> <div className="w-60 relative z-10 bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg">
<LuSignature className="text-4xl font-bold animate-text" /> <FaFileSignature className="text-4xl font-bold animate-text" />
<p>Loading...!</p> <p>Loading...!</p>
</div> </div>
</div> </div>

View File

@ -115,7 +115,7 @@ canvas {
} }
50% { 50% {
color: #41644A; color: var(--primary);
/* Tailwind blue-500 */ /* Tailwind blue-500 */
} }
} }
@ -140,6 +140,34 @@ canvas {
} }
} }
/* Custom scrollbar size via CSS */
@layer utilities {
.custom-scrollbar {
scrollbar-width: thin; /* For Firefox */
scrollbar-color: rgba(120, 120, 120, 0.4) transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 1px;
height: 1px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(120, 120, 120, 0.4);
border-radius: 9999px; /* fully rounded */
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(120, 120, 120, 0.6); /* slightly darker on hover */
}
.custom-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
}
}
/* Chrome, Safari, Edge, Opera */ /* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button, input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button { input::-webkit-inner-spin-button {

View File

@ -200,7 +200,7 @@ const selectedRecipientsSlice = createSlice({
}, },
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => { updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
const { id, routingOrder } = action.payload; const { id, routingOrder } = action.payload;
const recipient = state.recipients.find((r) => r.id === id || r.email === id); const recipient = state.recipients.find((r) => r.id === id || r.uuid === id);
if (recipient) { if (recipient) {
recipient.routingOrder = routingOrder; recipient.routingOrder = routingOrder;
} }

View File

@ -206,4 +206,47 @@ export interface DroppedItem {
value?: string | number; value?: string | number;
isDeleted: boolean; isDeleted: boolean;
} }
export interface TemplateInterface {
id: number;
name: string | null;
code: string;
description: string | null;
active: boolean;
enableIdStamping: boolean;
enableMobileSigning: boolean;
enableSignatureIdStamping: boolean;
customFields: any[];
allowReassign: boolean;
allowViewAudit: boolean;
subject: string | null;
message: string | null;
reminderConfig: {
reminderEnabled: boolean;
reminderFrequency: number;
reminderDelay: number;
expireReminder: number;
};
expireAfter: number;
expireEnabled: boolean;
recipients: any | null;
documents: any | null;
createdAt: string;
lastModifiedAt: string;
createdBy: {
userId: number;
firstName: string;
lastName: string;
name: string;
email: string;
};
lastModifiedBy: {
userId: number;
firstName: string;
lastName: string;
name: string;
email: string;
};
}