Compare commits
No commits in common. "aa9bd93bcef041bb3dec63dcb07523952335a41c" and "f3b1fc021f11412dfa72fb59356df7ea7da62514" have entirely different histories.
aa9bd93bce
...
f3b1fc021f
8
.env
8
.env
|
|
@ -1,7 +1,3 @@
|
|||
# VITE_BASE_URI="https://wired-terminally-muskrat.ngrok-free.app"
|
||||
# VITE_BASE_URI="https://officially-powerful-goat.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://concrete-gladly-mullet.ngrok-free.app"
|
||||
|
||||
|
||||
# VITE_BASE_URI="http://192.168.2.176:8083"
|
||||
# VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
|
||||
|
|
@ -22,7 +22,6 @@ import Templates from "./components/templates/Templates";
|
|||
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
|
||||
import JoyrideController from "./utlity/JoyRideTour";
|
||||
import Contacts from "./components/contacts/Contacts";
|
||||
import TransactionSentSuccess from "./components/process/CompletionScreen/TransactionSentSuccess";
|
||||
const App = () => {
|
||||
|
||||
return (
|
||||
|
|
@ -85,16 +84,10 @@ const App = () => {
|
|||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* <Route path="/document-completion-screen/:accountId/:transactionId" element={
|
||||
<Route path="/document-completion-screen/:accountId/:transactionId" element={
|
||||
<ProtectedRoute>
|
||||
<DocumentCompletionScreen />
|
||||
</ProtectedRoute>
|
||||
} /> */}
|
||||
|
||||
<Route path="/transaction-sent/:accountId/:transactionId" element={
|
||||
<ProtectedRoute>
|
||||
<TransactionSentSuccess />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Links work wothout Login */}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
|
|
@ -2,9 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa";
|
||||
import api from '../config/api';
|
||||
import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store/store';
|
||||
|
||||
interface AccountNavProps {
|
||||
accountName?: string | null;
|
||||
|
|
@ -23,8 +20,6 @@ interface Account {
|
|||
interface UserData {
|
||||
id: number;
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
language: string;
|
||||
account: Account;
|
||||
|
|
@ -55,8 +50,6 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
|
||||
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
|
||||
|
||||
const userDetailsAsync = useSelector((state: RootState) => state.user)
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [accountsOpen, setAccountsOpen] = useState(false);
|
||||
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
||||
|
|
@ -64,13 +57,9 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [initial, setInitial] = useState<string>("");
|
||||
|
||||
const [user, setUser] = useState<UserData>({
|
||||
id: 0,
|
||||
name: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
language: "",
|
||||
account: {
|
||||
|
|
@ -80,19 +69,6 @@ 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 () => {
|
||||
try {
|
||||
let res = await api.get('/api/user/accounts');
|
||||
|
|
@ -141,8 +117,6 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
setUser({
|
||||
id: account.userId,
|
||||
name: account.name,
|
||||
firstName: account.firstName,
|
||||
lastName: account.lastName,
|
||||
email: account.userEmail,
|
||||
language: account.language, // set it if available
|
||||
account: {
|
||||
|
|
@ -152,12 +126,6 @@ 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({
|
||||
id: account.userId,
|
||||
name: account.name,
|
||||
|
|
@ -185,8 +153,6 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
const userData = {
|
||||
id: item?.id,
|
||||
name: item?.firstName + " " + item?.lastName,
|
||||
firstName: item?.firstName,
|
||||
lastName: item?.lastName,
|
||||
email: item?.email,
|
||||
language: item?.language,
|
||||
account: {
|
||||
|
|
@ -197,12 +163,6 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
};
|
||||
|
||||
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('activeAccountId', JSON.stringify(userData.account.id));
|
||||
|
||||
|
|
@ -374,7 +334,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">
|
||||
{user ? (
|
||||
<span className="w-full h-full object-cover p-1.5 font-bold">
|
||||
{initial}
|
||||
{(user?.name?.slice(0, 2)).toUpperCase()}
|
||||
</span>
|
||||
) : (
|
||||
<FaUser className="text-lg" />
|
||||
|
|
@ -389,7 +349,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">
|
||||
{user ? (
|
||||
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold">
|
||||
{initial}
|
||||
{(user?.name?.slice(0, 2)).toUpperCase()}
|
||||
</span>
|
||||
) : (
|
||||
<FaUser className="text-lg" />
|
||||
|
|
@ -405,18 +365,6 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
</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">
|
||||
<button
|
||||
onClick={handleAccountsClick}
|
||||
|
|
@ -489,5 +437,20 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
|||
</div>
|
||||
</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;
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { FaCheck } from "react-icons/fa";
|
||||
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
|
||||
import { FaDatabase } from "react-icons/fa";
|
||||
import AccountNav from "./AccountNav";
|
||||
import api from "../../config/api";
|
||||
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Cards from "../home/Cards";
|
||||
import AddAccountModal from "./AddAccountModal";
|
||||
import { Bounce, toast } from "react-toastify";
|
||||
import Loader from "../../helper/Loader";
|
||||
|
||||
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
|
||||
|
||||
|
|
@ -32,9 +36,15 @@ const getStatusLabel = (
|
|||
const Accounts = () => {
|
||||
|
||||
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchAccounts = async () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
|
||||
// import OAuthGoogle from "./OAuthGoogle";
|
||||
import axios from "axios";
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
// import { FcGoogle } from "react-icons/fc";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import OAuthGoogle from "./OAuthGoogle";
|
||||
import { Bounce, toast } from "react-toastify";
|
||||
// import Cookies from "js-cookie";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { AppDispatch } from "../../store/store";
|
||||
import { BsEye } from "react-icons/bs";
|
||||
import { FiEyeOff } from "react-icons/fi";
|
||||
// import api from "../../config/api";
|
||||
|
|
@ -24,9 +27,6 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
|||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [errors, setErrors] = useState({ email: "", password: "" });
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const validateEmail = (email) => {
|
||||
|
|
@ -66,6 +66,15 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
|||
if (!password) {
|
||||
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 "";
|
||||
};
|
||||
|
||||
|
|
@ -89,6 +98,49 @@ 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 emailError = validateEmail(formData.email);
|
||||
const passwordError = validatePassword(formData.password);
|
||||
|
|
@ -115,16 +167,57 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
|||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
navigate("/");
|
||||
formErrorRef.current = { message: "Login Successfully", error: false }
|
||||
toast.dismiss();
|
||||
|
||||
// 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) {
|
||||
toast.dismiss();
|
||||
|
||||
if (err.response?.status === 400 || err.response?.status === 401) {
|
||||
formErrorRef.current = { message: "The email or password you entered is invalid.", error: true }
|
||||
} else if(err.response?.status === 404) {
|
||||
formErrorRef.current = { message: "Email not found. Please sign up to continue.", error: true }
|
||||
toast.error("Credentials Not Match", {
|
||||
position: "top-center",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "light",
|
||||
transition: Bounce,
|
||||
});
|
||||
} else {
|
||||
formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true }
|
||||
toast.error("Some Error Occurred", {
|
||||
position: "top-center",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "light",
|
||||
transition: Bounce,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
@ -212,9 +305,18 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
|||
</div>
|
||||
{/* )} */}
|
||||
|
||||
<p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
|
||||
|
||||
|
||||
{/* {!nextClick ? (
|
||||
<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
|
||||
onClick={handleFormLoginClick}
|
||||
disabled={isLoading}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
// import { GoogleSvg, SalesforceSvg } from "../svgs/Svg"
|
||||
import OAuthGoogle from "./OAuthGoogle"
|
||||
import { useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, Navigate, useNavigate } from "react-router-dom";
|
||||
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 { FiEyeOff } from "react-icons/fi";
|
||||
// import api from "../../config/api";
|
||||
|
|
@ -17,9 +23,7 @@ interface FormDataState {
|
|||
}
|
||||
|
||||
|
||||
const RegisterForm = ({ onSwitchToLogin }) => {
|
||||
|
||||
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
|
||||
const RegisterForm = ({ onSwitchToLogin }) => {
|
||||
const [formData, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
|
|
@ -171,68 +175,92 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
};
|
||||
|
||||
const handleFormRegisterClick = async () => {
|
||||
const firstNameError = validateFirstName(formData.firstName);
|
||||
const lastNameError = validateLastName(formData.lastName);
|
||||
const emailError = validateEmail(formData.email);
|
||||
const passwordError = validatePassword(formData.password);
|
||||
const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password);
|
||||
const firstNameError = validateFirstName(formData.firstName);
|
||||
const lastNameError = validateLastName(formData.lastName);
|
||||
const emailError = validateEmail(formData.email);
|
||||
const passwordError = validatePassword(formData.password);
|
||||
const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password);
|
||||
|
||||
const newErrors = {
|
||||
firstName: firstNameError,
|
||||
lastName: lastNameError,
|
||||
email: emailError,
|
||||
password: passwordError,
|
||||
confirmPassword: confirmPasswordError,
|
||||
};
|
||||
const newErrors = {
|
||||
firstName: firstNameError,
|
||||
lastName: lastNameError,
|
||||
email: emailError,
|
||||
password: passwordError,
|
||||
confirmPassword: confirmPasswordError,
|
||||
};
|
||||
|
||||
setErrors(newErrors);
|
||||
setErrors(newErrors);
|
||||
|
||||
// If any errors exist, stop
|
||||
if (Object.values(newErrors).some((error) => error !== "")) {
|
||||
return;
|
||||
}
|
||||
// If any errors exist, stop
|
||||
if (Object.values(newErrors).some((error) => error !== "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setIsLoading(true);
|
||||
|
||||
const trimmedData = {
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
email: formData.email.trim(),
|
||||
password: formData.password,
|
||||
confirmPassword: formData.confirmPassword,
|
||||
};
|
||||
const trimmedData = {
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
email: formData.email.trim(),
|
||||
password: formData.password,
|
||||
confirmPassword: formData.confirmPassword,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`${import.meta.env.VITE_BASE_URI}/api/auth/signup`,
|
||||
{
|
||||
firstName: trimmedData.firstName,
|
||||
lastName: trimmedData.lastName,
|
||||
email: trimmedData.email,
|
||||
password: trimmedData.password,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"ngrok-skip-browser-warning": "69420",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
const res = await axios.post(
|
||||
`${import.meta.env.VITE_BASE_URI}/api/auth/signup`,
|
||||
{
|
||||
firstName: trimmedData.firstName,
|
||||
lastName: trimmedData.lastName,
|
||||
email: trimmedData.email,
|
||||
password: trimmedData.password,
|
||||
},
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"ngrok-skip-browser-warning": "69420",
|
||||
"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 = () => {
|
||||
// Your Google OAuth logic here
|
||||
|
|
@ -268,8 +296,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
onChange={(e) => handleInputChange("firstName", e.target.value)}
|
||||
placeholder="First name"
|
||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
|
@ -284,8 +312,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
onChange={(e) => handleInputChange("lastName", e.target.value)}
|
||||
placeholder="Last name"
|
||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
|
@ -306,8 +334,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
|
@ -327,8 +355,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
|
@ -357,8 +385,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
||||
placeholder="Confirm your password"
|
||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.confirmPassword
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
? "border-red-500 focus:ring-red-500"
|
||||
: "border-gray-300 focus:ring-blue-500"
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
|
@ -376,14 +404,12 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
|
||||
|
||||
<button
|
||||
onClick={handleFormRegisterClick}
|
||||
disabled={isLoading}
|
||||
className={`w-full mt-2 py-2 rounded-lg font-medium transition-colors ${isLoading
|
||||
? "bg-gray-400 cursor-not-allowed"
|
||||
: "bg-blue-500 hover:bg-blue-600 text-white"
|
||||
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 ? "Creating Account..." : "Create Account"}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
// import "../styles/style.css";
|
||||
import "../styles/style.css";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import LoginForm from "./LoginForm";
|
||||
import RegisterForm from "./RegisterForm";
|
||||
|
|
@ -37,7 +37,7 @@ function UserAuthUI() {
|
|||
return (
|
||||
<div className="max-h-screen bg-white flex">
|
||||
{/* Left Side - Branding */}
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-[var(--primary)] text-white p-12 lg:h-[100vh]">
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-[var(--primary)] text-white p-12">
|
||||
<div className="flex flex-col items-center justify-center text-center w-full h-full gap-6">
|
||||
{/* Logos */}
|
||||
<div className="flex flex-col items-center gap-2 space-y-3">
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
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
|
|
@ -1,806 +0,0 @@
|
|||
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
|
||||
|
|
@ -11,9 +11,11 @@ import { clearTransaction, TRANSACTION_STATUS_ENUMS } from '../../store/slices/t
|
|||
import { resetSelDoc } from '../../store/slices/selectedDocsSlice'
|
||||
import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice'
|
||||
import { hideLoader, showLoader } from '../../store/slices/loaderSlice'
|
||||
import { Bounce, toast } from 'react-toastify'
|
||||
import SignModal from '../modal/SignModal'
|
||||
|
||||
import confetti from 'canvas-confetti'
|
||||
import { useWindowSize } from 'react-use'
|
||||
import { FaCheckCircle } from 'react-icons/fa'
|
||||
import { IoClose } from 'react-icons/io5'
|
||||
import CommonNav from '../CommonNav'
|
||||
|
|
@ -37,6 +39,13 @@ function Dashboard() {
|
|||
|
||||
// Check if cookie already exists - Redirect to Dashboard
|
||||
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) {
|
||||
|
||||
dispatch(resetSelDoc())
|
||||
|
|
@ -45,6 +54,22 @@ function Dashboard() {
|
|||
|
||||
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { LuSignature } from "react-icons/lu";
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store/store';
|
||||
import SMELogo from "../../assets/SMELogo2.png";
|
||||
import { FaFileSignature } from "react-icons/fa";
|
||||
|
||||
|
||||
const Loader = () => {
|
||||
|
|
@ -18,7 +16,7 @@ const 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='bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg'>
|
||||
<FaFileSignature className="text-4xl font-bold animate-text" />
|
||||
<LuSignature className="text-4xl font-bold animate-text" />
|
||||
<p>
|
||||
{loaderData.message != undefined ? `${loaderData.message}` : 'Loading ...'}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import { IoMdArrowDropdown } from "react-icons/io";
|
||||
import { IoIosSend } from "react-icons/io";
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setSignType, SignType } from '../../store/slices/selectedDocsSlice';
|
||||
import { RootState } from '../../store/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Bounce, toast } from 'react-toastify';
|
||||
import api from '../../config/api';
|
||||
import { StartCard } from '../home/StartSection';
|
||||
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice';
|
||||
import { SignTypeEnum } from '../../utlity/types';
|
||||
// import { IoMdArrowDropright } from "react-icons/io";
|
||||
|
|
@ -116,7 +120,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
|
|||
px-2 py-2.5
|
||||
overflow-hidden
|
||||
transition-all duration-300 ease-in-out w-full
|
||||
rounded
|
||||
${showSideBar ? "rounded-l" : "rounded"}
|
||||
`}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
|
|
@ -127,7 +131,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* {showSideBar && (
|
||||
{showSideBar && (
|
||||
<div
|
||||
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 }}
|
||||
|
|
@ -138,7 +142,7 @@ const SendForSign: React.FC<Props> = ({ showSideBar }) => {
|
|||
}`}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,10 +33,9 @@ import confetti from 'canvas-confetti'
|
|||
import { IoClose } from "react-icons/io5";
|
||||
import { IoMdClose, IoMdHome } from "react-icons/io";
|
||||
import { Bounce, toast } from "react-toastify";
|
||||
import NavTwo from '../processStepTwo/NavTwo';
|
||||
|
||||
|
||||
export type Signer = {
|
||||
type Signer = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
|
|
@ -45,7 +44,7 @@ export type Signer = {
|
|||
};
|
||||
|
||||
// Signers List Component
|
||||
export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||||
const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||||
return (
|
||||
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 ">
|
||||
<div className="p-2 ">
|
||||
|
|
@ -199,7 +198,6 @@ const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
|||
// </div>
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentSubmissionOptions = () => {
|
||||
const location = useLocation();
|
||||
const [status, setStatus] = useState(null);
|
||||
|
|
@ -226,6 +224,18 @@ const DocumentSubmissionOptions = () => {
|
|||
}, [])
|
||||
|
||||
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) {
|
||||
const duration = 3000;
|
||||
|
|
@ -304,6 +314,25 @@ const DocumentSubmissionOptions = () => {
|
|||
}
|
||||
}, [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 [isVisible, setIsVisible] = useState(false);
|
||||
const [previewData, setPreviewData] = useState({
|
||||
|
|
@ -411,6 +440,95 @@ const DocumentSubmissionOptions = () => {
|
|||
</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'}`}>
|
||||
|
||||
{/* 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="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" />
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react'
|
||||
import NavTwo from '../processStepTwo/NavTwo'
|
||||
|
||||
const FinalPreview = () => {
|
||||
return (
|
||||
<>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FinalPreview
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from ".
|
|||
import { SignTypeEnum } from "../../../utlity/types";
|
||||
import { RiPencilFill } from "react-icons/ri";
|
||||
import SelfSignWarnModal from "./SelfSignWarnModal";
|
||||
import AdvancedOptions from "./AdvancedOptions";
|
||||
|
||||
const ProcessStepOne = () => {
|
||||
interface EmailDtls {
|
||||
|
|
@ -26,29 +25,10 @@ const ProcessStepOne = () => {
|
|||
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;
|
||||
|
||||
const signType = useParams<{ signType: string }>().signType;
|
||||
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 }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
|
|
@ -122,12 +102,6 @@ const ProcessStepOne = () => {
|
|||
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 = () => {
|
||||
return selectedRecipients.reduce((min, recipient) => {
|
||||
|
|
@ -138,20 +112,11 @@ const ProcessStepOne = () => {
|
|||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const handleNextClick = async (saveAndClose: boolean = false) => {
|
||||
let formError = { ...FormSubmitIntitialState }
|
||||
|
||||
const recipientsData: { [key: string | number]: string[] } = {}
|
||||
for (const recipient of selectedRecipients) {
|
||||
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]
|
||||
}
|
||||
let formError = {
|
||||
documents: false,
|
||||
recipients: false,
|
||||
subject: false,
|
||||
recipientEmailAndLastName: false
|
||||
}
|
||||
|
||||
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
|
||||
|
|
@ -178,12 +143,29 @@ const ProcessStepOne = () => {
|
|||
|
||||
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
|
||||
|
||||
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName || formError.duplicateEmailSameRoutingOrder) {
|
||||
setSubmitError(formError)
|
||||
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName) {
|
||||
toast.dismiss();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
dispatch(showLoader("Processing Documents..."));
|
||||
|
||||
|
|
@ -206,7 +188,12 @@ const ProcessStepOne = () => {
|
|||
dispatch(showLoader("Processing Recipients..."));
|
||||
|
||||
let minRoutingOrder = getMinRoutingOrder();
|
||||
|
||||
// selectedRecipients.forEach((recipient) => {
|
||||
// if (recipient.routingOrder ?? 1 < minRoutingOrder) {
|
||||
// minRoutingOrder = recipient.routingOrder ?? 1;
|
||||
// }
|
||||
// });
|
||||
|
||||
const recipientsData = selectedRecipients.map((recipient, index) => ({
|
||||
id: "id" in recipient ? recipient.id : null,
|
||||
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
|
||||
|
|
@ -280,7 +267,7 @@ const ProcessStepOne = () => {
|
|||
}
|
||||
|
||||
} else {
|
||||
if (!accountId || !transactionId) return;
|
||||
if(!accountId || !transactionId) return;
|
||||
dispatch(addRecipientAsync({ recipientsData: recipientsDataTemp, accountId, transactionId }));
|
||||
}
|
||||
dispatch(showLoader("Processing Message..."));
|
||||
|
|
@ -316,6 +303,8 @@ const ProcessStepOne = () => {
|
|||
navigate(`/process-step-two/${accountId}/${transactionId}`);
|
||||
}
|
||||
}
|
||||
// }, 1000);
|
||||
// }
|
||||
|
||||
} catch (error) {
|
||||
dispatch(hideLoader());
|
||||
|
|
@ -351,82 +340,55 @@ const ProcessStepOne = () => {
|
|||
|
||||
const globalLoader = useSelector((state: RootState) => state.loader)
|
||||
|
||||
|
||||
if (showAdvancedOptions) {
|
||||
return (
|
||||
<AdvancedOptions setShowAdvancedOptions={setShowAdvancedOptions} />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}>
|
||||
|
||||
{/* Top Navigation - Fixed */}
|
||||
<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 flex-col sm:flex-row sm:items-center sm:justify-between gap-1 flex-wrap'>
|
||||
<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='flex items-center gap-2'>
|
||||
<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>
|
||||
|
||||
{/* Left Section */}
|
||||
<div className='flex sm:flex-row items-start sm:items-center gap-2 sm:gap-4 w-full sm:w-auto'>
|
||||
<div
|
||||
id="add-tags-btn-step-one"
|
||||
className='flex items-center gap-3'>
|
||||
|
||||
<IoMdClose
|
||||
onClick={handleClose}
|
||||
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition self-start sm:self-auto'
|
||||
/>
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <button
|
||||
onClick={() => handleNextClick(true)}
|
||||
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
|
||||
>
|
||||
Save & Close
|
||||
</button>}
|
||||
|
||||
<p className="text-base font-normal text-gray-700 border-l pl-4 relative w-full sm:w-auto">
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? (
|
||||
<>
|
||||
<input
|
||||
value={templateName}
|
||||
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>
|
||||
<button
|
||||
onClick={() => handleNextClick(false)}
|
||||
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
|
||||
>
|
||||
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
|
||||
</button>
|
||||
|
||||
{/* 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>
|
||||
|
||||
|
||||
{/* Main Content - Scrollable */}
|
||||
<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">
|
||||
|
|
@ -434,10 +396,7 @@ const ProcessStepOne = () => {
|
|||
{/* Documents */}
|
||||
<section>
|
||||
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
|
||||
<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>
|
||||
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
|
||||
</section>
|
||||
|
||||
{/* Recipients */}
|
||||
|
|
@ -463,26 +422,13 @@ const ProcessStepOne = () => {
|
|||
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
|
||||
<div className="flex flex-col gap-4 align-start">
|
||||
|
||||
<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>
|
||||
<RecipientCards userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} editRecipient={editRecipient} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} editRecipientCurrentEmail={editRecipientCurrentEmail} />
|
||||
|
||||
<div
|
||||
ref={topRef}
|
||||
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" : ""}`}
|
||||
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''}`}
|
||||
>
|
||||
<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>
|
||||
|
|
@ -494,21 +440,16 @@ const ProcessStepOne = () => {
|
|||
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
|
||||
|
||||
{/* Subject */}
|
||||
<div className={`${submitError.subject ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""} flex flex-col mb-4`}>
|
||||
<div className='flex flex-col mb-4'>
|
||||
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
|
||||
<input
|
||||
id="subject"
|
||||
type="text"
|
||||
required
|
||||
value={emailDtls.subject}
|
||||
onChange={(e) => {
|
||||
setEmailDtls({ ...emailDtls, subject: e.target.value })
|
||||
submitError.subject && setSubmitError(prev => ({ ...prev, subject: false }))
|
||||
}}
|
||||
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
|
||||
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)]"
|
||||
/>
|
||||
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.subject && "Enter a subject."}</p>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
|
|
|
|||
|
|
@ -363,26 +363,27 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
|||
|
||||
|
||||
|
||||
return (
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={handleRecipientAdd}
|
||||
id="add-recipient-step-one"
|
||||
className="w-full bg-white border border-gray-200 rounded-lg shadow-sm px-3 sm:px-4 lg:px-6 py-3 sm:py-4 space-y-3 sm:space-y-4 relative"
|
||||
className="w-full bg-white border border-gray-200 rounded-lg shadow-sm px-6 py-4 space-y-3 relative"
|
||||
>
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
{/* Role Name and Permissions Section - Responsive Layout */}
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-start gap-3 lg:gap-4">
|
||||
<div className="space-y-1">
|
||||
{/* Top Row: Role Input + Settings */}
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Role Name Input */}
|
||||
<div className="w-full lg:w-64 xl:w-80">
|
||||
{/* Role Name Input */}
|
||||
<div className="w-64">
|
||||
<input
|
||||
type="text"
|
||||
value={recipient.roleName}
|
||||
placeholder="Role name"
|
||||
className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none transition
|
||||
${errors.roleName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||
${errors.roleName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||
onChange={(e) => {
|
||||
setRecipients({ ...recipient, roleName: e.target.value });
|
||||
setErrors(prev => ({ ...prev, roleName: false }));
|
||||
|
|
@ -393,76 +394,125 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Permissions Section */}
|
||||
<div className="flex items-center justify-start lg:justify-end">
|
||||
<div className="flex sm:flex-row items-start sm:items-center gap-2 sm:gap-3">
|
||||
{/* Edit Permission */}
|
||||
<div className="relative group">
|
||||
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={recipient.allowSenderToEdit}
|
||||
onChange={(e) =>
|
||||
setRecipients({ ...recipient, allowSenderToEdit: e.target.checked })
|
||||
}
|
||||
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>
|
||||
|
||||
{/* Settings + Permissions */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex justify-between w-full">
|
||||
{/* Permission Status Text - Always Visible */}
|
||||
<div className="max-h-10 opacity-100 me-3 overflow-hidden">
|
||||
<div className="text-sm text-gray-600 pt-0.5">
|
||||
{/* {recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Full permissions granted"}
|
||||
{recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "Edit permission granted"}
|
||||
{!recipient.allowSenderToEdit && recipient.allowSenderToDelete && "Delete permission granted"}
|
||||
{!recipient.allowSenderToEdit && !recipient.allowSenderToDelete && "No permissions granted"} */}
|
||||
Permissions granted :
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Permission */}
|
||||
<div className="relative group">
|
||||
<label className="flex items-center gap-1 text-xs sm:text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={recipient.allowSenderToDelete}
|
||||
onChange={(e) =>
|
||||
setRecipients({ ...recipient, allowSenderToDelete: e.target.checked })
|
||||
}
|
||||
onFocus={() => setOpenContactModal(true)}
|
||||
onBlur={handleContactModalBlur}
|
||||
className="accent-red-500 w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
<span className="whitespace-nowrap">Allow Delete</span>
|
||||
</label>
|
||||
{/* Permission Controls - Always Visible */}
|
||||
<div className="max-w-80 opacity-100 overflow-visible">
|
||||
<div className="flex items-center space-x-3 pr-1.5">
|
||||
{/* Edit Permission */}
|
||||
<div className="relative group">
|
||||
<label
|
||||
className={`relative flex items-center justify-center w-12 h-7 border-2 rounded-lg cursor-pointer transition-all duration-300 transform hover:scale-105 ${recipient.allowSenderToEdit
|
||||
? 'bg-blue-100 border-blue-500 shadow-md'
|
||||
: 'bg-white border-gray-300 hover:border-blue-400 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={recipient.allowSenderToEdit}
|
||||
onChange={(e) =>
|
||||
setRecipients({ ...recipient, allowSenderToEdit: e.target.checked })
|
||||
}
|
||||
onFocus={() => setOpenContactModal(true)}
|
||||
onBlur={handleContactModalBlur}
|
||||
className="absolute w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<FiEdit
|
||||
className={`w-2.5 h-2.5 pointer-events-none transition-all duration-300 ${recipient.allowSenderToEdit ? 'text-blue-600' : 'text-gray-400'
|
||||
}`}
|
||||
/><span className="ms-0.5 text-[12px]">Edit</span>
|
||||
</label>
|
||||
{/* Tooltip */}
|
||||
<div className="absolute bottom-0 -translate-y-full left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-20">
|
||||
<div className="bg-gray-800 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap">
|
||||
Edit Permission
|
||||
<div className="absolute bottom-[-5px] left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Name and Email Inputs - Responsive Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 mt-3">
|
||||
{/* First Name */}
|
||||
<div className="w-full">
|
||||
{/* Name Inputs */}
|
||||
<div className="grid grid-cols-12 gap-4 mt-3">
|
||||
<div className="col-span-4">
|
||||
<input
|
||||
type="text"
|
||||
onFocus={() => handleFocus("firstName")}
|
||||
onBlur={handleContactModalBlur}
|
||||
value={recipient.firstName}
|
||||
placeholder="First Name"
|
||||
className={`w-full px-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)]'}`}
|
||||
className={`w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition
|
||||
${errors.firstName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||
onChange={(e) => {
|
||||
setRecipients({ ...recipient, firstName: e.target.value });
|
||||
// setErrors((prev) => ({ ...prev, firstName: false }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="w-full">
|
||||
{/* {errors.firstName && (
|
||||
<p className="text-red-500 text-xs mt-1">First Name is required</p>
|
||||
)} */}
|
||||
<div className="col-span-4">
|
||||
<input
|
||||
type="text"
|
||||
onFocus={() => handleFocus("lastName")}
|
||||
onBlur={handleContactModalBlur}
|
||||
value={recipient.lastName}
|
||||
placeholder="Last Name"
|
||||
className={`w-full px-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)]'}`}
|
||||
className={` w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition
|
||||
${errors.lastName ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||
onChange={(e) => {
|
||||
setRecipients({ ...recipient, lastName: e.target.value });
|
||||
setErrors((prev) => ({ ...prev, lastName: false }));
|
||||
|
|
@ -472,17 +522,15 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
|||
<p className="text-red-500 text-xs mt-1">Last Name is required</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email - Full width on small screens, shares row on larger */}
|
||||
<div className="w-full sm:col-span-2 lg:col-span-1">
|
||||
<div className="col-span-4">
|
||||
<input
|
||||
type="email"
|
||||
onFocus={() => handleFocus("email")}
|
||||
onBlur={handleContactModalBlur}
|
||||
value={recipient.email}
|
||||
placeholder="Recipient Email"
|
||||
className={`w-full px-3 sm:px-4 py-2 border rounded-md text-sm focus:outline-none transition
|
||||
${errors.email || errors.emailFormat ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||
className={`w-full px-4 py-2 border rounded-md text-sm focus:outline-none transition
|
||||
${errors.email ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||
onChange={(e) => {
|
||||
setRecipients({ ...recipient, email: e.target.value });
|
||||
setErrors((prev) => ({ ...prev, email: false }));
|
||||
|
|
@ -491,41 +539,36 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
|||
{errors.email && (
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Contact Modal - Responsive Positioning */}
|
||||
{recipient.type === recipientType.INPERSONSIGNER && <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<input
|
||||
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
|
||||
type="text"
|
||||
onFocus={() => setOpenContactModal(true)}
|
||||
onBlur={handleContactModalBlur}
|
||||
value={recipient.hostName}
|
||||
placeholder="Host Name"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||
onChange={(e) => setRecipients({ ...recipient, hostName: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
required={transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES}
|
||||
type="email"
|
||||
onFocus={() => setOpenContactModal(true)}
|
||||
onBlur={handleContactModalBlur}
|
||||
value={recipient.hostEmail}
|
||||
placeholder="Host Email"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||
onChange={(e) => setRecipients({ ...recipient, hostEmail: e.target.value })}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
{/* Contact Modal */}
|
||||
{openContactModal && contactData.contacts.length > 0 && (
|
||||
<div className="absolute z-50 mt-1 w-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">
|
||||
<div className="absolute z-50 mt-1 w-2/3 max-h-[50vh] bg-white shadow-lg rounded-md p-3 border border-gray-100 overflow-y-auto no-scrollbar">
|
||||
<ContactModal
|
||||
contactData={contactData}
|
||||
inpFirstName={recipient.firstName}
|
||||
|
|
@ -540,41 +583,36 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
|||
</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">
|
||||
{/* Recipient Type Radio Options */}
|
||||
<div className="flex flex-col sm:flex-row sm:flex-wrap gap-2 sm:gap-4 lg:gap-6">
|
||||
|
||||
|
||||
{/* Signing options + Add button in one line */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex flex-wrap gap-6">
|
||||
{[
|
||||
{ label: "Signer", value: recipientType.SIGNER },
|
||||
{ label: "In Person Signer", value: recipientType.INPERSONSIGNER },
|
||||
{ label: "CC", value: recipientType.CC },
|
||||
].map((option) => (
|
||||
<label key={option.value} className="flex items-center gap-2 text-gray-700 text-xs sm:text-sm cursor-pointer">
|
||||
<label key={option.value} className="flex items-center gap-2 text-gray-700 text-sm cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="recipient-type"
|
||||
value={option.value}
|
||||
checked={recipient.type === option.value}
|
||||
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
|
||||
className="accent-[var(--primary)] cursor-pointer w-3 h-3 sm:w-4 sm:h-4"
|
||||
className="accent-[var(--primary)] cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap">{option.label}</span>
|
||||
{option.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Button */}
|
||||
{!showCard && (
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
<FaPlus size={12} className="sm:w-3.5 sm:h-3.5" />
|
||||
<span className="whitespace-nowrap">
|
||||
{editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{!showCard && <button
|
||||
type="submit"
|
||||
className="inline-flex items-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-sm font-semibold px-5 py-2 rounded-md transition"
|
||||
>
|
||||
<FaPlus size={14} /> {editCase === "Edit" ? "Edit Recipient" : "Add Recipient"}
|
||||
</button>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ import { IoCloseOutline } from "react-icons/io5";
|
|||
import { useParams } from "react-router-dom";
|
||||
import { FiEdit } from "react-icons/fi";
|
||||
import { FaUsersLine } from "react-icons/fa6";
|
||||
import { IoMdClose } from "react-icons/io";
|
||||
import { BiSolidHelpCircle } from "react-icons/bi";
|
||||
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
if (!name) return "";
|
||||
|
|
@ -133,28 +130,48 @@ const RecipientCards = ({
|
|||
) => {
|
||||
const identifier = getRecipientIdentifier(recipient);
|
||||
const currentValue = getCurrentValue(recipient, fieldName);
|
||||
const isEditing = editingField?.recipientId === identifier && editingField?.fieldName === fieldName;
|
||||
const showValue = hasValue(recipient, fieldName);
|
||||
const error = errors[identifier]?.[fieldName];
|
||||
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 (
|
||||
<div className="relative inline-block w-full sm:mx-1 lg:mx-3">
|
||||
<input
|
||||
type={type}
|
||||
value={currentValue || ""}
|
||||
placeholder={placeholder}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
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>
|
||||
<span
|
||||
onClick={editable ? () => handleFieldFocus(identifier, fieldName) : undefined}
|
||||
className={`${editable
|
||||
? "cursor-pointer hover:bg-gray-100 rounded px-1 transition-colors"
|
||||
: "cursor-not-allowed"
|
||||
} ${showValue ? "text-gray-800" : "text-gray-400 italic"} ${className}`}
|
||||
title={editable ? "Click to edit" : "Editing disabled"}
|
||||
>
|
||||
{showValue ? currentValue : placeholder}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -223,12 +240,12 @@ const RecipientCards = ({
|
|||
{recipients.length > 0 ? (
|
||||
<>
|
||||
{recipients.length > 1 && (
|
||||
<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">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-300">
|
||||
<h3 className="text-sm font-semibold text-gray-700">
|
||||
Recipients ({recipients.length})
|
||||
</h3>
|
||||
<label
|
||||
className="flex items-center gap-2 cursor-pointer select-none justify-center sm:justify-end"
|
||||
className="flex items-center gap-2 cursor-pointer select-none"
|
||||
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}
|
||||
>
|
||||
<span className="text-xs font-medium text-gray-600">Signing Order</span>
|
||||
|
|
@ -237,42 +254,30 @@ const RecipientCards = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col divide-y divide-gray-200 max-h-[47vh] overflow-y-auto custom-scrollbar">
|
||||
<div className="flex flex-col divide-y divide-gray-200">
|
||||
{recipients.map((recipient, idx) => (
|
||||
<div
|
||||
key={recipient.email + idx}
|
||||
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"
|
||||
: ""
|
||||
className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
|
||||
? "bg-neutral-100 opacity-40"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{/* Main Content Section */}
|
||||
<div className="flex items-start sm:items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
||||
{/* Order Number */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
{recipients.length > 1 ? (
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
value={recipient.routingOrder}
|
||||
// min={1}
|
||||
value={recipient.routingOrder || 1}
|
||||
min={1}
|
||||
onChange={(e) =>
|
||||
handleOrderChange(
|
||||
recipient.id || recipient.uuid,
|
||||
recipient.id || recipient.email,
|
||||
Number(e.target.value)
|
||||
)
|
||||
}
|
||||
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"
|
||||
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"
|
||||
/>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
<input
|
||||
|
|
@ -282,163 +287,144 @@ const RecipientCards = ({
|
|||
min={1}
|
||||
onChange={(e) =>
|
||||
handleOrderChange(
|
||||
recipient.id || recipient.uuid,
|
||||
recipient.id || recipient.email,
|
||||
Number(e.target.value)
|
||||
)
|
||||
}
|
||||
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"
|
||||
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
|
||||
</div> */}
|
||||
|
||||
{/* Form Fields Section */}
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Role Name and Permissions Row */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <div className="flex-1 lg:w-1/2">
|
||||
<div className="font-semibold text-gray-800 text-xs">
|
||||
{renderEditableField(recipient, "roleName", "Role name")}
|
||||
</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 className="min-w-0">
|
||||
<p className="font-semibold text-gray-800 truncate text-xs flex items-center gap-0.5">
|
||||
{renderEditableField(recipient, "firstName", "First Name")}
|
||||
{renderEditableField(recipient, "lastName", "Last Name")}
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-gray-400 ">| Permissions granted :</span>
|
||||
<FiEdit
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
{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 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>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{signType !== "self-sign" && (
|
||||
<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">
|
||||
<div className="flex justify-between gap-2 items-center">
|
||||
<button
|
||||
type="button"
|
||||
title="Remove recipient"
|
||||
|
|
@ -449,9 +435,9 @@ const RecipientCards = ({
|
|||
? false
|
||||
: !recipient.newOne && !recipient.allowSenderToDelete
|
||||
}
|
||||
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50 p-1"
|
||||
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<ImBin size={14} className="sm:w-4 sm:h-4" />
|
||||
<ImBin size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -460,27 +446,24 @@ const RecipientCards = ({
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="p-4 sm:p-6 text-center text-gray-500 font-medium text-sm">
|
||||
<div className="p-6 text-center text-gray-500 font-medium text-sm">
|
||||
No Recipients Added
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Responsive Modal */}
|
||||
{signingOrderModal.open && (
|
||||
<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 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-3 sm:p-4 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
|
||||
<p className="font-semibold text-sm sm:text-base">Signing Order</p>
|
||||
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center">
|
||||
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full overflow-auto">
|
||||
<div className="p-2 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
|
||||
<p className="font-semibold">Signing Order</p>
|
||||
<div
|
||||
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
|
||||
className="text-xl sm:text-2xl cursor-pointer hover:text-gray-200 transition-all p-1"
|
||||
className="text-xl cursor-pointer hover:text-gray-200 transition-all"
|
||||
>
|
||||
<IoCloseOutline />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-b-lg">
|
||||
<SigningOrderDiagram signers={recipients} />
|
||||
</div>
|
||||
<SigningOrderDiagram signers={recipients} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -90,26 +90,15 @@ const SelectedDocuments = ({ transactionTemplateToggle }) => {
|
|||
|
||||
// Get proper Tailwind class names for upload card span
|
||||
const getUploadCardSpanClass = () => {
|
||||
if (docCount === 0) {
|
||||
// Upload card takes all columns only when no docs
|
||||
return "col-span-1 sm:col-span-2 md:col-span-3 lg:col-span-4 xl:col-span-5";
|
||||
}
|
||||
|
||||
switch (docCount) {
|
||||
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";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
switch (docCount) {
|
||||
case 0: return 'col-span-5'; // Full width when no docs
|
||||
case 1: return 'col-span-4'; // 4 cols for upload, 1 for doc
|
||||
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
|
||||
default: return 'col-span-1'; // 1 col for upload, rest for docs
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate how many documents should be in the first row
|
||||
const getFirstRowDocCount = () => {
|
||||
|
|
@ -124,151 +113,148 @@ const SelectedDocuments = ({ transactionTemplateToggle }) => {
|
|||
return (
|
||||
<>
|
||||
{/* 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
|
||||
id="upload-doc-step-one"
|
||||
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-5 gap-2"
|
||||
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`}
|
||||
>
|
||||
{/* Upload Document Card */}
|
||||
<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>
|
||||
<FaPlus className="text-[var(--primary)] text-3xl mb-2" />
|
||||
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p>
|
||||
</div>
|
||||
|
||||
{/* Additional Rows */}
|
||||
{remainingDocs > 0 && (
|
||||
<ReactSortable
|
||||
className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-5 gap-2"
|
||||
list={documents.slice(firstRowDocCount).map((doc) => ({ ...doc }))}
|
||||
setList={(newState) => {
|
||||
const updatedDocs = [
|
||||
...documents.slice(0, firstRowDocCount),
|
||||
...newState,
|
||||
];
|
||||
dispatch(setDocuments(updatedDocs));
|
||||
}}
|
||||
handle=".drag-handle"
|
||||
animation={200}
|
||||
ghostClass="sortable-ghost"
|
||||
disabled={false}
|
||||
>
|
||||
{documents.slice(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>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-2 sm:px-4">
|
||||
<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">
|
||||
{/* Close Button */}
|
||||
<button
|
||||
className="absolute top-2 right-2 text-gray-500 hover:text-red-500 transition text-xl bg-white rounded-full p-2"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
<IoMdClose />
|
||||
</button>
|
||||
|
||||
{/* Sidebar Tabs */}
|
||||
<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">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`flex items-center gap-2 p-3 rounded-md text-sm font-medium transition whitespace-nowrap ${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-full sm:w-3/4 p-4 sm: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>
|
||||
<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>
|
||||
{/* First Row Documents */}
|
||||
<ReactSortable
|
||||
className="contents" // Use 'contents' to make children participate in parent grid
|
||||
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">
|
||||
<DocumentCard
|
||||
id={doc.id}
|
||||
name={doc.name}
|
||||
isCompleted={
|
||||
("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED"))
|
||||
? true
|
||||
: false
|
||||
}
|
||||
transactionTemplateToggle={transactionTemplateToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
|
||||
{/* Additional Rows - Remaining Documents (5th, 6th, 7th, etc.) */}
|
||||
{remainingDocs > 0 && (
|
||||
<ReactSortable
|
||||
className="grid grid-cols-5 gap-2"
|
||||
list={documents.slice(firstRowDocCount).map(doc => ({ ...doc }))}
|
||||
setList={(newState) => {
|
||||
const updatedDocs = [
|
||||
...documents.slice(0, firstRowDocCount),
|
||||
...newState
|
||||
];
|
||||
dispatch(setDocuments(updatedDocs));
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-4">
|
||||
<div className="bg-white w-full max-w-5xl h-[500px] rounded-xl shadow-lg flex overflow-hidden relative">
|
||||
{/* Close Button */}
|
||||
<button
|
||||
className="absolute top-2 right-2 text-gray-500 hover:text-red-500 transition text-xl bg-white rounded-full p-2"
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
<IoMdClose />
|
||||
</button>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useLayoutEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../store/store'
|
||||
|
||||
|
|
@ -35,23 +35,17 @@ export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
|
|||
}
|
||||
|
||||
interface DocumentContainerProps {
|
||||
transactionSaving?: boolean;
|
||||
setTransactionSaving?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isSend?: boolean | null;
|
||||
setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>;
|
||||
isPreview?: boolean;
|
||||
aId?: string;
|
||||
tId?: string;
|
||||
transactionSaving: boolean;
|
||||
setTransactionSaving: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isSend: boolean | null;
|
||||
setIsSend: React.Dispatch<React.SetStateAction<boolean | null>>;
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving, setTransactionSaving, isSend }) => {
|
||||
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
|
||||
const dispatch = useDispatch();
|
||||
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
|
||||
let accountId = params.accountId;
|
||||
const accountId = params.accountId;
|
||||
let transactionId = params.transactionId
|
||||
if (!transactionId) {
|
||||
transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
|
||||
|
|
@ -63,16 +57,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
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 navigate = useNavigate();
|
||||
const childRef = useRef<any>(null);
|
||||
const tagUndoRef = useRef<DroppedItem[] | []>([]);
|
||||
|
||||
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||
|
|
@ -80,11 +66,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
|
||||
const [droppedItems, setDroppedItems] = useState<DroppedItemsObj>({});
|
||||
|
||||
|
||||
interface HotKeysObj {
|
||||
isActive: boolean;
|
||||
key: string | null;
|
||||
tagId?: string | null;
|
||||
hotKey?: string | null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -153,8 +141,10 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
if (selectedRecipients.length === 0) return;
|
||||
fetchTags();
|
||||
}, [selectedRecipients]);
|
||||
|
||||
// FETCHING TAG - END
|
||||
|
||||
|
||||
const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1);
|
||||
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
|
||||
|
||||
|
|
@ -223,7 +213,9 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
})
|
||||
)
|
||||
|
||||
setDroppedItems(droppedItemsUpdateFalse);
|
||||
console.log(droppedItemsUpdateFalse, "Line 182")
|
||||
|
||||
// setDroppedItems(droppedItemsUpdateFalse);
|
||||
} catch (error) {
|
||||
console.error("Error submitting recipient tags:", error);
|
||||
}
|
||||
|
|
@ -243,7 +235,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
|
||||
let tagsData = groupByRecipientId(updatedTags);
|
||||
await sendTagsToRecipients(tagsData, accountId, transactionId);
|
||||
// return
|
||||
return
|
||||
// SENDING Transaction - START
|
||||
if (!save) {
|
||||
const res = await api.put(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" })
|
||||
|
|
@ -351,13 +343,12 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
setTimeout(() => {
|
||||
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
||||
console.log(tagIndex, "line 307", key);
|
||||
|
||||
setSelectedTag({ key, tagIndex })
|
||||
}, 0)
|
||||
};
|
||||
// HANDLE DROP - END
|
||||
|
||||
console.log(selectedTag, "Line 359")
|
||||
|
||||
useEffect(() => {
|
||||
if (!hotKeysObj.isActive) return
|
||||
|
||||
|
|
@ -365,24 +356,9 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
|
||||
switch (hotKeysObj.hotKey) {
|
||||
case "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)
|
||||
// Handle Ctrl+C
|
||||
break;
|
||||
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
|
||||
break;
|
||||
case "Ctrl+x":
|
||||
|
|
@ -396,11 +372,12 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
break;
|
||||
case "Ctrl+a":
|
||||
docCon?.querySelectorAll("[data-tag-id]").forEach(tag => {
|
||||
tag.classList.add("border-2", "border-sky-700")
|
||||
tag.classList.add("border-2", "border-slate-800")
|
||||
tag.setAttribute("all-select-group", "true")
|
||||
|
||||
docCon.addEventListener("click", () => {
|
||||
tag.classList.remove("border-2", "border-sky-800")
|
||||
console.log("focus in", tag)
|
||||
tag.classList.remove("border-2", "border-slate-800")
|
||||
tag.removeAttribute("all-select-group")
|
||||
})
|
||||
// docCon.addEventListener("focusout", () => {
|
||||
|
|
@ -411,15 +388,9 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
});
|
||||
break;
|
||||
case "delete":
|
||||
const allselectedTags = docCon?.querySelectorAll("[all-select-group]") ?? []
|
||||
allselectedTags.forEach((tag) => {
|
||||
const tagIndex = tag.getAttribute("data-tag-id") ?? "";
|
||||
const key = tag.getAttribute("data-drop-key") ?? "";
|
||||
|
||||
childRef.current.handleRemoveTag(key, Number(tagIndex))
|
||||
docCon?.querySelectorAll("[all-select-group]").forEach((tag) => {
|
||||
console.log(tag, "TAGID")
|
||||
})
|
||||
|
||||
if (allselectedTags.length === 0) childRef.current.handleRemoveTag(selectedTag?.key, selectedTag?.index)
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -428,9 +399,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
}, [hotKeysObj])
|
||||
|
||||
const handleDocumentFocus = (key: string) => {
|
||||
// const id = key.replace("_", "");
|
||||
const targetCon = document.getElementById('doc-con');
|
||||
selectedDoc.key = key;
|
||||
const id = key.replace("_", "");
|
||||
const targetCon = document.getElementById(id);
|
||||
|
||||
targetCon?.addEventListener("keydown", (event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -455,7 +425,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
|
||||
setHotKeysObj({
|
||||
isActive: true,
|
||||
key: key,
|
||||
key: pressedKey,
|
||||
tagId: null,
|
||||
hotKey: hotKey
|
||||
});
|
||||
|
|
@ -469,18 +439,18 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
return (
|
||||
<>
|
||||
{/* 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) => {
|
||||
return (
|
||||
<div key={indxDoc} className='flex flex-col overflow-x-auto'>
|
||||
<div key={indxDoc} className='flex flex-col'>
|
||||
<div
|
||||
className='py-2 text-sm'>
|
||||
<p className='text-center'>{doc.name}</p>
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%] ${isPreview ? "pointer-events-none" : ""}`}>
|
||||
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
|
||||
{
|
||||
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
|
||||
const key = `${doc.id}_${page.index}`;
|
||||
|
|
@ -496,7 +466,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
>
|
||||
|
||||
<DroppedItemsTagsCon
|
||||
ref={childRef}
|
||||
droppedItems={droppedItems}
|
||||
pageIdkey={key}
|
||||
selectedTag={selectedTag}
|
||||
|
|
@ -506,7 +475,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
|||
accountId={accountId}
|
||||
transactionId={transactionId}
|
||||
transactionTemplateToggle={transactionTemplateToggle}
|
||||
tagUndoRef={tagUndoRef}
|
||||
/>
|
||||
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
// -- handleGroupBorderPosition
|
||||
// -- handleAddGroupItemClicked
|
||||
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { DroppedItemsObj } from './DocumentContainer';
|
||||
import typeToIconMap from '../../../helper/iconMap';
|
||||
import { IoMdClose } from 'react-icons/io';
|
||||
|
|
@ -16,7 +16,6 @@ import { IoAdd } from 'react-icons/io5';
|
|||
import { FaQuestionCircle } from 'react-icons/fa';
|
||||
import api from '../../../config/api';
|
||||
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
|
||||
import { DroppedItem } from '../../../utlity/types';
|
||||
|
||||
interface DroppedItemsTagsConProps {
|
||||
droppedItems: DroppedItemsObj;
|
||||
|
|
@ -28,11 +27,9 @@ interface DroppedItemsTagsConProps {
|
|||
accountId: string;
|
||||
transactionId: string;
|
||||
transactionTemplateToggle: string;
|
||||
tagUndoRef: React.MutableRefObject<DroppedItem[]>;
|
||||
}
|
||||
|
||||
// 👶 Child component with forwardRef
|
||||
const DroppedItemsTagsCon = forwardRef(({
|
||||
const DroppedItemsTagsCon: React.FC<DroppedItemsTagsConProps> = ({
|
||||
droppedItems,
|
||||
pageIdkey,
|
||||
selectedTag,
|
||||
|
|
@ -41,24 +38,19 @@ const DroppedItemsTagsCon = forwardRef(({
|
|||
setDroppedItems,
|
||||
accountId,
|
||||
transactionId,
|
||||
transactionTemplateToggle,
|
||||
tagUndoRef
|
||||
}: DroppedItemsTagsConProps, ref) => {
|
||||
transactionTemplateToggle
|
||||
}) => {
|
||||
|
||||
const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 });
|
||||
|
||||
const handleTagClick = (key: string, index: number) => {
|
||||
console.log(index, "line 307", key);
|
||||
|
||||
setSelectedTag({ key, index })
|
||||
}
|
||||
|
||||
const handleRemoveTag = (key: string, targetIndex: number) => {
|
||||
if(key === undefined || targetIndex === undefined) return
|
||||
|
||||
const tag = droppedItems[key][targetIndex];
|
||||
const recipientId = tag.recipientId;
|
||||
|
||||
|
||||
(async () => {
|
||||
if ("id" in tag) {
|
||||
try {
|
||||
|
|
@ -70,8 +62,6 @@ const DroppedItemsTagsCon = forwardRef(({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSelectedTag(null);
|
||||
|
||||
// Check if this item has a group for special group handling
|
||||
|
|
@ -146,10 +136,6 @@ const DroppedItemsTagsCon = forwardRef(({
|
|||
})()
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRemoveTag
|
||||
}));
|
||||
|
||||
// HANDLE TAG MOUSE DOWN - START
|
||||
// Updated handleTagMouseDown function with proper group border positioning
|
||||
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
||||
|
|
@ -550,7 +536,6 @@ const DroppedItemsTagsCon = forwardRef(({
|
|||
// key={index}
|
||||
draggable="false"
|
||||
data-tag-id={index}
|
||||
data-drop-key={pageIdkey}
|
||||
data-tag-selected={index === selectedTag?.index}
|
||||
className="absolute rounded dropped-item"
|
||||
onClick={() => handleTagClick(pageIdkey, index)}
|
||||
|
|
@ -636,6 +621,6 @@ const DroppedItemsTagsCon = forwardRef(({
|
|||
{/* Render only the dropped items for this specific docId and page - ENDS */}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default DroppedItemsTagsCon
|
||||
|
|
@ -12,18 +12,14 @@ import { fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE
|
|||
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Bounce, toast } from 'react-toastify'
|
||||
import { FiChevronDown } from 'react-icons/fi'
|
||||
|
||||
interface NavTwoProps {
|
||||
transactionSaving?: boolean;
|
||||
transactionSaving: boolean;
|
||||
// setTransactionSaving: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isFields?: boolean;
|
||||
setIsFields?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isSend?: boolean | null;
|
||||
setIsSend?: React.Dispatch<React.SetStateAction<boolean | null>>
|
||||
isPreview?: boolean
|
||||
aId?: string | null
|
||||
tId?: string | null
|
||||
isFields: boolean;
|
||||
setIsFields: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isSend: boolean | null;
|
||||
setIsSend: React.Dispatch<React.SetStateAction<boolean | null>>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -51,11 +47,11 @@ const fitObj = [
|
|||
},
|
||||
]
|
||||
|
||||
const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, setIsFields, isSend, setIsSend, isPreview = false, aId = null, tId = null }) => {
|
||||
const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving, isFields, setIsFields, isSend, setIsSend }) => {
|
||||
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
|
||||
const dispatch = useDispatch();
|
||||
const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>();
|
||||
let accountId = params.accountId;
|
||||
const accountId = params.accountId;
|
||||
let transactionId = params.transactionId
|
||||
if (!transactionId) {
|
||||
transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES;
|
||||
|
|
@ -67,12 +63,7 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
|||
return null;
|
||||
}
|
||||
|
||||
// If is preview is enable
|
||||
if (isPreview && aId !== null && tId !== null) {
|
||||
accountId = aId;
|
||||
transactionId = tId;
|
||||
}
|
||||
|
||||
|
||||
// const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
|
||||
const [dotCount, setDotCount] = useState(0);
|
||||
|
||||
|
|
@ -139,8 +130,8 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
|||
if (isSignSend) {
|
||||
window.open(`./../../../sign/${selfUserSign.recipientId}`, "_self");
|
||||
}
|
||||
else {
|
||||
navigate(`/transaction-sent/${accountId}/${transactionId}`, { replace: true })
|
||||
else {
|
||||
navigate(`/document-completion-screen/${accountId}/${transactionId}`, { replace: true })
|
||||
}
|
||||
} else {
|
||||
navigate(`/templates`);
|
||||
|
|
@ -243,12 +234,12 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
|||
</div>
|
||||
|
||||
|
||||
{!isPreview && <div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||
<RiResetRightLine
|
||||
className={`${transactionSaving ? "animate-spin" : ""} transition duration-300`}
|
||||
/>
|
||||
<p className="tracking-wide transition-all">{transactionSaving ? `Saving ${dots}` : "Save"}</p>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -256,32 +247,6 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
|||
{/* Right Side - STARTS */}
|
||||
<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 */}
|
||||
<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'>
|
||||
|
|
@ -328,26 +293,24 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
|||
</div>
|
||||
|
||||
{/* Send */}
|
||||
{!isPreview && <>
|
||||
<div
|
||||
id='send-sign-dash-btn'
|
||||
onClick={() => handleSend()}
|
||||
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
|
||||
<p className='tracking-wide text-white'>
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"}
|
||||
</p>
|
||||
<FaSignature className='text-white text-md' />
|
||||
</div>
|
||||
{transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign && <div
|
||||
id='send-and-sign-dash-btn'
|
||||
onClick={() => handleSend(selfUserSign.sign)}
|
||||
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
|
||||
<p className='tracking-wide text-white'>
|
||||
Sign & Send
|
||||
</p>
|
||||
<FaSignature className='text-white text-md' />
|
||||
</div>}
|
||||
</>}
|
||||
<div
|
||||
id='send-sign-dash-btn'
|
||||
onClick={() => handleSend()}
|
||||
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
|
||||
<p className='tracking-wide text-white'>
|
||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES ? "Save" : "Send"}
|
||||
</p>
|
||||
<FaSignature className='text-white text-md' />
|
||||
</div>
|
||||
{transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && selfUserSign.sign && <div
|
||||
id='send-and-sign-dash-btn'
|
||||
onClick={() => handleSend(selfUserSign.sign)}
|
||||
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
|
||||
<p className='tracking-wide text-white'>
|
||||
Sign & Send
|
||||
</p>
|
||||
<FaSignature className='text-white text-md' />
|
||||
</div>}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -302,7 +302,9 @@ const RightSideBar: React.FC<DroppedItemProps> = ({ droppedItems, selectedTag, s
|
|||
<CurrTimeStampAttr type={selectedTag.type} handlePropertyChange={handlePropertyChange} />
|
||||
}
|
||||
|
||||
<LabelAttr label={selectedTag.label ?? ""} handlePropertyChange={handlePropertyChange} />
|
||||
{selectedTag.label &&
|
||||
<LabelAttr label={selectedTag.label} handlePropertyChange={handlePropertyChange} />}
|
||||
|
||||
<ToolTipAttr tooltip={selectedTag.tooltip ?? ""} handlePropertyChange={handlePropertyChange} />
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import logoRecord from '../../assets/record.svg';
|
||||
import LoaderUi from '../../helper/LoaderUi';
|
||||
import api from '../../config/api';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
import { TemplateInterface } from '../../utlity/types';
|
||||
|
||||
const TemplateTable = () => {
|
||||
const loaction = useLocation()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [templates, setTemplates] = useState<TemplateInterface[] | []>([])
|
||||
const [templates, setTemplates] = useState([])
|
||||
const accountDetails = useSelector((state: any) => state.user.account);
|
||||
const accountId = accountDetails.id || "";
|
||||
const [showMenu, setShowMenu] = useState(null);
|
||||
|
|
@ -87,7 +88,7 @@ const TemplateTable = () => {
|
|||
templateId: item.id,
|
||||
};
|
||||
let res = await api.post(`/api/accounts/${activeAccountId}/transactions`, payload)
|
||||
|
||||
|
||||
// /process-step-one/:accountId/:transactionId
|
||||
navigate(`/process-step-one/${activeAccountId}/${res?.data?.id}?useTemplate=true`);
|
||||
} catch (error) {
|
||||
|
|
@ -138,7 +139,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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -180,14 +181,14 @@ const TemplateTable = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* <td className="px-6 py-4 text-center">
|
||||
<td className="px-6 py-4 text-center">
|
||||
<button
|
||||
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"
|
||||
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"
|
||||
onClick={() => handleUseClick(item)}
|
||||
>
|
||||
USE
|
||||
Use
|
||||
</button>
|
||||
</td> */}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 text-center">
|
||||
<div
|
||||
|
|
@ -200,7 +201,7 @@ const TemplateTable = () => {
|
|||
}}
|
||||
>
|
||||
<button
|
||||
className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
|
||||
onClick={() =>
|
||||
setShowMenu(showMenu === index ? null : index)
|
||||
}
|
||||
|
|
@ -209,16 +210,10 @@ const TemplateTable = () => {
|
|||
</button>
|
||||
|
||||
{showMenu === index && (
|
||||
<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>
|
||||
<div className="absolute -top-9 right-4 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[100px]">
|
||||
<button
|
||||
onMouseDown={() => handleEditClick(item.id)}
|
||||
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"
|
||||
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
|
@ -226,13 +221,12 @@ const TemplateTable = () => {
|
|||
onMouseDown={() => {
|
||||
setShowMenu(null);
|
||||
}}
|
||||
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"
|
||||
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -87,10 +87,6 @@ const TransactionView = () => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(transactionDetails, "transactionDetails")
|
||||
|
||||
|
||||
const fetchDocuments = async () => {
|
||||
try {
|
||||
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents`)
|
||||
|
|
@ -159,16 +155,13 @@ const TransactionView = () => {
|
|||
More
|
||||
</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-one/${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-two/${accountId}/${transactionId}`}>
|
||||
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}>
|
||||
Audit
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr className='my-6 border-slate-200' />
|
||||
|
|
@ -210,7 +203,7 @@ const TransactionView = () => {
|
|||
<p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p>
|
||||
</div>
|
||||
|
||||
{transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && (
|
||||
{recipient.email === user.email && recipient.status !== "COMPLETED" && (
|
||||
<button
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ api.interceptors.response.use(
|
|||
const originalRequest = error.config;
|
||||
// Prevent multiple refresh attempts
|
||||
// if (error.response?.status === 401 || error.response?.status === 500) {
|
||||
if (error.response?.status === 401) {
|
||||
if (error.response?.status === 401 || error.response?.status === 500) {
|
||||
originalRequest._retry = true;
|
||||
try {
|
||||
if (!isRefreshing) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import { LuSignature } from "react-icons/lu";
|
||||
import SMELogo from "../assets/SMELogo2.png";
|
||||
import { FaFileSignature } from "react-icons/fa";
|
||||
|
||||
|
||||
interface StyleProps {
|
||||
classStyle?: string;
|
||||
|
|
@ -14,8 +11,8 @@ const LoaderUi = ({ classStyle = '' }: StyleProps) => {
|
|||
<div className="absolute inset-0 bg-black/30 opacity-60"></div>
|
||||
|
||||
{/* Loader box */}
|
||||
<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">
|
||||
<FaFileSignature className="text-4xl font-bold animate-text" />
|
||||
<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">
|
||||
<LuSignature className="text-4xl font-bold animate-text" />
|
||||
<p>Loading...!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ canvas {
|
|||
}
|
||||
|
||||
50% {
|
||||
color: var(--primary);
|
||||
color: #41644A;
|
||||
/* Tailwind blue-500 */
|
||||
}
|
||||
}
|
||||
|
|
@ -140,34 +140,6 @@ 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 */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ const selectedRecipientsSlice = createSlice({
|
|||
},
|
||||
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
||||
const { id, routingOrder } = action.payload;
|
||||
const recipient = state.recipients.find((r) => r.id === id || r.uuid === id);
|
||||
const recipient = state.recipients.find((r) => r.id === id || r.email === id);
|
||||
if (recipient) {
|
||||
recipient.routingOrder = routingOrder;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,47 +206,4 @@ export interface DroppedItem {
|
|||
value?: string | number;
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue