356 lines
18 KiB
TypeScript
356 lines
18 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 { IoMdHome } from "react-icons/io";
|
|
|
|
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.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(() => {
|
|
if (showConfetti) {
|
|
const duration = 3000;
|
|
const end = Date.now() + duration;
|
|
|
|
const interval = setInterval(() => {
|
|
if (Date.now() > end) {
|
|
clearInterval(interval);
|
|
return;
|
|
}
|
|
|
|
confetti({
|
|
particleCount: 80, // more particles
|
|
angle: Math.random() * 360, // fire in random direction
|
|
spread: 220, // wider spread
|
|
startVelocity: 15, // more power
|
|
scalar: 1.2, // bigger pieces (default is 1)
|
|
origin: {
|
|
x: Math.random(),
|
|
y: Math.random() * 0.6,
|
|
},
|
|
zIndex: 9999,
|
|
});
|
|
}, 350); // fire every 150ms
|
|
}
|
|
}, [showConfetti]);
|
|
|
|
return (
|
|
<>
|
|
<>
|
|
{
|
|
showConfetti && (
|
|
<div className="absolute top-0 left-0 w-full h-full z-[9999] bg-white/80 backdrop-blur-sm flex items-center justify-center">
|
|
{/* <Confetti width={width} height={height} /> */}
|
|
|
|
<div className="relative bg-white bg-opacity-90 backdrop-blur-lg border border-green-200 shadow-xl rounded-md px-8 py-6 w-[90%] max-w-sm text-center animate-fade-in-up">
|
|
{/* Close Icon */}
|
|
<button
|
|
onClick={() => setShowConfetti(false)} // or whatever logic hides the modal
|
|
className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
|
|
aria-label="Close"
|
|
>
|
|
<IoClose className="text-2xl" />
|
|
</button>
|
|
|
|
{/* Success Icon */}
|
|
<FaCheckCircle className="text-green-500 text-5xl mb-4 mx-auto drop-shadow-lg" />
|
|
|
|
{/* Message */}
|
|
<h2 className="text-2xl font-semibold text-gray-800 mb-1">Transaction Successful</h2>
|
|
<p className="text-gray-600 text-sm">Your transaction has been created and saved successfully.</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
</>
|
|
<div className="min-h-screen bg-[var(--mainViewBg)]">
|
|
{/* Top Banner */}
|
|
|
|
<div className="w-full px-6 py-2 bg-white border-b rounded-sm border-gray-200">
|
|
<div className="max-w-[97vw] mx-auto px-2 py-2.5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Link to='/' className="w-5 h-5 text-green-700 rounded-full flex items-center justify-center">
|
|
<IoMdHome className="h-10 w-10" />
|
|
</Link>
|
|
{/* <div className="w-5 h-5 bg-white text-green-800 rounded-full flex items-center justify-center">
|
|
<FaCheck className="text-sm" />
|
|
</div> */}
|
|
<div>
|
|
<h2 className="text-md font-semibold text-gray-900">Awesome! Your document is sent for signature</h2>
|
|
</div>
|
|
</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{(selectedRecipients.length > 0 && selectedDocuments.length > 0) ?
|
|
<div className="p-3">
|
|
<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.images[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; |