SME-Web/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx

544 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-between mb-4">
<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-[50vh] overflow-y-auto no-scrollbar">
{signers.map((signer) => (
<div key={signer.id} className="flex items-center gap-4 p-3 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-sm 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-sm">
<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-sm">
<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="p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Signing Flow</h3>
<div className="overflow-x-auto overflow-y-auto h-[50vh] no-scrollbar flex justify-center items-center">
<div className="flex justify-center items-center gap-4 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 p-6">
<div className={`max-w-6xl mx-auto transition-all duration-700 ${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}>
{/* Header */}
<div className="flex items-center space-x-4 mb-5">
<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-2xl font-bold text-gray-800">🎉 Awesome! The document’s been sent for signature. 🎉</h1>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 max-w-5xl mx-auto">
{/* Left Panel - Configuration */}
<div className="space-y-6 ">
{/* Action Selection */}
<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>
{/* Selection indicator */}
{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>
{/* Custom Image Section */}
</div>
{/* Right Panel - Preview */}
<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>
</div>
);
};
export default DocumentSubmissionOptions;