357 lines
17 KiB
TypeScript
357 lines
17 KiB
TypeScript
import React, { useEffect } from "react";
|
||
import {
|
||
FaCheck,
|
||
FaFileAlt,
|
||
FaUsers,
|
||
FaEnvelope,
|
||
FaClock,
|
||
FaDownload,
|
||
FaShare,
|
||
FaShieldAlt,
|
||
FaLock,
|
||
FaPen,
|
||
FaCheckCircle
|
||
} from 'react-icons/fa';
|
||
import { fetchRecipients } from "../../../store/slices/selectedRecipientsSlice";
|
||
import { fetchDocuments } from "../../../store/slices/documentSlice";
|
||
import { useDispatch, useSelector } from "react-redux";
|
||
import { Link, 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-[29vh] overflow-y-auto no-scrollbar">
|
||
{signers.map((signer) => (
|
||
<div key={signer.id} className="flex items-center gap-4 p-3 bg-white/40 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-[69vh] 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]}
|
||
</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>
|
||
);
|
||
};
|
||
|
||
// Main DocumentCompletionScreen Component
|
||
const DocumentCompletionScreen = () => {
|
||
const documentList = [
|
||
{ title: 'Document 1', pages: 12 },
|
||
{ title: 'Document 2', pages: 8 },
|
||
{ title: 'Document 3', pages: 5 },
|
||
{ title: 'Document 4', pages: 22 },
|
||
{ title: 'Document 5', pages: 10 },
|
||
{ title: 'Document 6', pages: 15 },
|
||
{ title: 'Document 7', pages: 7 },
|
||
{ title: 'Document 8', pages: 9 },
|
||
{ title: 'Document 9', pages: 6 },
|
||
{ title: 'Document 10', pages: 11 },
|
||
{ title: 'Document 11', pages: 4 }, // Won't be shown due to slice
|
||
];
|
||
|
||
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)
|
||
|
||
|
||
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]);
|
||
|
||
|
||
|
||
return (
|
||
<>
|
||
<div className="min-h-screen bg-[var(--mainViewBg)]">
|
||
{/* Top Banner */}
|
||
<div className='fixed top-0 w-full flex items-center justify-between bg-white shadow z-50 px-4 py-3'>
|
||
<div className='flex items-center gap-2'>
|
||
<Link to={"/"}>
|
||
<IoMdHome
|
||
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
|
||
/>
|
||
</Link>
|
||
<p className="text-base font-normal text-gray-700 border-l pl-4">🎉 Awesome! The document’s been sent for signature. 🎉</p>
|
||
</div>
|
||
|
||
<div
|
||
id="add-tags-btn-step-one"
|
||
className='flex items-center gap-3'>
|
||
|
||
<Link to={`/accounts/${accountId}/transactions/${transactionId}`} className="flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]">
|
||
<FaPen className=" text-white text-md" />
|
||
<span className="tracking-wide text-white">Edit</span>
|
||
</Link>
|
||
|
||
{/* <button
|
||
onClick={() => handleNextClick(false)}
|
||
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
|
||
>
|
||
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
|
||
</button> */}
|
||
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
{(selectedRecipients.length > 0 && selectedDocuments.length > 0) ?
|
||
<div className="p-3 mt-[55px]">
|
||
<div className="max-w-[97vw] mx-auto">
|
||
{/* Header */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
||
{/* Left Section - Document Details */}
|
||
<div className="lg:col-span-6 space-y-2">
|
||
{/* Document Preview Card */}
|
||
<div className="bg-white/60 rounded-md shadow-sm border border-gray-100 overflow-hidden max-h-[88vh] no-scrollbar overflow-y-auto">
|
||
<div className="p-3 ">
|
||
{selectedDocuments?.map((item, index) => (
|
||
<div key={item.id} className="flex items-start justify-between mb-2 px-2 py-3">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||
{/* <FaFileAlt className="text-blue-600 text-xl" /> */}
|
||
<img src={item.pages[0].src} alt="document" className="h-10 w-10" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-md font-semibold text-gray-800">
|
||
Name : {item.name ?? '--'}
|
||
</h3>
|
||
<div className="text-gray-600 text-sm">
|
||
Status : {item.status ?? '--'}
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div><SignersList signers={selectedRecipients} /></div>
|
||
</div>
|
||
|
||
{/* Right Section - Signer Components */}
|
||
<div className="lg:col-span-6 space-y-1.5">
|
||
{/* Signing Order Diagram */}
|
||
<SigningOrderDiagram signers={selectedRecipients} />
|
||
|
||
{/* Signers List */}
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
: <Loader />}
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default DocumentCompletionScreen; |