merge 05 Sep
This commit is contained in:
parent
cdbada638b
commit
aa9bd93bce
4
.env
4
.env
|
|
@ -1,7 +1,7 @@
|
||||||
VITE_BASE_URI="https://wired-terminally-muskrat.ngrok-free.app"
|
# VITE_BASE_URI="https://wired-terminally-muskrat.ngrok-free.app"
|
||||||
# VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app"
|
# VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app"
|
||||||
# VITE_BASE_URI="https://python-amused-adder.ngrok-free.app"
|
# VITE_BASE_URI="https://python-amused-adder.ngrok-free.app"
|
||||||
# VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
|
VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
|
||||||
|
|
||||||
|
|
||||||
# VITE_BASE_URI="http://192.168.2.176:8083"
|
# VITE_BASE_URI="http://192.168.2.176:8083"
|
||||||
|
|
@ -3,6 +3,8 @@ import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa";
|
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck, FaBars } from "react-icons/fa";
|
||||||
import api from '../config/api';
|
import api from '../config/api';
|
||||||
import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
|
import { IoSettings, IoSettingsOutline } from 'react-icons/io5';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../store/store';
|
||||||
|
|
||||||
interface AccountNavProps {
|
interface AccountNavProps {
|
||||||
accountName?: string | null;
|
accountName?: string | null;
|
||||||
|
|
@ -21,6 +23,8 @@ interface Account {
|
||||||
interface UserData {
|
interface UserData {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
|
|
@ -51,6 +55,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
|
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
|
||||||
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
|
const userDetails = JSON.parse(sessionStorage.getItem('userDetails') || '{}');
|
||||||
|
|
||||||
|
const userDetailsAsync = useSelector((state: RootState) => state.user)
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [accountsOpen, setAccountsOpen] = useState(false);
|
const [accountsOpen, setAccountsOpen] = useState(false);
|
||||||
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
||||||
|
|
@ -58,9 +64,13 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
|
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [initial, setInitial] = useState<string>("");
|
||||||
|
|
||||||
const [user, setUser] = useState<UserData>({
|
const [user, setUser] = useState<UserData>({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "",
|
name: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
language: "",
|
language: "",
|
||||||
account: {
|
account: {
|
||||||
|
|
@ -75,6 +85,14 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
navigate("/s/settings#profile-info");
|
navigate("/s/settings#profile-info");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(initial !== "") return
|
||||||
|
console.log(userDetailsAsync, 'line 89')
|
||||||
|
let initialN = (userDetailsAsync.firstName && userDetailsAsync.firstName[0])
|
||||||
|
initialN = initialN+""+(userDetailsAsync.lastName && userDetailsAsync.lastName[0])
|
||||||
|
setInitial(initialN)
|
||||||
|
}, [userDetailsAsync])
|
||||||
|
|
||||||
const getAccountDetails = async () => {
|
const getAccountDetails = async () => {
|
||||||
try {
|
try {
|
||||||
let res = await api.get('/api/user/accounts');
|
let res = await api.get('/api/user/accounts');
|
||||||
|
|
@ -123,6 +141,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
setUser({
|
setUser({
|
||||||
id: account.userId,
|
id: account.userId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
|
firstName: account.firstName,
|
||||||
|
lastName: account.lastName,
|
||||||
email: account.userEmail,
|
email: account.userEmail,
|
||||||
language: account.language, // set it if available
|
language: account.language, // set it if available
|
||||||
account: {
|
account: {
|
||||||
|
|
@ -132,6 +152,12 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (account.firstName && account.lastName) {
|
||||||
|
setInitial((account?.firstName?.slice(0, 1)).toUpperCase() + "" + (account?.lastName?.slice(0, 1)).toUpperCase())
|
||||||
|
} else {
|
||||||
|
setInitial((account?.name?.slice(0, 1)).toUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
sessionStorage.setItem('userDetails', JSON.stringify({
|
sessionStorage.setItem('userDetails', JSON.stringify({
|
||||||
id: account.userId,
|
id: account.userId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
|
|
@ -159,6 +185,8 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
const userData = {
|
const userData = {
|
||||||
id: item?.id,
|
id: item?.id,
|
||||||
name: item?.firstName + " " + item?.lastName,
|
name: item?.firstName + " " + item?.lastName,
|
||||||
|
firstName: item?.firstName,
|
||||||
|
lastName: item?.lastName,
|
||||||
email: item?.email,
|
email: item?.email,
|
||||||
language: item?.language,
|
language: item?.language,
|
||||||
account: {
|
account: {
|
||||||
|
|
@ -169,6 +197,12 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
if (userData.firstName && userData.lastName) {
|
||||||
|
setInitial((userData?.firstName?.slice(0, 1)).toUpperCase() + "" + (userData?.lastName?.slice(0, 1)).toUpperCase())
|
||||||
|
} else {
|
||||||
|
setInitial((userData?.name?.slice(0, 1)).toUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
sessionStorage.setItem('userDetails', JSON.stringify(userData));
|
sessionStorage.setItem('userDetails', JSON.stringify(userData));
|
||||||
sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id));
|
sessionStorage.setItem('activeAccountId', JSON.stringify(userData.account.id));
|
||||||
|
|
||||||
|
|
@ -340,7 +374,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
|
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
|
||||||
{user ? (
|
{user ? (
|
||||||
<span className="w-full h-full object-cover p-1.5 font-bold">
|
<span className="w-full h-full object-cover p-1.5 font-bold">
|
||||||
{(user?.name?.slice(0, 2)).toUpperCase()}
|
{initial}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<FaUser className="text-lg" />
|
<FaUser className="text-lg" />
|
||||||
|
|
@ -355,7 +389,7 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
|
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-[var(--primary)] to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
|
||||||
{user ? (
|
{user ? (
|
||||||
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold">
|
<span className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold">
|
||||||
{(user?.name?.slice(0, 2)).toUpperCase()}
|
{initial}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<FaUser className="text-lg" />
|
<FaUser className="text-lg" />
|
||||||
|
|
@ -455,20 +489,5 @@ const CommonNav: React.FC<AccountNavProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
||||||
// const ShimmerRow = () => (
|
|
||||||
// <div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
|
|
||||||
// <div className="flex items-center space-x-3">
|
|
||||||
// <div className="w-8 h-8 bg-gray-200 rounded-full"></div>
|
|
||||||
// <div className="flex-1 space-y-2">
|
|
||||||
// <div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
||||||
// <div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
||||||
// </div>
|
|
||||||
// <div className="w-16 h-6 bg-gray-200 rounded"></div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
}
|
}
|
||||||
export default CommonNav;
|
export default CommonNav;
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FaCheck } from "react-icons/fa";
|
|
||||||
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
|
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
|
||||||
import { FaDatabase } from "react-icons/fa";
|
import { FaDatabase } from "react-icons/fa";
|
||||||
import AccountNav from "./AccountNav";
|
|
||||||
import api from "../../config/api";
|
import api from "../../config/api";
|
||||||
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
|
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Cards from "../home/Cards";
|
|
||||||
import AddAccountModal from "./AddAccountModal";
|
import AddAccountModal from "./AddAccountModal";
|
||||||
import { Bounce, toast } from "react-toastify";
|
import { Bounce, toast } from "react-toastify";
|
||||||
import Loader from "../../helper/Loader";
|
|
||||||
|
|
||||||
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
|
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
|
||||||
|
|
||||||
|
|
@ -36,15 +32,9 @@ const getStatusLabel = (
|
||||||
const Accounts = () => {
|
const Accounts = () => {
|
||||||
|
|
||||||
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [accounts, setAccounts] = useState<string[]>([]);
|
|
||||||
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const fetchAccounts = async () => {
|
const fetchAccounts = async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
|
|
||||||
// import OAuthGoogle from "./OAuthGoogle";
|
// import OAuthGoogle from "./OAuthGoogle";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
// import { FcGoogle } from "react-icons/fc";
|
// import { FcGoogle } from "react-icons/fc";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import OAuthGoogle from "./OAuthGoogle";
|
import OAuthGoogle from "./OAuthGoogle";
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
// import Cookies from "js-cookie";
|
// import Cookies from "js-cookie";
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { AppDispatch } from "../../store/store";
|
|
||||||
import { BsEye } from "react-icons/bs";
|
import { BsEye } from "react-icons/bs";
|
||||||
import { FiEyeOff } from "react-icons/fi";
|
import { FiEyeOff } from "react-icons/fi";
|
||||||
// import api from "../../config/api";
|
// import api from "../../config/api";
|
||||||
|
|
@ -27,6 +24,9 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [errors, setErrors] = useState({ email: "", password: "" });
|
const [errors, setErrors] = useState({ email: "", password: "" });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const validateEmail = (email) => {
|
const validateEmail = (email) => {
|
||||||
|
|
@ -66,15 +66,6 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
||||||
if (!password) {
|
if (!password) {
|
||||||
return "Password is required";
|
return "Password is required";
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (password.length < 6) {
|
|
||||||
// return "Password must be at least 6 characters long";
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (password.length > 128) {
|
|
||||||
// return "Password is too long";
|
|
||||||
// }
|
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -98,49 +89,6 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextClick = async () => {
|
|
||||||
const emailError = validateEmail(formData.email);
|
|
||||||
|
|
||||||
if (emailError) {
|
|
||||||
setErrors({ ...errors, email: emailError });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios.get(
|
|
||||||
`${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"ngrok-skip-browser-warning": "69420",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
toast.dismiss();
|
|
||||||
setCacheKey(res.data);
|
|
||||||
setNextClick(true);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.dismiss();
|
|
||||||
toast.error("User Does Not Exist", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleFormLoginClick = async () => {
|
const handleFormLoginClick = async () => {
|
||||||
const emailError = validateEmail(formData.email);
|
const emailError = validateEmail(formData.email);
|
||||||
const passwordError = validatePassword(formData.password);
|
const passwordError = validatePassword(formData.password);
|
||||||
|
|
@ -167,57 +115,16 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.dismiss();
|
navigate("/");
|
||||||
|
formErrorRef.current = { message: "Login Successfully", error: false }
|
||||||
// Example if you want cookies / redux:
|
|
||||||
// Cookies.set("authorization", res.headers.authorization, {
|
|
||||||
// path: "/",
|
|
||||||
// secure: true,
|
|
||||||
// sameSite: "Lax",
|
|
||||||
// });
|
|
||||||
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
|
|
||||||
|
|
||||||
navigate("/"); // Redirect after 1 second
|
|
||||||
|
|
||||||
toast.success("Login Successful", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.dismiss();
|
|
||||||
|
|
||||||
if (err.response?.status === 400 || err.response?.status === 401) {
|
if (err.response?.status === 400 || err.response?.status === 401) {
|
||||||
toast.error("Credentials Not Match", {
|
formErrorRef.current = { message: "The email or password you entered is invalid.", error: true }
|
||||||
position: "top-center",
|
} else if(err.response?.status === 404) {
|
||||||
autoClose: 5000,
|
formErrorRef.current = { message: "Email not found. Please sign up to continue.", error: true }
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
toast.error("Some Error Occurred", {
|
formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true }
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -305,18 +212,9 @@ const LoginForm = ({ onSwitchToRegister }) => {
|
||||||
</div>
|
</div>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
|
|
||||||
{/* {!nextClick ? (
|
<p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
|
||||||
<button
|
|
||||||
onClick={handleNextClick}
|
|
||||||
disabled={isLoading}
|
|
||||||
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading
|
|
||||||
? "bg-gray-400 cursor-not-allowed"
|
|
||||||
: "bg-blue-500 hover:bg-blue-600 text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isLoading ? "Verifying..." : "Next"}
|
|
||||||
</button>
|
|
||||||
) : ( */}
|
|
||||||
<button
|
<button
|
||||||
onClick={handleFormLoginClick}
|
onClick={handleFormLoginClick}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
// import { GoogleSvg, SalesforceSvg } from "../svgs/Svg"
|
// import { GoogleSvg, SalesforceSvg } from "../svgs/Svg"
|
||||||
import OAuthGoogle from "./OAuthGoogle"
|
import OAuthGoogle from "./OAuthGoogle"
|
||||||
import { useEffect, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Link, Navigate, useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
// import Cookies from "js-cookie";
|
|
||||||
|
|
||||||
import { Bounce, toast } from "react-toastify";
|
|
||||||
import { updateUser, UserState } from "../../store/slices/userSlice";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { AppDispatch, RootState } from "../../store/store";
|
|
||||||
import { BsEye } from "react-icons/bs";
|
import { BsEye } from "react-icons/bs";
|
||||||
import { FiEyeOff } from "react-icons/fi";
|
import { FiEyeOff } from "react-icons/fi";
|
||||||
// import api from "../../config/api";
|
// import api from "../../config/api";
|
||||||
|
|
@ -23,7 +17,9 @@ interface FormDataState {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const RegisterForm = ({ onSwitchToLogin }) => {
|
const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
|
|
||||||
|
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
|
||||||
const [formData, setForm] = useState({
|
const [formData, setForm] = useState({
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
|
@ -175,92 +171,68 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormRegisterClick = async () => {
|
const handleFormRegisterClick = async () => {
|
||||||
const firstNameError = validateFirstName(formData.firstName);
|
const firstNameError = validateFirstName(formData.firstName);
|
||||||
const lastNameError = validateLastName(formData.lastName);
|
const lastNameError = validateLastName(formData.lastName);
|
||||||
const emailError = validateEmail(formData.email);
|
const emailError = validateEmail(formData.email);
|
||||||
const passwordError = validatePassword(formData.password);
|
const passwordError = validatePassword(formData.password);
|
||||||
const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password);
|
const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password);
|
||||||
|
|
||||||
const newErrors = {
|
const newErrors = {
|
||||||
firstName: firstNameError,
|
firstName: firstNameError,
|
||||||
lastName: lastNameError,
|
lastName: lastNameError,
|
||||||
email: emailError,
|
email: emailError,
|
||||||
password: passwordError,
|
password: passwordError,
|
||||||
confirmPassword: confirmPasswordError,
|
confirmPassword: confirmPasswordError,
|
||||||
};
|
};
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
// If any errors exist, stop
|
// If any errors exist, stop
|
||||||
if (Object.values(newErrors).some((error) => error !== "")) {
|
if (Object.values(newErrors).some((error) => error !== "")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const trimmedData = {
|
const trimmedData = {
|
||||||
firstName: formData.firstName.trim(),
|
firstName: formData.firstName.trim(),
|
||||||
lastName: formData.lastName.trim(),
|
lastName: formData.lastName.trim(),
|
||||||
email: formData.email.trim(),
|
email: formData.email.trim(),
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
confirmPassword: formData.confirmPassword,
|
confirmPassword: formData.confirmPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
`${import.meta.env.VITE_BASE_URI}/api/auth/signup`,
|
`${import.meta.env.VITE_BASE_URI}/api/auth/signup`,
|
||||||
{
|
{
|
||||||
firstName: trimmedData.firstName,
|
firstName: trimmedData.firstName,
|
||||||
lastName: trimmedData.lastName,
|
lastName: trimmedData.lastName,
|
||||||
email: trimmedData.email,
|
email: trimmedData.email,
|
||||||
password: trimmedData.password,
|
password: trimmedData.password,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
"ngrok-skip-browser-warning": "69420",
|
"ngrok-skip-browser-warning": "69420",
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 201) {
|
|
||||||
toast.dismiss();
|
|
||||||
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
|
|
||||||
// dispatch(updateUser({ ...res.data, authorized: true }));
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.dismiss();
|
|
||||||
|
|
||||||
if (err.response?.status === 409) {
|
|
||||||
toast.error("User Already Exists", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.error("Some Error Occurred", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (res.status === 201) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.status === 409) {
|
||||||
|
formErrorRef.current = { message: 'User Already Exists. Please try logging.', error: true }
|
||||||
|
} else {
|
||||||
|
formErrorRef.current = { message: 'Something went wrong. Please refresh and try again.', error: true }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
// Your Google OAuth logic here
|
// Your Google OAuth logic here
|
||||||
|
|
@ -296,8 +268,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
onChange={(e) => handleInputChange("firstName", e.target.value)}
|
onChange={(e) => handleInputChange("firstName", e.target.value)}
|
||||||
placeholder="First name"
|
placeholder="First name"
|
||||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName
|
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName
|
||||||
? "border-red-500 focus:ring-red-500"
|
? "border-red-500 focus:ring-red-500"
|
||||||
: "border-gray-300 focus:ring-blue-500"
|
: "border-gray-300 focus:ring-blue-500"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -312,8 +284,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
onChange={(e) => handleInputChange("lastName", e.target.value)}
|
onChange={(e) => handleInputChange("lastName", e.target.value)}
|
||||||
placeholder="Last name"
|
placeholder="Last name"
|
||||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName
|
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName
|
||||||
? "border-red-500 focus:ring-red-500"
|
? "border-red-500 focus:ring-red-500"
|
||||||
: "border-gray-300 focus:ring-blue-500"
|
: "border-gray-300 focus:ring-blue-500"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -334,8 +306,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email
|
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email
|
||||||
? "border-red-500 focus:ring-red-500"
|
? "border-red-500 focus:ring-red-500"
|
||||||
: "border-gray-300 focus:ring-blue-500"
|
: "border-gray-300 focus:ring-blue-500"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -355,8 +327,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password
|
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password
|
||||||
? "border-red-500 focus:ring-red-500"
|
? "border-red-500 focus:ring-red-500"
|
||||||
: "border-gray-300 focus:ring-blue-500"
|
: "border-gray-300 focus:ring-blue-500"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -385,8 +357,8 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
||||||
placeholder="Confirm your password"
|
placeholder="Confirm your password"
|
||||||
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.confirmPassword
|
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.confirmPassword
|
||||||
? "border-red-500 focus:ring-red-500"
|
? "border-red-500 focus:ring-red-500"
|
||||||
: "border-gray-300 focus:ring-blue-500"
|
: "border-gray-300 focus:ring-blue-500"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -404,12 +376,14 @@ const RegisterForm = ({ onSwitchToLogin }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleFormRegisterClick}
|
onClick={handleFormRegisterClick}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading
|
className={`w-full mt-2 py-2 rounded-lg font-medium transition-colors ${isLoading
|
||||||
? "bg-gray-400 cursor-not-allowed"
|
? "bg-gray-400 cursor-not-allowed"
|
||||||
: "bg-blue-500 hover:bg-blue-600 text-white"
|
: "bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isLoading ? "Creating Account..." : "Create Account"}
|
{isLoading ? "Creating Account..." : "Create Account"}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaSignature, FaCheckCircle } from "react-icons/fa";
|
||||||
|
import { MdOutlinePrivacyTip } from "react-icons/md";
|
||||||
|
|
||||||
|
interface ConsentScreenProps {
|
||||||
|
onConsent: () => void;
|
||||||
|
onDecline?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConsentScreen({ onConsent, onDecline }: ConsentScreenProps) {
|
||||||
|
const [accepted, setAccepted] = useState(false);
|
||||||
|
|
||||||
|
console.log("USER ACCEPTED CALLED")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="absolute top-0 left-0 w-[100vw] h-[100vh] bg-slate-100 opacity-50 backdrop-filter backdrop-blur-2xl"></div>
|
||||||
|
<div className="absolute top-0 left-0 w-full h-screen flex items-center justify-center p-6">
|
||||||
|
<div className="bg-white shadow-xl rounded-2xl p-8 max-w-lg w-full border border-gray-200">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<FaSignature className="text-blue-600 text-3xl" />
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-800">
|
||||||
|
Consent for E-Signature
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="flex items-start gap-3 mb-6 text-gray-600">
|
||||||
|
<MdOutlinePrivacyTip className="text-xl text-blue-500 mt-1" />
|
||||||
|
<p className="text-sm leading-relaxed">
|
||||||
|
By providing your electronic signature, you agree that it is the legal
|
||||||
|
equivalent of your manual signature on this document. Please review the
|
||||||
|
agreement carefully before proceeding.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Checkbox */}
|
||||||
|
<label className="flex items-center gap-3 mb-6 cursor-pointer select-none">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={accepted}
|
||||||
|
onChange={(e) => setAccepted(e.target.checked)}
|
||||||
|
className="w-5 h-5 accent-blue-600 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
I agree to sign electronically
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex gap-4 flex-row-reverse">
|
||||||
|
<button
|
||||||
|
className={`flex-1 cursor-pointer flex items-center justify-center gap-2 rounded-xl py-2 font-medium transition-colors
|
||||||
|
${accepted ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-300 text-gray-500 cursor-not-allowed"}
|
||||||
|
`}
|
||||||
|
disabled={!accepted}
|
||||||
|
onClick={onConsent}
|
||||||
|
>
|
||||||
|
<FaCheckCircle /> Continue
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* {onDecline && (
|
||||||
|
<button
|
||||||
|
className="flex-1 border border-red-500 text-red-500 hover:bg-red-50 rounded-xl py-2 font-medium transition-colors"
|
||||||
|
onClick={onDecline}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { FaTimesCircle } from "react-icons/fa";
|
||||||
|
|
||||||
|
interface DeclineModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeclineModal({ open, onClose, onConfirm }: DeclineModalProps) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-6 relative">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<FaTimesCircle className="text-red-500 text-2xl" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">
|
||||||
|
Confirm Decline
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<p className="text-sm text-gray-600 mb-6">
|
||||||
|
Are you sure you want to decline this document? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 rounded-md border border-gray-300 text-gray-600 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 rounded-md bg-red-500 text-white hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaUserEdit } from "react-icons/fa";
|
||||||
|
|
||||||
|
interface ReassignModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: (data: { firstName: string; lastName: string; email: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReassignModal({ open, onClose, onConfirm }: ReassignModalProps) {
|
||||||
|
const [firstName, setFirstName] = useState("");
|
||||||
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<{ lastName?: string; email?: string }>({});
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
const newErrors: { lastName?: string; email?: string } = {};
|
||||||
|
if (!lastName.trim()) newErrors.lastName = "Last name is required.";
|
||||||
|
if (!email.trim()) newErrors.email = "Email is required.";
|
||||||
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) newErrors.email = "Enter a valid email.";
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!validate()) return;
|
||||||
|
onConfirm({ firstName, lastName, email });
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<FaUserEdit className="text-blue-500 text-2xl" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">Reassign Document</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
Please enter the recipient details to reassign this document.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Form Fields */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="First Name (optional)"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
className="w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name *"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
className={`w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 ${
|
||||||
|
errors.lastName ? "border-red-500 focus:ring-red-400" : "focus:ring-blue-400"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.lastName && (
|
||||||
|
<p className="text-red-500 text-xs mt-1">{errors.lastName}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email *"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className={`w-full border border-slate-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 ${
|
||||||
|
errors.email ? "border-red-500 focus:ring-red-400" : "focus:ring-blue-400"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 rounded-md border border-gray-300 text-gray-600 hover:bg-gray-100 text-sm"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="px-4 py-2 rounded-md bg-blue-500 text-white hover:bg-blue-600 text-sm"
|
||||||
|
>
|
||||||
|
Reassign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { FaSignature, FaDownload, FaUserEdit, FaTimesCircle } from "react-icons/fa";
|
||||||
|
import { BsThreeDotsVertical } from "react-icons/bs";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
transparentTags?: boolean;
|
||||||
|
hideTags?: boolean;
|
||||||
|
handleFormSubmit: () => void;
|
||||||
|
onDownloadClick?: () => void;
|
||||||
|
onDeclineClick?: () => void;
|
||||||
|
onReassignClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SignButton({
|
||||||
|
transparentTags,
|
||||||
|
hideTags,
|
||||||
|
handleFormSubmit,
|
||||||
|
onDownloadClick,
|
||||||
|
onDeclineClick,
|
||||||
|
onReassignClick,
|
||||||
|
}: Props) {
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Close menu on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||||
|
setMenuOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative inline-block" ref={menuRef}>
|
||||||
|
{(!transparentTags && !hideTags) ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Sign Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleFormSubmit}
|
||||||
|
className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-md bg-[var(--primary)] hover:opacity-90 transition text-white outline-none"
|
||||||
|
>
|
||||||
|
<span id="sign-client-side-btn" className="tracking-wide">
|
||||||
|
Sign
|
||||||
|
</span>
|
||||||
|
<FaSignature className="text-md" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Three Dots Dropdown Trigger */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMenuOpen((prev) => !prev)}
|
||||||
|
className="ml-2 p-2 rounded-full hover:bg-gray-100 focus:outline-none transition"
|
||||||
|
>
|
||||||
|
<BsThreeDotsVertical className="text-gray-600 text-lg cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
{menuOpen && (
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
onBlur={() => setMenuOpen(false)} // close on blur
|
||||||
|
className="absolute right-0 top-10 w-56 bg-white border border-gray-200 rounded-lg shadow-lg z-10"
|
||||||
|
>
|
||||||
|
<ul className="py-1 text-sm text-gray-700">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onDownloadClick?.();
|
||||||
|
setMenuOpen(false);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<FaDownload className="text-blue-500" />
|
||||||
|
<span>Download Document</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onReassignClick?.();
|
||||||
|
setMenuOpen(false);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<FaUserEdit className="text-yellow-500" />
|
||||||
|
<span>Reassign</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onDeclineClick?.();
|
||||||
|
setMenuOpen(false);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-3 w-full cursor-pointer text-left px-4 py-2 hover:bg-gray-100 text-red-500 border-t border-gray-200"
|
||||||
|
>
|
||||||
|
<FaTimesCircle />
|
||||||
|
<span>Decline</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-green-600 font-medium">Signed Successfully</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { RootState } from "../../store/store";
|
import { RootState } from "../../store/store";
|
||||||
import { useNavigate, useParams } from "react-router-dom"
|
import { Link, useNavigate, useParams } from "react-router-dom"
|
||||||
import { DroppedItem, TagsTypes } from "../../utlity/types";
|
import { DroppedItem, TagsTypes } from "../../utlity/types";
|
||||||
|
|
||||||
import Loader from "../dashboard/Loader";
|
import Loader from "../dashboard/Loader";
|
||||||
|
|
@ -12,8 +12,9 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
|
import { hideLoader, showLoader } from "../../store/slices/loaderSlice";
|
||||||
|
|
||||||
import { FaSignature, FaCheck, FaQuestionCircle } from 'react-icons/fa'
|
import { FaSignature, FaCheck, FaQuestionCircle } from 'react-icons/fa'
|
||||||
import { IoIosDocument } from 'react-icons/io'
|
import { IoIosDocument, IoMdHome } from 'react-icons/io'
|
||||||
import { MdKeyboardArrowDown } from 'react-icons/md'
|
import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
|
import confetti from 'canvas-confetti'
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
|
@ -25,6 +26,10 @@ import SignCompletionScreen from "./SignCompletionScreen";
|
||||||
|
|
||||||
import RightSideBar from "../process/processStepTwo/rightSideBar/RightSideBar";
|
import RightSideBar from "../process/processStepTwo/rightSideBar/RightSideBar";
|
||||||
import TagsCon from "./TagsCon";
|
import TagsCon from "./TagsCon";
|
||||||
|
import ConsentScreen from "./ConsentScreen";
|
||||||
|
import SignButton from "./SignButton";
|
||||||
|
import DeclineModal from "./DeclineModal";
|
||||||
|
import ReassignModal from "./ReassignModal";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -150,11 +155,18 @@ const SignDocument = () => {
|
||||||
const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
|
const [recSignStatus, setRecSignStatus] = useState<string | null>(null);
|
||||||
const [droppedItems, setDroppedItems] = useState<DroppedItem[]>([]);
|
const [droppedItems, setDroppedItems] = useState<DroppedItem[]>([]);
|
||||||
const [signTagId, setSignTagId] = useState<number>(0)
|
const [signTagId, setSignTagId] = useState<number>(0)
|
||||||
|
const [showConfetti, setShowConfetti] = useState<boolean>(false)
|
||||||
|
|
||||||
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
|
const [stamp, setStamp] = useState<StampTypeObj>({ open: false, blob: "" });
|
||||||
|
|
||||||
const [rightSideNavToggle, setRightSideNavToggle] = useState<RightSideNav>(RightSideNav.PAGES);
|
const [signerConsent, setSignerConsent] = useState<{ consent: boolean; decline: boolean }>({
|
||||||
|
consent: false,
|
||||||
|
decline: false
|
||||||
|
});
|
||||||
|
const [declineOpen, setDeclineOpen] = useState<boolean>(false);
|
||||||
|
const [reassignOpen, setReassignOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [rightSideNavToggle, setRightSideNavToggle] = useState<RightSideNav>(RightSideNav.PAGES);
|
||||||
|
|
||||||
// Show is fields - Showing all fields There
|
// Show is fields - Showing all fields There
|
||||||
const [isFields, setIsFields] = useState<boolean>(true);
|
const [isFields, setIsFields] = useState<boolean>(true);
|
||||||
|
|
@ -237,6 +249,35 @@ const SignDocument = () => {
|
||||||
}
|
}
|
||||||
}, [droppedItems])
|
}, [droppedItems])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (showConfetti) {
|
||||||
|
const duration = 3000;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Date.now() > end) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 5, // fewer particles per burst for smoothness
|
||||||
|
angle: 90, // falling straight down
|
||||||
|
spread: 30, // slight spread
|
||||||
|
startVelocity: 8, // gentle falling speed
|
||||||
|
origin: {
|
||||||
|
x: Math.random(), // random horizontal position across the top
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
scalar: 1,
|
||||||
|
zIndex: 9999,
|
||||||
|
});
|
||||||
|
}, 50); // fire every 50ms for continuous effect
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [showConfetti]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recSignStatus === "COMPLETED") {
|
if (recSignStatus === "COMPLETED") {
|
||||||
|
|
@ -371,24 +412,7 @@ const SignDocument = () => {
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setTransparentTags(true)
|
setTransparentTags(true)
|
||||||
setTimeout(() => {
|
setShowConfetti(true)
|
||||||
dispatch(hideLoader())
|
|
||||||
toast.dismiss();
|
|
||||||
toast.info("Signed Successfully!", {
|
|
||||||
position: "bottom-right",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
// Completion Sign Screen Modal and download file
|
|
||||||
setShowCompletionScreen(true)
|
|
||||||
}, 800)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
@ -584,11 +608,18 @@ const SignDocument = () => {
|
||||||
if (setTo !== isFields) setIsFields(setTo);
|
if (setTo !== isFields) setIsFields(setTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleOnConsent = () => {
|
||||||
|
setSignerConsent((prev) => {
|
||||||
|
return { ...prev, consent: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
|
<div className={`w-full h-screen overflow-hidden relative ${(globalLoader.isLoading) ? 'pointer-events-none overflow-hidden' : ''}`}>
|
||||||
<>
|
{!showCompletionScreen && <>
|
||||||
{/* <TermsModal showTerms={showTerms} setShowTerms={setShowTerms} recipientId={recipientId || ""} /> */}
|
{/* <TermsModal showTerms={showTerms} setShowTerms={setShowTerms} recipientId={recipientId || ""} /> */}
|
||||||
{showCompletionScreen && <SignCompletionScreen />}
|
{/* {showCompletionScreen && <SignCompletionScreen />} */}
|
||||||
<div className="bg-slate-100"
|
<div className="bg-slate-100"
|
||||||
// onSubmit={(e: React.FormEvent<HTMLFormElement>) => handleFormSubmit(e)}
|
// onSubmit={(e: React.FormEvent<HTMLFormElement>) => handleFormSubmit(e)}
|
||||||
>
|
>
|
||||||
|
|
@ -688,7 +719,7 @@ const SignDocument = () => {
|
||||||
</>
|
</>
|
||||||
|
|
||||||
{/* Send */}
|
{/* Send */}
|
||||||
{(!transparentTags && !hideTags) ? <button
|
{/* {(!transparentTags && !hideTags) ? <button
|
||||||
onClick={handleFormSubmit}
|
onClick={handleFormSubmit}
|
||||||
// onClick={() => handleFormSubmitBtnClick()}
|
// onClick={() => handleFormSubmitBtnClick()}
|
||||||
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)] outline-none'>
|
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)] outline-none'>
|
||||||
|
|
@ -702,9 +733,18 @@ const SignDocument = () => {
|
||||||
</button>
|
</button>
|
||||||
:
|
:
|
||||||
<p className="text-xs">Signed Successfully</p>
|
<p className="text-xs">Signed Successfully</p>
|
||||||
}
|
} */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<SignButton
|
||||||
|
transparentTags={transparentTags}
|
||||||
|
hideTags={hideTags}
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
|
onDeclineClick={() => setDeclineOpen(true)}
|
||||||
|
onReassignClick={() => setReassignOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/* Right Side - ENDS */}
|
{/* Right Side - ENDS */}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -827,8 +867,169 @@ const SignDocument = () => {
|
||||||
</div>
|
</div>
|
||||||
{/* Document Container End */}
|
{/* Document Container End */}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>}
|
||||||
|
|
||||||
|
{signerConsent && <>
|
||||||
|
<div className='w-full h-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'
|
||||||
|
/> */}
|
||||||
|
SME
|
||||||
|
</Link>
|
||||||
|
<div className="h-8 w-px bg-gray-300"></div>
|
||||||
|
<h1 className="text-md text-gray-800">🎉 The document has been signed successfully!</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)]'>You have successfully signed the document.</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'>
|
||||||
|
{/* SIGN Documen Container - START */}
|
||||||
|
<div
|
||||||
|
id="doc-con"
|
||||||
|
className={`relative px-4 bg-slate-100 max-w-max h-full w-full overflow-auto scroll-smooth transition-all duration-200`}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
selectedDocuments && selectedDocuments.length > 0 && selectedDocuments.map((doc) => {
|
||||||
|
return (
|
||||||
|
<div key={doc.documentId} className='flex flex-col pointer-events-none'>
|
||||||
|
<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'>
|
||||||
|
{
|
||||||
|
doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => {
|
||||||
|
const key = `${doc.documentId}_${page.index}`;
|
||||||
|
// const tagJsonData = [...droppedItems];
|
||||||
|
// debugger
|
||||||
|
const currDocTags = droppedItems.filter((val) => val.pageIndex === page.index)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
id={doc.documentId + indx}
|
||||||
|
onDrop={(e) => handleDrop(e, doc.documentId, doc.documentId, page.index)}
|
||||||
|
onDragOver={(e) => handleDragOver(e)}
|
||||||
|
className='drop-page-in shadow-md relative'
|
||||||
|
>
|
||||||
|
|
||||||
|
{!hideTags && <>
|
||||||
|
<TagsCon
|
||||||
|
currDocTags={currDocTags}
|
||||||
|
pageIdkey={key}
|
||||||
|
provideTags={provideTags}
|
||||||
|
documentId={doc.documentId}
|
||||||
|
pageIndex={page.index}
|
||||||
|
hideTags={hideTags}
|
||||||
|
transparentTags={transparentTags}
|
||||||
|
focusElement={focusElement}
|
||||||
|
selectedTag={selectedTag}
|
||||||
|
droppedItems={droppedItems}
|
||||||
|
saved={saved}
|
||||||
|
signConfObj={signConfObj}
|
||||||
|
setStamp={setStamp}
|
||||||
|
setSignConfObj={setSignConfObj}
|
||||||
|
setOpenSign={setOpenSign}
|
||||||
|
setSignTagId={setSignTagId}
|
||||||
|
setDroppedItems={setDroppedItems}
|
||||||
|
setSelectedTag={setSelectedTag}
|
||||||
|
setFocusElement={setFocusElement} />
|
||||||
|
</>}
|
||||||
|
|
||||||
|
<img
|
||||||
|
style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
|
||||||
|
draggable="false"
|
||||||
|
width={page.width}
|
||||||
|
height={page.height}
|
||||||
|
src={page.src}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Side details of documents Pages and document Name - START */}
|
||||||
|
< p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600' >
|
||||||
|
page {indx + 1} of {doc.pages && doc.pages.length}
|
||||||
|
</p>
|
||||||
|
{/* Side details of documents Pages and document Name - END */}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* SIGN Documen Container - END */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Link to={"/"} className="px-4 py-2 rounded-sm text-white bg-[var(--primary)] lg:min-w-[240px] text-center">Download Document</Link>
|
||||||
|
<Link to={"/"} className="px-4 py-2 rounded-sm text-white bg-[var(--primary)] lg:min-w-[240px] text-center">Visit Sign Made Easy</Link>
|
||||||
|
</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>
|
||||||
|
</>}
|
||||||
|
|
||||||
<div style={{ display: `${openSign ? 'block' : 'none'}` }}>
|
<div style={{ display: `${openSign ? 'block' : 'none'}` }}>
|
||||||
{recipient &&
|
{recipient &&
|
||||||
|
|
@ -848,6 +1049,24 @@ const SignDocument = () => {
|
||||||
<InitialCon openInitial={openInitial} setOpenInitial={setOpenInitial} savedInitial={savedInitial} setSavedInitial={setSavedInitial} /> */}
|
<InitialCon openInitial={openInitial} setOpenInitial={setOpenInitial} savedInitial={savedInitial} setSavedInitial={setSavedInitial} /> */}
|
||||||
|
|
||||||
{globalLoader.isLoading && <Loader />}
|
{globalLoader.isLoading && <Loader />}
|
||||||
|
|
||||||
|
{!showCompletionScreen && !signerConsent.consent && <ConsentScreen
|
||||||
|
onConsent={() => handleOnConsent()}
|
||||||
|
// onDecline={() => console.log("User declined")}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<DeclineModal
|
||||||
|
open={declineOpen}
|
||||||
|
onClose={() => setDeclineOpen(false)}
|
||||||
|
onConfirm={() => setSignerConsent((prev) => ({ ...prev, decline: false }))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReassignModal
|
||||||
|
open={reassignOpen}
|
||||||
|
onClose={() => setReassignOpen(false)}
|
||||||
|
onConfirm={(email) => console.log("Reassigned to:", email)}
|
||||||
|
/>
|
||||||
|
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -312,18 +312,18 @@ const TagsCon: React.FC<TagsConProps> = ({
|
||||||
{selectedTag?.key && selectedTag.key === pageIdkey && selectedTag.index === index &&
|
{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='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'>
|
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
|
||||||
<p className='w-2.5 h-2.5 rounded'
|
{/* <p className='w-2.5 h-2.5 rounded'
|
||||||
style={{ "background": "#2d2d2d" }}
|
style={{ "background": "#2d2d2d" }}
|
||||||
>
|
>
|
||||||
</p>
|
</p> */}
|
||||||
{/* <p
|
{/* <p
|
||||||
className=''>{IconComponent}
|
className=''>{IconComponent}
|
||||||
</p> */}
|
</p> */}
|
||||||
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
|
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
|
||||||
<IoMdClose onClick={(e) => {
|
{provideTags && <IoMdClose onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRemoveTag(pageIdkey, index)
|
handleRemoveTag(pageIdkey, index)
|
||||||
}} className='cursor-pointer' />
|
}} className='cursor-pointer' />}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,9 @@ import { clearTransaction, TRANSACTION_STATUS_ENUMS } from '../../store/slices/t
|
||||||
import { resetSelDoc } from '../../store/slices/selectedDocsSlice'
|
import { resetSelDoc } from '../../store/slices/selectedDocsSlice'
|
||||||
import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice'
|
import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice'
|
||||||
import { hideLoader, showLoader } from '../../store/slices/loaderSlice'
|
import { hideLoader, showLoader } from '../../store/slices/loaderSlice'
|
||||||
import { Bounce, toast } from 'react-toastify'
|
|
||||||
import SignModal from '../modal/SignModal'
|
import SignModal from '../modal/SignModal'
|
||||||
|
|
||||||
import confetti from 'canvas-confetti'
|
import confetti from 'canvas-confetti'
|
||||||
import { useWindowSize } from 'react-use'
|
|
||||||
import { FaCheckCircle } from 'react-icons/fa'
|
import { FaCheckCircle } from 'react-icons/fa'
|
||||||
import { IoClose } from 'react-icons/io5'
|
import { IoClose } from 'react-icons/io5'
|
||||||
import CommonNav from '../CommonNav'
|
import CommonNav from '../CommonNav'
|
||||||
|
|
@ -39,13 +37,6 @@ function Dashboard() {
|
||||||
|
|
||||||
// Check if cookie already exists - Redirect to Dashboard
|
// Check if cookie already exists - Redirect to Dashboard
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// if (!user.authorized) {
|
|
||||||
// window.location.href = '/login';
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// console.log("OUT MIC CHECK", transactionStatus, TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS, TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED);
|
|
||||||
|
|
||||||
// Clear all if transaction success
|
|
||||||
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_FAILURE) {
|
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_FAILURE) {
|
||||||
|
|
||||||
dispatch(resetSelDoc())
|
dispatch(resetSelDoc())
|
||||||
|
|
@ -54,22 +45,6 @@ function Dashboard() {
|
||||||
|
|
||||||
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) {
|
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) {
|
||||||
setShowConfetti(true)
|
setShowConfetti(true)
|
||||||
// setTimeout(() => {
|
|
||||||
// setShowConfetti(false)
|
|
||||||
// dispatch(hideLoader())
|
|
||||||
// toast.dismiss();
|
|
||||||
// toast.info("Transaction Send Successfully!", {
|
|
||||||
// position: "bottom-right",
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeOnClick: false,
|
|
||||||
// pauseOnHover: true,
|
|
||||||
// draggable: true,
|
|
||||||
// progress: undefined,
|
|
||||||
// theme: "light",
|
|
||||||
// transition: Bounce,
|
|
||||||
// });
|
|
||||||
// }, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { IoMdArrowDropdown } from "react-icons/io";
|
|
||||||
import { IoIosSend } from "react-icons/io";
|
import { IoIosSend } from "react-icons/io";
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { setSignType, SignType } from '../../store/slices/selectedDocsSlice';
|
|
||||||
import { RootState } from '../../store/store';
|
import { RootState } from '../../store/store';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Bounce, toast } from 'react-toastify';
|
|
||||||
import api from '../../config/api';
|
import api from '../../config/api';
|
||||||
import { StartCard } from '../home/StartSection';
|
|
||||||
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice';
|
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice';
|
||||||
import { SignTypeEnum } from '../../utlity/types';
|
import { SignTypeEnum } from '../../utlity/types';
|
||||||
// import { IoMdArrowDropright } from "react-icons/io";
|
// import { IoMdArrowDropright } from "react-icons/io";
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ export const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||||||
{signers.map((signer) => (
|
{signers.map((signer) => (
|
||||||
<div key={signer.id} className="text-sm flex items-center gap-4 rounded-lg">
|
<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">
|
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
|
||||||
{signer.name.length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.email.split("").map(n => n[0].toUpperCase()).join("")}
|
{/* {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>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-medium text-gray-800">{signer.name}</div>
|
<div className="font-medium text-gray-800">{signer.name}</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FiLock } from "react-icons/fi";
|
||||||
|
|
||||||
|
export default function AdvancedOptions({setShowAdvancedOptions}) {
|
||||||
|
const [autoReminders, setAutoReminders] = useState(false);
|
||||||
|
const [expireDays, setExpireDays] = useState(120);
|
||||||
|
const [alertDays, setAlertDays] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen bg-white flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex justify-between items-center border-b px-6 py-4">
|
||||||
|
<h2 className="text-xl font-semibold">Advanced Options</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdvancedOptions(false)}
|
||||||
|
className="text-gray-500 hover:text-black">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="w-60 border-r px-6 py-4 space-y-4 text-sm font-medium text-gray-700">
|
||||||
|
<div className="cursor-pointer hover:text-black">Recipient Privileges</div>
|
||||||
|
<div className="cursor-pointer hover:text-black">Reminders</div>
|
||||||
|
<div className="cursor-pointer hover:text-black">Expiration</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 px-8 py-6 overflow-y-auto space-y-10">
|
||||||
|
{/* Recipient Privileges */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Recipient Privileges</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">Give recipients options for how they sign.</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{/* <label className="flex items-center gap-2 text-gray-700">
|
||||||
|
<input type="checkbox" checked readOnly className="accent-purple-600" />
|
||||||
|
Recipients can sign on paper
|
||||||
|
</label> */}
|
||||||
|
<label className="flex items-center gap-2 text-gray-400">
|
||||||
|
<input type="checkbox" disabled className="accent-purple-600" />
|
||||||
|
Recipients can change signing responsibility or assign a delegate
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<FiLock className="text-gray-400 mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reminders */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Reminders</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
Follow up with automatic reminders. Signers will receive emails until they sign or decline the envelope.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setAutoReminders(!autoReminders)}
|
||||||
|
className={`relative w-14 h-7 flex items-center rounded-full p-1 transition-colors duration-200 ${autoReminders ? "bg-purple-600" : "bg-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`bg-white w-5 h-5 rounded-full shadow-md transform transition-transform duration-200 ${autoReminders ? "translate-x-7" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span className="ml-3 text-sm">Turn on auto reminders</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expiration */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Expiration</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
By default, envelopes expire after 120 days. Recipients can no longer view or sign an envelope after it expires.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mb-3">
|
||||||
|
<select className="border rounded px-3 py-2 text-sm">
|
||||||
|
<option>Custom days</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={expireDays}
|
||||||
|
onChange={(e) => setExpireDays(Number(e.target.value))}
|
||||||
|
className="border rounded px-3 py-2 text-sm w-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 mb-6">
|
||||||
|
Envelopes will expire on: <span className="font-medium">1/3/2026</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<select className="border rounded px-3 py-2 text-sm">
|
||||||
|
<option>Custom days</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={alertDays}
|
||||||
|
onChange={(e) => setAlertDays(Number(e.target.value))}
|
||||||
|
className="border rounded px-3 py-2 text-sm w-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="border-t p-4 flex justify-end">
|
||||||
|
<button className="bg-purple-600 text-white px-6 py-2 rounded hover:bg-purple-700 transition">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from ".
|
||||||
import { SignTypeEnum } from "../../../utlity/types";
|
import { SignTypeEnum } from "../../../utlity/types";
|
||||||
import { RiPencilFill } from "react-icons/ri";
|
import { RiPencilFill } from "react-icons/ri";
|
||||||
import SelfSignWarnModal from "./SelfSignWarnModal";
|
import SelfSignWarnModal from "./SelfSignWarnModal";
|
||||||
|
import AdvancedOptions from "./AdvancedOptions";
|
||||||
|
|
||||||
const ProcessStepOne = () => {
|
const ProcessStepOne = () => {
|
||||||
interface EmailDtls {
|
interface EmailDtls {
|
||||||
|
|
@ -25,10 +26,29 @@ const ProcessStepOne = () => {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FormSubmitInt {
|
||||||
|
documents: boolean,
|
||||||
|
recipients: boolean,
|
||||||
|
subject: boolean,
|
||||||
|
recipientEmailAndLastName: boolean,
|
||||||
|
duplicateEmailSameRoutingOrder: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormSubmitIntitialState = {
|
||||||
|
documents: false,
|
||||||
|
recipients: false,
|
||||||
|
subject: false,
|
||||||
|
recipientEmailAndLastName: false,
|
||||||
|
duplicateEmailSameRoutingOrder: false
|
||||||
|
}
|
||||||
|
|
||||||
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
|
let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS;
|
||||||
|
|
||||||
const signType = useParams<{ signType: string }>().signType;
|
const signType = useParams<{ signType: string }>().signType;
|
||||||
const templateId = useParams<{ templateId: string }>().templateId;
|
const templateId = useParams<{ templateId: string }>().templateId;
|
||||||
|
const [submitError, setSubmitError] = useState<FormSubmitInt>({ ...FormSubmitIntitialState })
|
||||||
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(false)
|
||||||
|
|
||||||
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
|
@ -102,6 +122,12 @@ const ProcessStepOne = () => {
|
||||||
dispatch(hideLoader());
|
dispatch(hideLoader());
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitError.duplicateEmailSameRoutingOrder) setSubmitError(prev => ({ ...prev, duplicateEmailSameRoutingOrder: false }))
|
||||||
|
if (submitError.documents) setSubmitError(prev => ({ ...prev, documents: false }))
|
||||||
|
if (submitError.recipients) setSubmitError(prev => ({ ...prev, recipients: false }))
|
||||||
|
}, [selectedRecipients])
|
||||||
|
|
||||||
|
|
||||||
const getMinRoutingOrder = () => {
|
const getMinRoutingOrder = () => {
|
||||||
return selectedRecipients.reduce((min, recipient) => {
|
return selectedRecipients.reduce((min, recipient) => {
|
||||||
|
|
@ -112,11 +138,20 @@ const ProcessStepOne = () => {
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
const handleNextClick = async (saveAndClose: boolean = false) => {
|
const handleNextClick = async (saveAndClose: boolean = false) => {
|
||||||
let formError = {
|
let formError = { ...FormSubmitIntitialState }
|
||||||
documents: false,
|
|
||||||
recipients: false,
|
const recipientsData: { [key: string | number]: string[] } = {}
|
||||||
subject: false,
|
for (const recipient of selectedRecipients) {
|
||||||
recipientEmailAndLastName: false
|
if (recipientsData[recipient.routingOrder] && recipientsData[recipient.routingOrder].includes(recipient.email)) {
|
||||||
|
formError.duplicateEmailSameRoutingOrder = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipientsData[recipient.routingOrder]) {
|
||||||
|
recipientsData[recipient.routingOrder] = [...recipientsData[recipient.routingOrder], recipient.email]
|
||||||
|
} else {
|
||||||
|
recipientsData[recipient.routingOrder] = [recipient.email]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
|
if (transactionTemplateToggle !== TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) {
|
||||||
|
|
@ -143,29 +178,12 @@ const ProcessStepOne = () => {
|
||||||
|
|
||||||
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
|
if (SignTypeEnum.selfSign === signType) formError.recipients = false;
|
||||||
|
|
||||||
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName) {
|
if (formError.documents || formError.recipients || formError.subject || formError.recipientEmailAndLastName || formError.duplicateEmailSameRoutingOrder) {
|
||||||
toast.dismiss();
|
setSubmitError(formError)
|
||||||
toast.error(`${formError.documents ? "Add Document" : ""}
|
|
||||||
${(formError.documents && formError.recipients) ? " | " : ""}
|
|
||||||
${formError.recipients ? "Add Recipient" : ""}
|
|
||||||
${formError.recipientEmailAndLastName ? "All recipients must have both Email and Last Name." : ""}
|
|
||||||
${(formError.documents || formError.recipients) ? " | " : ""}
|
|
||||||
${formError.subject ? "Add Subject" : ""}`, {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "light",
|
|
||||||
transition: Bounce,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dispatch(showLoader("Processing Documents..."));
|
dispatch(showLoader("Processing Documents..."));
|
||||||
|
|
||||||
|
|
@ -188,11 +206,6 @@ const ProcessStepOne = () => {
|
||||||
dispatch(showLoader("Processing Recipients..."));
|
dispatch(showLoader("Processing Recipients..."));
|
||||||
|
|
||||||
let minRoutingOrder = getMinRoutingOrder();
|
let minRoutingOrder = getMinRoutingOrder();
|
||||||
// selectedRecipients.forEach((recipient) => {
|
|
||||||
// if (recipient.routingOrder ?? 1 < minRoutingOrder) {
|
|
||||||
// minRoutingOrder = recipient.routingOrder ?? 1;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
const recipientsData = selectedRecipients.map((recipient, index) => ({
|
const recipientsData = selectedRecipients.map((recipient, index) => ({
|
||||||
id: "id" in recipient ? recipient.id : null,
|
id: "id" in recipient ? recipient.id : null,
|
||||||
|
|
@ -303,8 +316,6 @@ const ProcessStepOne = () => {
|
||||||
navigate(`/process-step-two/${accountId}/${transactionId}`);
|
navigate(`/process-step-two/${accountId}/${transactionId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }, 1000);
|
|
||||||
// }
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(hideLoader());
|
dispatch(hideLoader());
|
||||||
|
|
@ -340,6 +351,14 @@ const ProcessStepOne = () => {
|
||||||
|
|
||||||
const globalLoader = useSelector((state: RootState) => state.loader)
|
const globalLoader = useSelector((state: RootState) => state.loader)
|
||||||
|
|
||||||
|
|
||||||
|
if (showAdvancedOptions) {
|
||||||
|
return (
|
||||||
|
<AdvancedOptions setShowAdvancedOptions={setShowAdvancedOptions} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}>
|
<div className={`relative w-full h-screen bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none overflow-hidden' : ''}`}>
|
||||||
|
|
@ -388,6 +407,15 @@ const ProcessStepOne = () => {
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => handleNextClick(false)}
|
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"
|
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"
|
||||||
|
|
@ -406,7 +434,10 @@ const ProcessStepOne = () => {
|
||||||
{/* Documents */}
|
{/* Documents */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
|
<h2 className="text-lg font-medium mb-4 pt-3 text-[var(--processStepOneTextHead)]">Documents</h2>
|
||||||
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
|
<div id="documents_con" className={`${submitError.documents ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
|
||||||
|
<SelectedDocuments transactionTemplateToggle={transactionTemplateToggle} />
|
||||||
|
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.documents && "At least one document is required."}</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Recipients */}
|
{/* Recipients */}
|
||||||
|
|
@ -432,13 +463,26 @@ const ProcessStepOne = () => {
|
||||||
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
|
(signType !== SignTypeEnum.selfSign || showSelfSignWarnModal) &&
|
||||||
<div className="flex flex-col gap-4 align-start">
|
<div className="flex flex-col gap-4 align-start">
|
||||||
|
|
||||||
<RecipientCards userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase} editRecipient={editRecipient} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} editRecipientCurrentEmail={editRecipientCurrentEmail} />
|
<div id="recipient_cards_con" className={`${submitError.duplicateEmailSameRoutingOrder ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}>
|
||||||
|
<RecipientCards
|
||||||
|
userEmail={userData.email}
|
||||||
|
getMinRoutingOrder={getMinRoutingOrder}
|
||||||
|
transactionTemplateToggle={transactionTemplateToggle}
|
||||||
|
setEditRecipient={setEditRecipient}
|
||||||
|
setEditCase={setEditCase}
|
||||||
|
editRecipient={editRecipient}
|
||||||
|
setEditRecipientCurrentEmail={setEditRecipientCurrentEmail}
|
||||||
|
editRecipientCurrentEmail={editRecipientCurrentEmail}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.duplicateEmailSameRoutingOrder && "Each Signing order must have unique emails."}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={topRef}
|
ref={topRef}
|
||||||
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''}`}
|
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none' : ''} ${submitError.recipients ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""}`}
|
||||||
>
|
>
|
||||||
<RecipientAddCard userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
|
<RecipientAddCard userEmail={userData.email} getMinRoutingOrder={getMinRoutingOrder} transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} setEditRecipientCurrentEmail={setEditRecipientCurrentEmail} />
|
||||||
|
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.recipients && "Each Signing order must have unique emails."}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -450,16 +494,21 @@ const ProcessStepOne = () => {
|
||||||
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
|
<h2 className="text-lg font-medium mb-4 pt-6 border-t border-gray-200 text-[var(--processStepOneTextHead)]">Email</h2>
|
||||||
|
|
||||||
{/* Subject */}
|
{/* Subject */}
|
||||||
<div className='flex flex-col mb-4'>
|
<div className={`${submitError.subject ? "border border-red-500 rounded-sm p-2 ring-2 ring-red-300" : ""} flex flex-col mb-4`}>
|
||||||
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
|
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
|
||||||
<input
|
<input
|
||||||
|
id="subject"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
value={emailDtls.subject}
|
value={emailDtls.subject}
|
||||||
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
|
onChange={(e) => {
|
||||||
|
setEmailDtls({ ...emailDtls, subject: e.target.value })
|
||||||
|
submitError.subject && setSubmitError(prev => ({ ...prev, subject: false }))
|
||||||
|
}}
|
||||||
placeholder="Email Subject"
|
placeholder="Email Subject"
|
||||||
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-right px-2 pt-2 text-red-500">{submitError.subject && "Enter a subject."}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
|
|
|
||||||
|
|
@ -482,7 +482,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
||||||
value={recipient.email}
|
value={recipient.email}
|
||||||
placeholder="Recipient Email"
|
placeholder="Recipient Email"
|
||||||
className={`w-full px-3 sm:px-4 py-2 border rounded-md text-sm focus:outline-none transition
|
className={`w-full px-3 sm:px-4 py-2 border rounded-md text-sm focus:outline-none transition
|
||||||
${errors.email ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
${errors.email || errors.emailFormat ? 'border-red-500 ring-2 ring-red-300' : 'border-gray-300 focus:ring-2 focus:ring-[var(--primary)]'}`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setRecipients({ ...recipient, email: e.target.value });
|
setRecipients({ ...recipient, email: e.target.value });
|
||||||
setErrors((prev) => ({ ...prev, email: false }));
|
setErrors((prev) => ({ ...prev, email: false }));
|
||||||
|
|
@ -491,6 +491,9 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<p className="text-red-500 text-xs mt-1">Email is required</p>
|
<p className="text-red-500 text-xs mt-1">Email is required</p>
|
||||||
)}
|
)}
|
||||||
|
{errors.emailFormat && (
|
||||||
|
<p className="text-red-500 text-xs mt-1">Email is not valid</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ import { IoCloseOutline } from "react-icons/io5";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { FiEdit } from "react-icons/fi";
|
import { FiEdit } from "react-icons/fi";
|
||||||
import { FaUsersLine } from "react-icons/fa6";
|
import { FaUsersLine } from "react-icons/fa6";
|
||||||
|
import { IoMdClose } from "react-icons/io";
|
||||||
|
import { BiSolidHelpCircle } from "react-icons/bi";
|
||||||
|
|
||||||
|
|
||||||
const getInitials = (name: string) => {
|
const getInitials = (name: string) => {
|
||||||
if (!name) return "";
|
if (!name) return "";
|
||||||
|
|
@ -121,7 +124,7 @@ const RecipientCards = ({
|
||||||
return currentValue && currentValue.toString().trim() !== "";
|
return currentValue && currentValue.toString().trim() !== "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderEditableField = (
|
const renderEditableField = (
|
||||||
recipient: any,
|
recipient: any,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
placeholder: string,
|
placeholder: string,
|
||||||
|
|
@ -215,7 +218,7 @@ const RecipientCards = ({
|
||||||
setEditingField(null);
|
setEditingField(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
|
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
{recipients.length > 0 ? (
|
{recipients.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -238,7 +241,7 @@ const RecipientCards = ({
|
||||||
{recipients.map((recipient, idx) => (
|
{recipients.map((recipient, idx) => (
|
||||||
<div
|
<div
|
||||||
key={recipient.email + idx}
|
key={recipient.email + idx}
|
||||||
className={`flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 hover:bg-gray-50 transition-colors gap-3 lg:gap-0 ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
|
className={`relative flex flex-col lg:flex-row lg:items-center justify-between px-2 sm:px-3 lg:px-2 py-3 transition-colors gap-3 lg:gap-0 ${idx % 2 === 0 ? 'bg-gray-50' : 'bg-white'} ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
|
||||||
? "bg-neutral-100 opacity-40"
|
? "bg-neutral-100 opacity-40"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -250,16 +253,26 @@ const RecipientCards = ({
|
||||||
<div className="relative flex-shrink-0">
|
<div className="relative flex-shrink-0">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipient.routingOrder || 1}
|
value={recipient.routingOrder}
|
||||||
min={1}
|
// min={1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleOrderChange(
|
handleOrderChange(
|
||||||
recipient.id || recipient.email,
|
recipient.id || recipient.uuid,
|
||||||
Number(e.target.value)
|
Number(e.target.value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-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 bg-slate-100"
|
className="w-8 sm:w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{idx === 0 && recipients.length > 1 && <p className="group">
|
||||||
|
<BiSolidHelpCircle className="absolute top-[-12px] right-[-10px] text-gray-400 cursor-pointer" />
|
||||||
|
|
||||||
|
{/* Tooltip */}
|
||||||
|
<span className="absolute top-[2px] left-[48px] hidden group-hover:block w-56 p-2 rounded-lg text-sm bg-white text-slate-600 shadow-lg z-10">
|
||||||
|
Signing order: Recipients receive emails one after another.
|
||||||
|
Same order means they all receive at once.
|
||||||
|
</span>
|
||||||
|
</p>}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
|
|
@ -269,7 +282,7 @@ const RecipientCards = ({
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleOrderChange(
|
handleOrderChange(
|
||||||
recipient.id || recipient.email,
|
recipient.id || recipient.uuid,
|
||||||
Number(e.target.value)
|
Number(e.target.value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -278,19 +291,19 @@ const RecipientCards = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Avatar */}
|
{/* 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-7 h-7 sm:w-8 sm:h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
|
||||||
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
|
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Form Fields Section */}
|
{/* Form Fields Section */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{/* Role Name and Permissions Row */}
|
{/* Role Name and Permissions Row */}
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
|
<div className="flex flex-col lg:flex-row lg:items-center gap-2 mb-2">
|
||||||
<div className="flex-1 lg:w-1/2">
|
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <div className="flex-1 lg:w-1/2">
|
||||||
<p className="font-semibold text-gray-800 text-xs">
|
<div className="font-semibold text-gray-800 text-xs">
|
||||||
{renderEditableField(recipient, "roleName", "Role name")}
|
{renderEditableField(recipient, "roleName", "Role name")}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>}
|
||||||
|
|
||||||
{/* Permissions - Mobile: Below role, Desktop: Right side */}
|
{/* Permissions - Mobile: Below role, Desktop: Right side */}
|
||||||
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
|
<div className="lg:w-1/2 lg:flex lg:items-center lg:justify-end">
|
||||||
|
|
@ -337,25 +350,25 @@ const RecipientCards = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name and Email Fields */}
|
{/* Name and Email Fields */}
|
||||||
<div className="mb-2">
|
<div className="mb-2 w-[92%]">
|
||||||
{/* Mobile: Stack vertically, Tablet+: Side by side */}
|
{/* 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">
|
||||||
<div className="flex flex-col sm:flex-row gap-1 sm:gap-2 sm:flex-1">
|
<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="w-full sm:w-1/2">
|
||||||
<p className="font-semibold text-gray-800 text-xs">
|
<div className="font-semibold text-gray-800 text-xs">
|
||||||
{renderEditableField(recipient, "firstName", "First Name")}
|
{renderEditableField(recipient, "firstName", "First Name")}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<p className="font-semibold text-gray-800 text-xs">
|
<div className="font-semibold text-gray-800 text-xs">
|
||||||
{renderEditableField(recipient, "lastName", "Last Name")}
|
{renderEditableField(recipient, "lastName", "Last Name")}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:flex-1">
|
<div className="w-full sm:flex-1">
|
||||||
<p className="font-semibold text-gray-800 text-xs">
|
<div className="font-semibold text-gray-800 text-xs">
|
||||||
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
|
{renderEditableField(recipient, "email", "Recipient Email", "", "email")}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -365,14 +378,14 @@ const RecipientCards = ({
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<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">
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<p className="font-semibold text-gray-800 text-xs">
|
<div className="font-semibold text-gray-800 text-xs">
|
||||||
{renderEditableField(recipient, "hostName", "Host Name")}
|
{renderEditableField(recipient, "hostName", "Host Name")}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<p className="font-semibold text-gray-800 text-xs">
|
<div className="font-semibold text-gray-800 text-xs">
|
||||||
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
|
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -425,7 +438,7 @@ const RecipientCards = ({
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
{signType !== "self-sign" && (
|
{signType !== "self-sign" && (
|
||||||
<div className="flex justify-end lg:justify-center gap-2 items-center lg:ml-2">
|
<div className="lg:ml-2 absolute top-0 right-0 px-1.5 py-1.5 rounded-bl-sm border-l-1 border-b-1 border-slate-300 border-dashed">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Remove recipient"
|
title="Remove recipient"
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
||||||
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
|
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const childRef = useRef<any>(null);
|
const childRef = useRef<any>(null);
|
||||||
|
const tagUndoRef = useRef<DroppedItem[] | []>([]);
|
||||||
|
|
||||||
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||||
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||||
|
|
@ -84,7 +85,6 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
||||||
key: string | null;
|
key: string | null;
|
||||||
tagId?: string | null;
|
tagId?: string | null;
|
||||||
hotKey?: string | null;
|
hotKey?: string | null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,10 +153,8 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
||||||
if (selectedRecipients.length === 0) return;
|
if (selectedRecipients.length === 0) return;
|
||||||
fetchTags();
|
fetchTags();
|
||||||
}, [selectedRecipients]);
|
}, [selectedRecipients]);
|
||||||
|
|
||||||
// FETCHING TAG - END
|
// FETCHING TAG - END
|
||||||
|
|
||||||
|
|
||||||
const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1);
|
const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1);
|
||||||
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
|
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
|
||||||
|
|
||||||
|
|
@ -353,12 +351,13 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
||||||
console.log(tagIndex, "line 307", key);
|
console.log(tagIndex, "line 307", key);
|
||||||
|
|
||||||
setSelectedTag({ key, tagIndex })
|
setSelectedTag({ key, tagIndex })
|
||||||
}, 0)
|
}, 0)
|
||||||
};
|
};
|
||||||
// HANDLE DROP - END
|
// HANDLE DROP - END
|
||||||
|
|
||||||
|
console.log(selectedTag, "Line 359")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hotKeysObj.isActive) return
|
if (!hotKeysObj.isActive) return
|
||||||
|
|
||||||
|
|
@ -430,10 +429,10 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
||||||
|
|
||||||
const handleDocumentFocus = (key: string) => {
|
const handleDocumentFocus = (key: string) => {
|
||||||
// const id = key.replace("_", "");
|
// const id = key.replace("_", "");
|
||||||
// const targetCon = document.getElementById(id);
|
const targetCon = document.getElementById('doc-con');
|
||||||
selectedDoc.key = key;
|
selectedDoc.key = key;
|
||||||
|
|
||||||
document?.addEventListener("keydown", (event) => {
|
targetCon?.addEventListener("keydown", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const ctrl = event.ctrlKey;
|
const ctrl = event.ctrlKey;
|
||||||
const alt = event.altKey;
|
const alt = event.altKey;
|
||||||
|
|
@ -507,6 +506,7 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ transactionSaving
|
||||||
accountId={accountId}
|
accountId={accountId}
|
||||||
transactionId={transactionId}
|
transactionId={transactionId}
|
||||||
transactionTemplateToggle={transactionTemplateToggle}
|
transactionTemplateToggle={transactionTemplateToggle}
|
||||||
|
tagUndoRef={tagUndoRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { IoAdd } from 'react-icons/io5';
|
||||||
import { FaQuestionCircle } from 'react-icons/fa';
|
import { FaQuestionCircle } from 'react-icons/fa';
|
||||||
import api from '../../../config/api';
|
import api from '../../../config/api';
|
||||||
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
|
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
|
||||||
|
import { DroppedItem } from '../../../utlity/types';
|
||||||
|
|
||||||
interface DroppedItemsTagsConProps {
|
interface DroppedItemsTagsConProps {
|
||||||
droppedItems: DroppedItemsObj;
|
droppedItems: DroppedItemsObj;
|
||||||
|
|
@ -27,6 +28,7 @@ interface DroppedItemsTagsConProps {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
transactionId: string;
|
transactionId: string;
|
||||||
transactionTemplateToggle: string;
|
transactionTemplateToggle: string;
|
||||||
|
tagUndoRef: React.MutableRefObject<DroppedItem[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👶 Child component with forwardRef
|
// 👶 Child component with forwardRef
|
||||||
|
|
@ -39,12 +41,15 @@ const DroppedItemsTagsCon = forwardRef(({
|
||||||
setDroppedItems,
|
setDroppedItems,
|
||||||
accountId,
|
accountId,
|
||||||
transactionId,
|
transactionId,
|
||||||
transactionTemplateToggle
|
transactionTemplateToggle,
|
||||||
|
tagUndoRef
|
||||||
}: DroppedItemsTagsConProps, ref) => {
|
}: DroppedItemsTagsConProps, ref) => {
|
||||||
|
|
||||||
const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 });
|
const positionRef = useRef<{ left: number; top: number }>({ left: 0, top: 0 });
|
||||||
|
|
||||||
const handleTagClick = (key: string, index: number) => {
|
const handleTagClick = (key: string, index: number) => {
|
||||||
|
console.log(index, "line 307", key);
|
||||||
|
|
||||||
setSelectedTag({ key, index })
|
setSelectedTag({ key, index })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +70,8 @@ const DroppedItemsTagsCon = forwardRef(({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSelectedTag(null);
|
setSelectedTag(null);
|
||||||
|
|
||||||
// Check if this item has a group for special group handling
|
// Check if this item has a group for special group handling
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { fetchRecipients, recipientType, toggleToTemplates, TRANSACTION_TEMPLATE
|
||||||
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { Bounce, toast } from 'react-toastify'
|
import { Bounce, toast } from 'react-toastify'
|
||||||
import { isPending } from '@reduxjs/toolkit'
|
import { FiChevronDown } from 'react-icons/fi'
|
||||||
|
|
||||||
interface NavTwoProps {
|
interface NavTwoProps {
|
||||||
transactionSaving?: boolean;
|
transactionSaving?: boolean;
|
||||||
|
|
@ -199,12 +199,12 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
||||||
{/* Left Side */}
|
{/* Left Side */}
|
||||||
<div className='flex justify-start items-center gap-2'>
|
<div className='flex justify-start items-center gap-2'>
|
||||||
|
|
||||||
{!isPending && <div
|
<div
|
||||||
onClick={() => handleSetIsFields(true)}
|
onClick={() => handleSetIsFields(true)}
|
||||||
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>
|
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>
|
||||||
<LuTableOfContents />
|
<LuTableOfContents />
|
||||||
<p className='tracking-wide'>Fields</p>
|
<p className='tracking-wide'>Fields</p>
|
||||||
</div>}
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => handleSetIsFields(false)}
|
onClick={() => handleSetIsFields(false)}
|
||||||
|
|
@ -256,6 +256,32 @@ const NavTwo: React.FC<NavTwoProps> = ({ transactionSaving = false, isFields, se
|
||||||
{/* Right Side - STARTS */}
|
{/* Right Side - STARTS */}
|
||||||
<div className='flex justify-center items-center'>
|
<div className='flex justify-center items-center'>
|
||||||
|
|
||||||
|
<div className="relative inline-block text-left">
|
||||||
|
{/* Trigger */}
|
||||||
|
<button className="peer flex justify-center items-center bg-white px-4 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50 ">
|
||||||
|
<p className='tracking-wide mr-1'>Actions</p>
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Menu */}
|
||||||
|
<div className="absolute right-0 mt-2 w-48 hidden peer-focus:block rounded-md bg-white shadow-sm ring-1 ring-black/5 z-10">
|
||||||
|
<div className="py-1">
|
||||||
|
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
|
||||||
|
Save & Close
|
||||||
|
</button>
|
||||||
|
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
|
||||||
|
Edit Message
|
||||||
|
</button>
|
||||||
|
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
|
||||||
|
Edit Recipients
|
||||||
|
</button>
|
||||||
|
<button className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100">
|
||||||
|
Advanced Options
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Documnt Nav */}
|
{/* Documnt Nav */}
|
||||||
<div tabIndex={1} onBlur={() => setNav2DocToggle(false)}>
|
<div tabIndex={1} onBlur={() => setNav2DocToggle(false)}>
|
||||||
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import logoRecord from '../../assets/record.svg';
|
import logoRecord from '../../assets/record.svg';
|
||||||
import LoaderUi from '../../helper/LoaderUi';
|
import LoaderUi from '../../helper/LoaderUi';
|
||||||
import api from '../../config/api';
|
import api from '../../config/api';
|
||||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||||
|
import { TemplateInterface } from '../../utlity/types';
|
||||||
|
|
||||||
const TemplateTable = () => {
|
const TemplateTable = () => {
|
||||||
const loaction = useLocation()
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [templates, setTemplates] = useState([])
|
const [templates, setTemplates] = useState<TemplateInterface[] | []>([])
|
||||||
const accountDetails = useSelector((state: any) => state.user.account);
|
const accountDetails = useSelector((state: any) => state.user.account);
|
||||||
const accountId = accountDetails.id || "";
|
const accountId = accountDetails.id || "";
|
||||||
const [showMenu, setShowMenu] = useState(null);
|
const [showMenu, setShowMenu] = useState(null);
|
||||||
|
|
@ -139,7 +138,7 @@ const TemplateTable = () => {
|
||||||
<th className="px-6 py-4 text-center font-semibold">Last Updated</th>
|
<th className="px-6 py-4 text-center font-semibold">Last Updated</th>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<th className="px-6 py-4 text-center font-semibold">Use</th>
|
{/* <th className="px-6 py-4 text-center font-semibold">Use</th> */}
|
||||||
<th className="px-6 py-4 text-center font-semibold">Action</th>
|
<th className="px-6 py-4 text-center font-semibold">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -181,14 +180,14 @@ const TemplateTable = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<td className="px-6 py-4 text-center">
|
{/* <td className="px-6 py-4 text-center">
|
||||||
<button
|
<button
|
||||||
className="bg-[var(--lightGreyCol)] cursor-pointer border-gray-300 text-gray-600 hover:bg-gray-100 px-3 py-1 rounded-lg font-semibold text-xs transition-colors duration-150"
|
className="cursor-pointer bg-[var(--primary)] border-gray-300 text-white hover:bg-gray-100 px-3 py-2 rounded-lg font-semibold text-xs transition-colors duration-150"
|
||||||
onClick={() => handleUseClick(item)}
|
onClick={() => handleUseClick(item)}
|
||||||
>
|
>
|
||||||
Use
|
USE
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td> */}
|
||||||
|
|
||||||
<td className="px-6 py-4 text-center">
|
<td className="px-6 py-4 text-center">
|
||||||
<div
|
<div
|
||||||
|
|
@ -201,7 +200,7 @@ const TemplateTable = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
|
className="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors duration-150 p-1 rounded-md hover:bg-gray-100"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setShowMenu(showMenu === index ? null : index)
|
setShowMenu(showMenu === index ? null : index)
|
||||||
}
|
}
|
||||||
|
|
@ -210,10 +209,16 @@ const TemplateTable = () => {
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showMenu === index && (
|
{showMenu === index && (
|
||||||
<div className="absolute -top-9 right-4 bg-white border border-gray-200 rounded-lg shadow-lg z-20 min-w-[100px]">
|
<div className="absolute top-7.5 right-0 bg-white border border-gray-200 rounded-xl shadow-lg z-20 min-w-[140px] overflow-hidden animate-fade-in">
|
||||||
|
<button
|
||||||
|
onClick={() => handleUseClick(item)}
|
||||||
|
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Use
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onMouseDown={() => handleEditClick(item.id)}
|
onMouseDown={() => handleEditClick(item.id)}
|
||||||
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
|
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-yellow-50 hover:text-yellow-600 transition-colors"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -221,12 +226,13 @@ const TemplateTable = () => {
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
setShowMenu(null);
|
setShowMenu(null);
|
||||||
}}
|
}}
|
||||||
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
|
className="w-full text-left px-4 py-2 text-sm cursor-pointer text-gray-700 hover:bg-red-50 hover:text-red-600 transition-colors"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,10 @@ const TransactionView = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log(transactionDetails, "transactionDetails")
|
||||||
|
|
||||||
|
|
||||||
const fetchDocuments = async () => {
|
const fetchDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents`)
|
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents`)
|
||||||
|
|
@ -155,13 +159,16 @@ const TransactionView = () => {
|
||||||
More
|
More
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* <Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-two/${accountId}/${transactionId}`}>
|
<Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-one/${accountId}/${transactionId}`}>
|
||||||
Edit
|
Edit
|
||||||
</Link> */}
|
</Link>
|
||||||
|
|
||||||
<button className='cursor-pointer border text-[var(--primary)] border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' onClick={handleOpenAuditModal}>
|
<button className='cursor-pointer border text-[var(--primary)] border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' onClick={handleOpenAuditModal}>
|
||||||
Audit
|
Audit
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className='my-6 border-slate-200' />
|
<hr className='my-6 border-slate-200' />
|
||||||
|
|
@ -203,7 +210,7 @@ const TransactionView = () => {
|
||||||
<p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p>
|
<p className={`text-black text-xs font-semibold rounded-lg px-2 py-0.5 ${getStatusBadge(recipient.status)}`}>{recipient.status}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{recipient.email === user.email && recipient.status !== "COMPLETED" && (
|
{transactionDetails.transaction.status === "SENT" && recipient.email === user.email && recipient.status !== "COMPLETED" && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavigateToSign(recipient.id)}
|
onClick={() => handleNavigateToSign(recipient.id)}
|
||||||
className="text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold text-xs hover:bg-opacity-90 transition"
|
className="text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold text-xs hover:bg-opacity-90 transition"
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ api.interceptors.response.use(
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
// Prevent multiple refresh attempts
|
// Prevent multiple refresh attempts
|
||||||
// if (error.response?.status === 401 || error.response?.status === 500) {
|
// if (error.response?.status === 401 || error.response?.status === 500) {
|
||||||
if (error.response?.status === 401 || error.response?.status === 500) {
|
if (error.response?.status === 401) {
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
try {
|
try {
|
||||||
if (!isRefreshing) {
|
if (!isRefreshing) {
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ const selectedRecipientsSlice = createSlice({
|
||||||
},
|
},
|
||||||
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
||||||
const { id, routingOrder } = action.payload;
|
const { id, routingOrder } = action.payload;
|
||||||
const recipient = state.recipients.find((r) => r.id === id || r.email === id);
|
const recipient = state.recipients.find((r) => r.id === id || r.uuid === id);
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
recipient.routingOrder = routingOrder;
|
recipient.routingOrder = routingOrder;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,3 +207,46 @@ export interface DroppedItem {
|
||||||
|
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface TemplateInterface {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
code: string;
|
||||||
|
description: string | null;
|
||||||
|
active: boolean;
|
||||||
|
enableIdStamping: boolean;
|
||||||
|
enableMobileSigning: boolean;
|
||||||
|
enableSignatureIdStamping: boolean;
|
||||||
|
customFields: any[];
|
||||||
|
allowReassign: boolean;
|
||||||
|
allowViewAudit: boolean;
|
||||||
|
subject: string | null;
|
||||||
|
message: string | null;
|
||||||
|
reminderConfig: {
|
||||||
|
reminderEnabled: boolean;
|
||||||
|
reminderFrequency: number;
|
||||||
|
reminderDelay: number;
|
||||||
|
expireReminder: number;
|
||||||
|
};
|
||||||
|
expireAfter: number;
|
||||||
|
expireEnabled: boolean;
|
||||||
|
recipients: any | null;
|
||||||
|
documents: any | null;
|
||||||
|
createdAt: string;
|
||||||
|
lastModifiedAt: string;
|
||||||
|
createdBy: {
|
||||||
|
userId: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
lastModifiedBy: {
|
||||||
|
userId: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue