560 lines
25 KiB
TypeScript
560 lines
25 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
FaArrowLeft,
|
||
FaCheck,
|
||
FaExternalLinkAlt,
|
||
FaEye,
|
||
FaEyeSlash,
|
||
FaRedo,
|
||
FaImage,
|
||
FaUsers,
|
||
FaFileAlt,
|
||
FaEnvelope,
|
||
FaCog,
|
||
FaShieldAlt,
|
||
FaBell,
|
||
FaDownload,
|
||
FaShare,
|
||
FaEdit,
|
||
FaSave,
|
||
FaClock,
|
||
FaLock
|
||
} from 'react-icons/fa';
|
||
|
||
import { RootState } from '../../../store/store';
|
||
|
||
import api from '../../../config/api';
|
||
import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice";
|
||
import { fetchDocuments } from "../../../store/slices/documentSlice";
|
||
import { useDispatch, useSelector } from "react-redux";
|
||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||
import Loader from "../../dashboard/Loader";
|
||
import confetti from 'canvas-confetti'
|
||
import { IoClose } from "react-icons/io5";
|
||
import { IoMdClose, IoMdHome } from "react-icons/io";
|
||
import { Bounce, toast } from "react-toastify";
|
||
|
||
|
||
type Signer = {
|
||
id: number;
|
||
name: string;
|
||
email: string;
|
||
routingOrder: number;
|
||
completed: boolean;
|
||
};
|
||
|
||
// Signers List Component
|
||
const SignersList: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||
return (
|
||
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100 ">
|
||
<div className="p-6 ">
|
||
<div className="flex items-center justify-evenly mb-2">
|
||
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2 ">
|
||
<FaUsers className="text-blue-600" />
|
||
Signature Status
|
||
</h3>
|
||
<span className="text-sm text-gray-600">
|
||
{signers.filter(s => s.completed).length} of {signers.length} signed
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-3 max-h-[20vh] overflow-y-auto no-scrollbar">
|
||
{signers.map((signer) => (
|
||
<div key={signer.id} className="text-xs mx-auto flex items-center gap-4 p-3 w-[70vh] bg-gradient-to-br from-gray-50 to-blue-100 rounded-lg">
|
||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-medium">
|
||
{signer.name.length > 2 ? signer.name.split(" ").map(n => n[0].toUpperCase()).join("") : signer.email.split("").map(n => n[0].toUpperCase()).join("")}
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="font-medium text-gray-800">{signer.name}</div>
|
||
<div className="text-xs text-gray-600">{signer.email}</div>
|
||
<div className="text-xs text-gray-500">Order: {signer.routingOrder}</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{signer.completed ? (
|
||
<div className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs">
|
||
<FaCheck className="w-3 h-3" />
|
||
Signed
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-1 px-3 py-1 bg-orange-100 text-orange-700 rounded-full text-xs">
|
||
<FaClock className="w-3 h-3" />
|
||
Pending
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
// </div>
|
||
);
|
||
};
|
||
|
||
// Fixed Signing Order Diagram Component
|
||
const SigningOrderDiagram: React.FC<{ signers: Signer[] }> = ({ signers }) => {
|
||
const groupByRoutingOrder = (data: Signer[]) => {
|
||
const grouped: Record<number, Signer[]> = {};
|
||
data.forEach((signer) => {
|
||
if (!grouped[signer.routingOrder]) {
|
||
grouped[signer.routingOrder] = [];
|
||
}
|
||
grouped[signer.routingOrder].push(signer);
|
||
});
|
||
return grouped;
|
||
};
|
||
|
||
const getInitials = (name: string) => {
|
||
return name
|
||
.split(" ")
|
||
.map((n) => n[0].toUpperCase())
|
||
.join("");
|
||
};
|
||
|
||
const grouped = groupByRoutingOrder(signers);
|
||
const sortedOrders = Object.keys(grouped)
|
||
.map(Number)
|
||
.sort((a, b) => a - b);
|
||
|
||
const currentProcessingOrder =
|
||
sortedOrders.find((order) =>
|
||
grouped[order].some((signer) => !signer.completed)
|
||
) || -1;
|
||
|
||
return (
|
||
// <div className="bg-white/60 rounded-md shadow-sm border border-gray-100">
|
||
<div className="py-3">
|
||
<h3 className="text-lg font-semibold text-gray-800 mb-1 text-center ">
|
||
Signing Flow</h3>
|
||
<div className="overflow-x-auto overflow-y-auto h-[18vh] no-scrollbar flex justify-center items-center">
|
||
<div className="flex justify-center items-center gap-2 min-w-max">
|
||
{/* Mail icon */}
|
||
<div className="flex flex-col items-center">
|
||
<div className="bg-blue-100 p-4 rounded-full">
|
||
<FaEnvelope className="text-blue-600 text-2xl" />
|
||
</div>
|
||
<p className="text-sm text-gray-600 mt-2">Sent</p>
|
||
</div>
|
||
|
||
{/* Dotted line */}
|
||
<div className="flex items-center mb-3">
|
||
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
|
||
</div>
|
||
|
||
{/* Signers by routing order */}
|
||
<div className="flex items-center justify-center space-x-6">
|
||
{sortedOrders.map((order, index) => (
|
||
<React.Fragment key={order}>
|
||
<div className="flex flex-col items-center space-y-4 mb-4">
|
||
<div className="text-sm font-medium text-gray-600 mb-2">
|
||
Step {order}
|
||
</div>
|
||
<div className="flex flex-col items-center space-y-3">
|
||
{grouped[order].map((signer) => {
|
||
let bgColor = "";
|
||
let icon = null;
|
||
|
||
if (signer.completed) {
|
||
bgColor = "bg-[var(--primary)]";
|
||
icon = <FaCheck className="text-green-600" />;
|
||
} else if (order === currentProcessingOrder) {
|
||
bgColor = "bg-yellow-500";
|
||
icon = <FaClock className="text-yellow-800" />;
|
||
} else {
|
||
bgColor = "bg-gray-400";
|
||
icon = <FaLock className="text-gray-500" />;
|
||
}
|
||
|
||
return (
|
||
<div key={signer.id} className="relative">
|
||
<div
|
||
className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`}
|
||
title={signer.name}
|
||
>
|
||
{getInitials(signer.name)}
|
||
<span className="absolute bottom-[-4px] right-[-8px] p-1 rounded-full bg-white shadow">
|
||
{icon}
|
||
</span>
|
||
</div>
|
||
<p className="mt-1 text-xs text-gray-600 text-center max-w-16 truncate">
|
||
{signer.name.split(" ")[0] || signer.firstName.split(0, 1)}
|
||
</p>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{index < sortedOrders.length - 1 && (
|
||
<div className="flex items-center">
|
||
<div className="w-16 border-t-2 border-dotted border-gray-300"></div>
|
||
</div>
|
||
)}
|
||
</React.Fragment>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
// </div>
|
||
);
|
||
};
|
||
const DocumentSubmissionOptions = () => {
|
||
const location = useLocation();
|
||
const [status, setStatus] = useState(null);
|
||
console.log("location.pathname", location.pathname, status)
|
||
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||
const [showConfetti, setShowConfetti] = React.useState(true)
|
||
if (!accountId || !transactionId) {
|
||
window.location.href = "/";
|
||
return null;
|
||
}
|
||
|
||
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
|
||
const navigate = useNavigate();
|
||
const dispatch = useDispatch();
|
||
|
||
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||
console.log('Line 218', selectedDocuments)
|
||
const [transactionData, setTransactionData] = useState({})
|
||
|
||
|
||
useEffect(() => {
|
||
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
// toast.dismiss();
|
||
// toast.success("Your document has been successfully sent for signature.", {
|
||
// position: "top-center",
|
||
// autoClose: 1500,
|
||
// hideProgressBar: false,
|
||
// closeOnClick: false,
|
||
// pauseOnHover: true,
|
||
// draggable: true,
|
||
// progress: undefined,
|
||
// theme: "light",
|
||
// transition: Bounce,
|
||
// });
|
||
|
||
if (showConfetti) {
|
||
const duration = 3000;
|
||
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]);
|
||
|
||
|
||
console.log("History length:", window.history.length);
|
||
|
||
|
||
// Step 1: Fetch transaction status
|
||
useEffect(() => {
|
||
const fetchTransaction = async () => {
|
||
// /api/accounts/{accountId}/transactions/{trxId}
|
||
try {
|
||
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
|
||
if (res?.data) {
|
||
setStatus(res?.data?.status);
|
||
setTransactionData(res?.data)
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch transaction:", error);
|
||
}
|
||
};
|
||
|
||
if (accountId && transactionId) {
|
||
fetchTransaction();
|
||
}
|
||
}, [accountId, transactionId]);
|
||
|
||
// Step 2: Prevent browser back when status === 'SENT'
|
||
useEffect(() => {
|
||
if (status === "SENT") {
|
||
|
||
const currentPath = window.location.pathname;
|
||
|
||
const handlePopState = (event) => {
|
||
// Show warning
|
||
alert("Back navigation is blocked on this screen.");
|
||
|
||
// Push the current URL back to prevent navigating away
|
||
window.history.pushState(null, "", currentPath);
|
||
};
|
||
|
||
// Push extra states to build history buffer
|
||
for (let i = 0; i < 50; i++) {
|
||
window.history.pushState(null, "", currentPath);
|
||
}
|
||
|
||
// Add popstate listener
|
||
window.addEventListener("popstate", handlePopState);
|
||
|
||
// Cleanup on unmount
|
||
return () => {
|
||
window.removeEventListener("popstate", handlePopState);
|
||
};
|
||
}
|
||
}, [status]);
|
||
|
||
|
||
// useEffect(() => {
|
||
// if (status === 'SENT') {
|
||
// // Step 1: Push a dummy state to trap the user
|
||
// window.history.pushState(null, '', window.location.href);
|
||
|
||
// // Step 2: Intercept back button
|
||
// const handlePopState = (event) => {
|
||
// // Push the same dummy state again to block back
|
||
// window.history.pushState(null, '', window.location.href);
|
||
// };
|
||
|
||
// window.addEventListener('popstate', handlePopState);
|
||
|
||
// return () => {
|
||
// window.removeEventListener('popstate', handlePopState);
|
||
// };
|
||
// }
|
||
// }, [status]);
|
||
const [selectedOption, setSelectedOption] = useState('success');
|
||
const [isVisible, setIsVisible] = useState(false);
|
||
const [previewData, setPreviewData] = useState({
|
||
documentName: "Contract Agreement.pdf",
|
||
submissionCount: 1,
|
||
signerName: "Sumit Kumar",
|
||
signerEmail: "sumit.kumar@hicglobalsolutions.com",
|
||
submissionTime: new Date().toLocaleString()
|
||
});
|
||
|
||
useEffect(() => {
|
||
setIsVisible(true);
|
||
}, []);
|
||
|
||
const options = [
|
||
{
|
||
id: 'success',
|
||
icon: FaCheck,
|
||
title: 'Success screen',
|
||
description: 'Display confirmation message after successful document submission',
|
||
color: 'bg-green-50 border-green-200 hover:bg-green-100',
|
||
iconColor: 'text-green-600',
|
||
isPremium: false
|
||
},
|
||
{
|
||
id: 'redirect',
|
||
icon: FaExternalLinkAlt,
|
||
title: 'Signing Flow',
|
||
description: 'Navigate user to view their transaction details',
|
||
color: 'bg-blue-50 border-blue-200 hover:bg-blue-100',
|
||
iconColor: 'text-blue-600',
|
||
isPremium: true
|
||
},
|
||
{
|
||
id: 'public',
|
||
icon: FaEye,
|
||
title: 'Signature status',
|
||
description: 'Show transaction status on public dashboard',
|
||
color: 'bg-purple-50 border-purple-200 hover:bg-purple-100',
|
||
iconColor: 'text-purple-600',
|
||
isPremium: true
|
||
},
|
||
];
|
||
|
||
const getPreviewContent = () => {
|
||
switch (selectedOption) {
|
||
case 'success':
|
||
return (
|
||
<div className="text-center space-y-6">
|
||
<div className="w-24 h-24 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-6 mx-auto shadow-lg animate-bounce">
|
||
<FaCheck className="text-white text-4xl" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
|
||
{selectedDocuments?.map((docs) => (
|
||
<p className="text-gray-600">Your document "{docs.name}" has been sent for signature.</p>
|
||
))}
|
||
</div>
|
||
<div className="bg-gray-50 rounded-lg p-4 space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-500">createdBy:</span>
|
||
<span className="font-medium">{transactionData?.createdBy?.name}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-500">Email:</span>
|
||
<span className="font-medium">{transactionData?.createdBy?.email}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-500">createdAt:</span>
|
||
<span className="font-medium">{transactionData.createdAt}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
case 'redirect':
|
||
return (
|
||
<>
|
||
|
||
<SigningOrderDiagram signers={selectedRecipients} />
|
||
</>
|
||
);
|
||
case 'public':
|
||
return (
|
||
<>
|
||
<SignersList signers={selectedRecipients} />
|
||
</>
|
||
);
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
|
||
<div className="flex items-center space-x-4 mb-3 bg-white w-full py-4 px-5 shadow">
|
||
<Link to={'/'} className="flex items-center space-x-2 text-gray-600 hover:text-[var(--primary)] transition-colors duration-200 group">
|
||
<IoMdHome
|
||
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
|
||
/>
|
||
</Link>
|
||
<div className="h-8 w-px bg-gray-300"></div>
|
||
<h1 className="text-md text-gray-800">🎉 Awesome! The document’s been sent for signature. 🎉</h1>
|
||
</div>
|
||
<div className={`max-w-4xl mx-auto transition-all bg-white rounded-lg duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
|
||
|
||
{/* Header */}
|
||
|
||
|
||
{/* <div className="grid grid-cols-1 lg:grid-cols-2 gap-3 max-w-5xl mx-auto">
|
||
<div className="space-y-6 ">
|
||
|
||
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 min-h-[81vh] ">
|
||
<div className="p-6">
|
||
<h3 className="text-lg font-semibold text-gray-800 mb-4">
|
||
Choose an action after a signer completes the document.
|
||
</h3>
|
||
<div className="space-y-3">
|
||
{options.map((option) => {
|
||
const Icon = option.icon;
|
||
const isSelected = selectedOption === option.id;
|
||
|
||
return (
|
||
<div
|
||
key={option.id}
|
||
onClick={() => setSelectedOption(option.id)}
|
||
className={`
|
||
relative cursor-pointer p-4 rounded-xl border-2 transition-all duration-200 group
|
||
${isSelected
|
||
? 'border-[var(--primary)] bg-blue-50 shadow-md scale-[1.02]'
|
||
: option.color + ' border-2'
|
||
}
|
||
hover:shadow-md hover:scale-[1.01] transform
|
||
`}
|
||
>
|
||
<div className="flex items-start space-x-4">
|
||
<div className={`
|
||
w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200
|
||
${isSelected
|
||
? 'bg-[var(--primary)] text-white shadow-lg'
|
||
: 'bg-white ' + option.iconColor + ' group-hover:scale-110'
|
||
}
|
||
`}>
|
||
<Icon className="text-lg" />
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center space-x-2">
|
||
<h4 className={`font-semibold transition-colors duration-200 ${isSelected ? 'text-[var(--primary)]' : 'text-gray-800'
|
||
}`}>
|
||
{option.title}
|
||
</h4>
|
||
{option.isPremium && (
|
||
<span className="text-purple-500 text-sm">★</span>
|
||
)}
|
||
</div>
|
||
<p className="text-sm text-gray-600 mt-1">
|
||
{option.description}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{isSelected && (
|
||
<div className="absolute top-4 right-4">
|
||
<div className="w-6 h-6 bg-[var(--primary)] rounded-full flex items-center justify-center">
|
||
<FaCheck className="text-white text-xs" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
<div className="space-y-6">
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden sticky top-6">
|
||
<div className="bg-gradient-to-r from-gray-50 to-blue-50 px-6 py-2 border-b border-gray-200">
|
||
<h3 className="text-lg font-semibold text-gray-800">Live Preview</h3>
|
||
<p className="text-sm text-gray-500">See how your users will experience the completion</p>
|
||
</div>
|
||
|
||
<div className="p-3 min-h-[70vh] flex justify-center">
|
||
<div className="w-full transition-all duration-500 transform">
|
||
{getPreviewContent()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div> */}
|
||
|
||
<div className="text-center pt-2 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>
|
||
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
|
||
</div>
|
||
</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 className=' min-h-[30vh]'>
|
||
<SignersList signers={selectedRecipients} />
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DocumentSubmissionOptions; |