811 lines
35 KiB
TypeScript
811 lines
35 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import ReactDOM from "react-dom";
|
|
import { motion } from "framer-motion";
|
|
import {
|
|
X,
|
|
ZoomIn,
|
|
ZoomOut,
|
|
RotateCcw,
|
|
Download,
|
|
Printer,
|
|
ShieldCheck,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
FileText,
|
|
Maximize,
|
|
Minimize,
|
|
} from "lucide-react";
|
|
import { Button } from "./Button";
|
|
|
|
interface Acceptance {
|
|
id: string;
|
|
signatureHash: string | null;
|
|
documentUrl: string | null;
|
|
signatureBase64: string | null;
|
|
ipAddress: string;
|
|
acceptedAt: string;
|
|
document: {
|
|
id: string;
|
|
type: string;
|
|
version: string;
|
|
content: string;
|
|
};
|
|
}
|
|
|
|
interface DocumentPreviewModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
partnerId: string;
|
|
partnerEmail: string;
|
|
partnerCreatedAt: string;
|
|
acceptances: Acceptance[];
|
|
verifiedDocs: { nda: boolean; msa: boolean };
|
|
onVerify: (docType: "NDA" | "MSA") => void;
|
|
onApprovePartner: () => void;
|
|
isApproving: boolean;
|
|
}
|
|
|
|
export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
partnerId: _partnerId,
|
|
partnerEmail,
|
|
partnerCreatedAt: _partnerCreatedAt,
|
|
acceptances,
|
|
verifiedDocs,
|
|
onVerify,
|
|
onApprovePartner,
|
|
isApproving,
|
|
}) => {
|
|
const [currentTab, setCurrentTab] = useState<"NDA" | "MSA">("NDA");
|
|
const [zoom, setZoom] = useState(100);
|
|
const [fitWidth, setFitWidth] = useState(false);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
|
|
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const ndaAcceptance = acceptances.find((a) => a.document.type === "NDA");
|
|
const msaAcceptance = acceptances.find((a) => a.document.type === "MSA");
|
|
const activeAcceptance = currentTab === "NDA" ? ndaAcceptance : msaAcceptance;
|
|
const totalPages = activeAcceptance?.documentUrl ? 1 : 2;
|
|
|
|
// Prevent background page scrolling when modal is open
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.body.style.overflow = "hidden";
|
|
} else {
|
|
document.body.style.overflow = "";
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = "";
|
|
};
|
|
}, [isOpen]);
|
|
|
|
// Handle auto-opening on the signature page (Page 2) for digitally signed documents
|
|
useEffect(() => {
|
|
if (isOpen && activeAcceptance) {
|
|
if (!activeAcceptance.documentUrl && activeAcceptance.signatureHash) {
|
|
setCurrentPage(2);
|
|
} else {
|
|
setCurrentPage(1);
|
|
}
|
|
}
|
|
}, [currentTab, isOpen, activeAcceptance]);
|
|
|
|
// Full Screen logic
|
|
const toggleFullScreen = () => {
|
|
if (!modalRef.current) return;
|
|
if (!isFullScreen) {
|
|
if (modalRef.current.requestFullscreen) {
|
|
modalRef.current.requestFullscreen().catch(() => {
|
|
setIsFullScreen(true);
|
|
});
|
|
} else {
|
|
setIsFullScreen(true);
|
|
}
|
|
} else {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen().catch(() => {});
|
|
}
|
|
setIsFullScreen(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleFullscreenChange = () => {
|
|
setIsFullScreen(!!document.fullscreenElement);
|
|
};
|
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
return () =>
|
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
}, []);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleZoomIn = () => {
|
|
setFitWidth(false);
|
|
setZoom((prev) => Math.min(200, prev + 25));
|
|
};
|
|
const handleZoomOut = () => {
|
|
setFitWidth(false);
|
|
setZoom((prev) => Math.max(50, prev - 25));
|
|
};
|
|
const handleZoomReset = () => {
|
|
setFitWidth(false);
|
|
setZoom(100);
|
|
};
|
|
const toggleFitWidth = () => {
|
|
setFitWidth(!fitWidth);
|
|
};
|
|
|
|
const handlePrint = () => {
|
|
const printContent = document.getElementById("printable-doc-content");
|
|
if (!printContent) return;
|
|
|
|
const windowUrl = "about:blank";
|
|
const uniqueName = new Date().getTime();
|
|
const printWindow = window.open(
|
|
windowUrl,
|
|
uniqueName.toString(),
|
|
"left=50000,top=50000,width=0,height=0",
|
|
);
|
|
if (!printWindow) return;
|
|
|
|
printWindow.document.write(`
|
|
<html>
|
|
<head>
|
|
<title>Print Document - ${currentTab}</title>
|
|
<style>
|
|
body { font-family: sans-serif; padding: 40px; color: #1b1b1b; }
|
|
h1 { font-size: 24px; font-weight: bold; margin-bottom: 20px; text-align: center; }
|
|
p { font-size: 14px; line-height: 1.6; white-space: pre-line; }
|
|
.sig-box { margin-top: 40px; padding: 20px; border: 2px dashed #ccc; text-align: center; max-width: 400px; margin-left: auto; margin-right: auto; }
|
|
.sig-title { font-weight: bold; color: #10b981; margin-bottom: 10px; }
|
|
.hash { font-family: monospace; font-size: 11px; word-break: break-all; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${currentTab === "NDA" ? "Non-Disclosure Agreement (NDA)" : "Master Services Agreement (MSA)"}</h1>
|
|
<p>${activeAcceptance?.document.content || ""}</p>
|
|
<div class="sig-box">
|
|
<div class="sig-title">Digitally Signed & Verified</div>
|
|
<div>Signed By: ${partnerEmail}</div>
|
|
<div class="hash">Verification Hash: ${activeAcceptance?.signatureHash || "N/A"}</div>
|
|
<div>IP Address: ${activeAcceptance?.ipAddress || "N/A"}</div>
|
|
<div>Date Signed: ${activeAcceptance ? new Date(activeAcceptance.acceptedAt).toLocaleString() : ""}</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`);
|
|
printWindow.document.close();
|
|
printWindow.focus();
|
|
printWindow.print();
|
|
printWindow.close();
|
|
};
|
|
|
|
const handleDownloadText = () => {
|
|
if (!activeAcceptance) return;
|
|
const element = document.createElement("a");
|
|
const file = new Blob(
|
|
[
|
|
`${currentTab === "NDA" ? "Non-Disclosure Agreement (NDA)" : "Master Services Agreement (MSA)"}\n\n`,
|
|
activeAcceptance.document.content,
|
|
`\n\n=== DIGITAL SIGNATURE ===\n`,
|
|
`Signed By: ${partnerEmail}\n`,
|
|
`Verification Hash: ${activeAcceptance.signatureHash || "N/A"}\n`,
|
|
`IP Address: ${activeAcceptance.ipAddress}\n`,
|
|
`Signed On: ${new Date(activeAcceptance.acceptedAt).toLocaleString()}\n`,
|
|
],
|
|
{ type: "text/plain" },
|
|
);
|
|
element.href = URL.createObjectURL(file);
|
|
element.download = `${currentTab}_Agreement_${partnerEmail.split("@")[0]}.txt`;
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
};
|
|
|
|
const fileHost = (
|
|
import.meta.env.VITE_API_URL || "http://localhost:5000/api/v1"
|
|
).replace("/api/v1", "");
|
|
const isBothVerified = verifiedDocs.nda && verifiedDocs.msa;
|
|
const isCurrentVerified =
|
|
currentTab === "NDA" ? verifiedDocs.nda : verifiedDocs.msa;
|
|
|
|
// Responsive classes based on screen size
|
|
const modalContainerClasses = isFullScreen
|
|
? "fixed inset-0 w-screen h-screen bg-ink-0 z-[9999] flex flex-col overflow-hidden"
|
|
: "relative w-full h-full sm:w-[90vw] sm:h-[88vh] lg:w-[78vw] lg:h-[86vh] bg-ink-0 border border-ink-200 sm:rounded-2xl shadow-2xl flex flex-col overflow-hidden z-[9999]";
|
|
|
|
return ReactDOM.createPortal(
|
|
<div className="fixed inset-0 z-[9999] flex justify-center items-center p-0 sm:p-4 md:p-6 overflow-hidden">
|
|
{/* Full-screen Backdrop overlay */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
className="fixed inset-0 bg-ink-950/60 backdrop-blur-md z-[9998]"
|
|
/>
|
|
|
|
{/* Modal Box */}
|
|
<motion.div
|
|
ref={modalRef}
|
|
initial={{ opacity: 0, scale: 0.97, y: 15 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.97, y: 15 }}
|
|
transition={{ type: "spring", damping: 26, stiffness: 340 }}
|
|
className={modalContainerClasses}
|
|
>
|
|
{/* Sticky Header */}
|
|
<div className="px-5 py-4 border-b border-ink-200 bg-ink-0 flex flex-col md:flex-row md:items-center justify-between gap-3 shrink-0">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-ink-900 to-ink-800 flex items-center justify-center text-ink-0 shrink-0">
|
|
<FileText className="w-5 h-5" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<h3 className="text-sm font-extrabold text-ink-900 tracking-tight truncate">
|
|
{currentTab === "NDA"
|
|
? "Mutual Non-Disclosure Agreement (NDA)"
|
|
: "Master Services Agreement (MSA)"}
|
|
</h3>
|
|
<p className="text-xs text-ink-500 font-semibold truncate">
|
|
Partner: <span className="text-ink-900">{partnerEmail}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Switcher tabs */}
|
|
<div className="flex items-center gap-2 bg-ink-50 p-1 rounded-xl border border-ink-200 self-start md:self-auto">
|
|
<button
|
|
onClick={() => {
|
|
setCurrentTab("NDA");
|
|
setCurrentPage(1);
|
|
}}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
|
currentTab === "NDA"
|
|
? "bg-ink-0 text-ink-900 shadow-sm border border-ink-200"
|
|
: "text-ink-500 hover:text-ink-900"
|
|
}`}
|
|
>
|
|
NDA Agreement {verifiedDocs.nda && "✓"}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setCurrentTab("MSA");
|
|
setCurrentPage(1);
|
|
}}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
|
currentTab === "MSA"
|
|
? "bg-ink-0 text-ink-900 shadow-sm border border-ink-200"
|
|
: "text-ink-500 hover:text-ink-900"
|
|
}`}
|
|
>
|
|
MSA Agreement {verifiedDocs.msa && "✓"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 self-end md:self-auto">
|
|
<button
|
|
onClick={toggleFullScreen}
|
|
className="p-2 rounded-lg text-ink-500 hover:text-ink-900 hover:bg-ink-50 transition-all cursor-pointer"
|
|
title={isFullScreen ? "Exit Fullscreen" : "Fullscreen"}
|
|
>
|
|
{isFullScreen ? (
|
|
<Minimize className="w-4 h-4" />
|
|
) : (
|
|
<Maximize className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg text-ink-400 hover:text-ink-900 hover:bg-ink-50 transition-all cursor-pointer"
|
|
title="Close Modal"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Warning Banner if Unsigned */}
|
|
{!activeAcceptance && (
|
|
<div className="bg-amber-50 border-b border-amber-200 px-5 py-2.5 flex items-center gap-3 shrink-0">
|
|
<AlertTriangle className="w-4.5 h-4.5 text-amber-600 shrink-0" />
|
|
<p className="text-xs font-bold text-amber-800">
|
|
Attention: This document has not been submitted or signed by the
|
|
partner.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Professional Document Viewer Toolbar */}
|
|
<div className="px-5 py-2.5 bg-ink-50 border-b border-ink-200 flex flex-wrap items-center justify-between gap-3 shrink-0">
|
|
{/* Page navigation controls */}
|
|
<div className="flex items-center gap-1.5">
|
|
<button
|
|
disabled={currentPage <= 1 || !activeAcceptance}
|
|
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer shadow-sm"
|
|
title="Previous Page"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
</button>
|
|
<span className="text-xs font-bold text-ink-600 min-w-[70px] text-center">
|
|
Page {activeAcceptance ? currentPage : 0} of{" "}
|
|
{activeAcceptance ? totalPages : 0}
|
|
</span>
|
|
<button
|
|
disabled={currentPage >= totalPages || !activeAcceptance}
|
|
onClick={() =>
|
|
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
|
|
}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer shadow-sm"
|
|
title="Next Page"
|
|
>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Zoom controls */}
|
|
<div className="flex items-center gap-1.5">
|
|
<button
|
|
onClick={handleZoomOut}
|
|
disabled={
|
|
!activeAcceptance || activeAcceptance.documentUrl !== null
|
|
}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer shadow-sm"
|
|
title="Zoom Out"
|
|
>
|
|
<ZoomOut className="w-4 h-4" />
|
|
</button>
|
|
<span className="text-xs font-bold text-ink-600 min-w-[45px] text-center">
|
|
{zoom}%
|
|
</span>
|
|
<button
|
|
onClick={handleZoomIn}
|
|
disabled={
|
|
!activeAcceptance || activeAcceptance.documentUrl !== null
|
|
}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer shadow-sm"
|
|
title="Zoom In"
|
|
>
|
|
<ZoomIn className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={handleZoomReset}
|
|
disabled={
|
|
!activeAcceptance || activeAcceptance.documentUrl !== null
|
|
}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer shadow-sm"
|
|
title="Reset Zoom"
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={toggleFitWidth}
|
|
disabled={
|
|
!activeAcceptance || activeAcceptance.documentUrl !== null
|
|
}
|
|
className={`px-2.5 py-1.5 rounded-lg border text-xs font-bold cursor-pointer shadow-sm transition-all ${
|
|
fitWidth
|
|
? "bg-ink-900 text-ink-0 border-transparent"
|
|
: "bg-ink-0 border-ink-200 text-ink-700 hover:bg-ink-50"
|
|
}`}
|
|
title="Fit Width"
|
|
>
|
|
Fit Width
|
|
</button>
|
|
</div>
|
|
|
|
{/* Download & Print Actions */}
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handlePrint}
|
|
disabled={!activeAcceptance}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer flex items-center gap-1.5 text-xs font-bold shadow-sm"
|
|
title="Print"
|
|
>
|
|
<Printer className="w-4 h-4" />
|
|
<span className="hidden sm:inline">Print</span>
|
|
</button>
|
|
<button
|
|
onClick={
|
|
activeAcceptance?.documentUrl ? undefined : handleDownloadText
|
|
}
|
|
disabled={!activeAcceptance}
|
|
className="p-1.5 rounded-lg border border-ink-200 hover:bg-ink-0 disabled:opacity-40 text-ink-700 bg-ink-0 cursor-pointer flex items-center gap-1.5 text-xs font-bold shadow-sm"
|
|
title="Download File"
|
|
>
|
|
{activeAcceptance?.documentUrl ? (
|
|
<a
|
|
href={`${fileHost}${activeAcceptance.documentUrl}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1.5"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
<span className="hidden sm:inline">Download</span>
|
|
</a>
|
|
) : (
|
|
<>
|
|
<Download className="w-4 h-4" />
|
|
<span className="hidden sm:inline">Download</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal Main Body (Split Pane) */}
|
|
<div className="flex-1 flex flex-col md:flex-row overflow-hidden min-h-0">
|
|
{/* Left Pane: Centered Document Viewer */}
|
|
<div className="flex-1 flex flex-col bg-ink-100 overflow-hidden relative p-4 sm:p-6 justify-center items-center">
|
|
<div
|
|
ref={containerRef}
|
|
className="w-full h-full overflow-y-auto flex justify-center items-start scrollbar-thin rounded-lg"
|
|
>
|
|
{activeAcceptance ? (
|
|
activeAcceptance.documentUrl ? (
|
|
// PDF / Uploaded Document Viewer centered
|
|
<div className="w-full h-full max-w-4xl bg-ink-0 shadow-lg rounded-xl overflow-hidden border border-ink-200 flex justify-center items-center">
|
|
{activeAcceptance.documentUrl
|
|
.toLowerCase()
|
|
.endsWith(".pdf") ? (
|
|
<iframe
|
|
src={`${fileHost}${activeAcceptance.documentUrl}#toolbar=0`}
|
|
title="Document PDF"
|
|
className="w-full h-full border-0 bg-white"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center p-4 bg-ink-50 overflow-auto">
|
|
<img
|
|
src={`${fileHost}${activeAcceptance.documentUrl}`}
|
|
alt="Signed legal document upload"
|
|
className="max-w-full max-h-full object-contain shadow-md rounded border border-ink-200"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
// Digitally Signed text template (Standard A4 letter styled)
|
|
<div
|
|
id="printable-doc-content"
|
|
className="w-full bg-ink-0 p-8 sm:p-12 my-2 rounded-xl shadow-lg border border-ink-200 font-serif leading-relaxed text-ink-800 transition-all duration-150 select-text flex flex-col"
|
|
style={
|
|
fitWidth
|
|
? { width: "100%", maxWidth: "100%", fontSize: "15px" }
|
|
: {
|
|
width: "100%",
|
|
maxWidth: "720px",
|
|
fontSize: `${14 * (zoom / 100)}px`,
|
|
}
|
|
}
|
|
>
|
|
{currentPage === 1 ? (
|
|
// Page 1: Agreement text
|
|
<div className="flex-1">
|
|
<div className="text-center mb-8 not-italic font-sans">
|
|
<h4 className="text-base sm:text-lg font-extrabold text-ink-900 tracking-tight">
|
|
{currentTab === "NDA"
|
|
? "MUTUAL NON-DISCLOSURE AGREEMENT"
|
|
: "MASTER SERVICES AGREEMENT"}
|
|
</h4>
|
|
<div className="w-16 h-1 bg-ink-900 mx-auto my-3" />
|
|
<p className="text-[10px] text-ink-400 font-bold uppercase tracking-wider">
|
|
Version {activeAcceptance.document.version}
|
|
</p>
|
|
</div>
|
|
<div className="whitespace-pre-line prose max-w-none text-xs sm:text-sm">
|
|
{activeAcceptance.document.content}
|
|
</div>
|
|
<div className="mt-8 pt-4 border-t border-ink-150 text-center not-italic font-sans text-xs text-ink-400 font-semibold">
|
|
--- Page 1 of 2 (Standard Terms) ---
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// Page 2: Actual signature rendering + metadata
|
|
<div className="flex-1 flex flex-col justify-between">
|
|
<div>
|
|
<div className="text-center mb-6 not-italic font-sans">
|
|
<h4 className="text-base sm:text-lg font-extrabold text-ink-900 tracking-tight">
|
|
SIGNATURE & ACCORD SIGNING PAGE
|
|
</h4>
|
|
<p className="text-[10px] text-ink-400 font-bold uppercase tracking-wider mt-1">
|
|
Agreement Version{" "}
|
|
{activeAcceptance.document.version}
|
|
</p>
|
|
</div>
|
|
|
|
<p className="text-xs sm:text-sm mb-6 text-ink-600 font-sans italic">
|
|
IN WITNESS WHEREOF, the parties hereto have caused
|
|
this Agreement to be executed by their digital
|
|
signatures as of the Acceptance Date specified
|
|
below.
|
|
</p>
|
|
</div>
|
|
|
|
{/* ── Actual Signature Rendering ── */}
|
|
<div className="my-auto max-w-lg mx-auto w-full space-y-4">
|
|
{/* Signature image box */}
|
|
<div className="border-2 border-ink-200 rounded-xl overflow-hidden bg-white shadow-sm">
|
|
<div className="px-4 pt-3 pb-1 border-b border-ink-100 flex items-center justify-between">
|
|
<span className="text-[10px] font-bold text-ink-400 uppercase tracking-widest">
|
|
Client Signature
|
|
</span>
|
|
<div className="flex items-center gap-1 text-emerald-600">
|
|
<ShieldCheck className="w-3.5 h-3.5" />
|
|
<span className="text-[10px] font-bold uppercase tracking-wider">
|
|
Verified
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 min-h-[100px] flex items-center justify-center bg-white">
|
|
{activeAcceptance.signatureBase64 ? (
|
|
// Render the exact drawn signature as-is — no modification
|
|
<img
|
|
src={activeAcceptance.signatureBase64}
|
|
alt="Client drawn signature"
|
|
className="max-w-full max-h-[160px] object-contain"
|
|
style={{ imageRendering: "crisp-edges" }}
|
|
/>
|
|
) : (
|
|
// Upload-based signing — no drawn image stored
|
|
<div className="text-center py-4">
|
|
<ShieldCheck className="w-8 h-8 text-emerald-500 mx-auto mb-2" />
|
|
<p className="text-xs text-ink-500 font-semibold">
|
|
Signed via document upload
|
|
</p>
|
|
<p className="text-[10px] text-ink-400 mt-0.5">
|
|
See uploaded file in viewer
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Signature baseline line */}
|
|
<div className="px-6 pb-3">
|
|
<div className="border-b-2 border-ink-300 w-full" />
|
|
<p className="text-[9px] text-ink-400 font-bold uppercase tracking-widest mt-1 text-center">
|
|
Authorized Digital Signature
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Verification metadata strip */}
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px] sm:text-xs font-sans bg-ink-50 border border-ink-200 rounded-xl p-4">
|
|
<div>
|
|
<p className="font-bold text-ink-400 uppercase tracking-wider text-[9px]">
|
|
Signed By
|
|
</p>
|
|
<p className="font-bold text-ink-900 mt-0.5 truncate">
|
|
{partnerEmail}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-ink-400 uppercase tracking-wider text-[9px]">
|
|
Accepted On
|
|
</p>
|
|
<p className="font-semibold text-ink-900 mt-0.5">
|
|
{new Date(
|
|
activeAcceptance.acceptedAt,
|
|
).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-ink-400 uppercase tracking-wider text-[9px]">
|
|
IP Address
|
|
</p>
|
|
<p className="font-semibold font-mono text-ink-900 mt-0.5">
|
|
{activeAcceptance.ipAddress}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-ink-400 uppercase tracking-wider text-[9px]">
|
|
Document Version
|
|
</p>
|
|
<p className="font-semibold text-ink-900 mt-0.5">
|
|
v{activeAcceptance.document.version}
|
|
</p>
|
|
</div>
|
|
{activeAcceptance.signatureHash && (
|
|
<div className="col-span-2">
|
|
<p className="font-bold text-ink-400 uppercase tracking-wider text-[9px]">
|
|
Verification Hash (SHA-256)
|
|
</p>
|
|
<p className="font-mono text-[9px] text-ink-500 bg-ink-100 p-1.5 rounded border border-ink-200 break-all mt-0.5">
|
|
{activeAcceptance.signatureHash}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 pt-4 border-t border-ink-150 text-center not-italic font-sans text-xs text-ink-400 font-semibold">
|
|
--- Page 2 of 2 (Signature & Seals) ---
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
) : (
|
|
<div className="w-full max-w-md my-auto flex flex-col items-center justify-center text-center p-8 bg-ink-0 rounded-2xl border border-ink-200 shadow-lg">
|
|
<AlertTriangle className="w-12 h-12 text-ink-400 mb-3" />
|
|
<h4 className="text-base font-extrabold text-ink-900">
|
|
Document Unavailable
|
|
</h4>
|
|
<p className="text-xs text-ink-500 mt-1.5 max-w-xs leading-relaxed font-semibold">
|
|
The partner has not yet submitted or signed the {currentTab}{" "}
|
|
document.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Pane: Document Details & Metadata */}
|
|
<div className="w-full md:w-80 border-t md:border-t-0 md:border-l border-ink-200 bg-ink-0 flex flex-col overflow-y-auto shrink-0 p-5 space-y-5">
|
|
<div>
|
|
<h4 className="text-xs font-bold uppercase tracking-wider text-ink-400 mb-3">
|
|
Onboarding Details
|
|
</h4>
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="font-semibold text-ink-500">
|
|
Document Category
|
|
</span>
|
|
<span className="font-bold text-ink-900 bg-ink-100 px-2 py-0.5 rounded border border-ink-200">
|
|
{currentTab}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="font-semibold text-ink-500">
|
|
Submission State
|
|
</span>
|
|
{activeAcceptance ? (
|
|
<span className="font-bold text-emerald-700 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
|
Signed
|
|
</span>
|
|
) : (
|
|
<span className="font-bold text-amber-700 bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
|
|
Pending
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex justify-between items-center text-xs">
|
|
<span className="font-semibold text-ink-500">
|
|
Digital Signature
|
|
</span>
|
|
{activeAcceptance ? (
|
|
<span className="font-bold text-emerald-700 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
|
Verified
|
|
</span>
|
|
) : (
|
|
<span className="font-bold text-red-650 bg-red-500/10 px-2 py-0.5 rounded border border-red-500/20">
|
|
Not Verified
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-ink-100 pt-4">
|
|
<h4 className="text-xs font-bold uppercase tracking-wider text-ink-400 mb-3">
|
|
Legal Verification
|
|
</h4>
|
|
{activeAcceptance ? (
|
|
<div className="space-y-3 text-xs">
|
|
<div>
|
|
<p className="font-semibold text-ink-400 text-[10px] uppercase">
|
|
Signed By
|
|
</p>
|
|
<p className="font-bold text-ink-900 truncate mt-0.5">
|
|
{partnerEmail}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-ink-400 text-[10px] uppercase">
|
|
Signed On
|
|
</p>
|
|
<p className="font-bold text-ink-900 mt-0.5">
|
|
{new Date(activeAcceptance.acceptedAt).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-ink-400 text-[10px] uppercase">
|
|
IP Address
|
|
</p>
|
|
<p className="font-bold text-ink-900 mt-0.5 font-mono">
|
|
{activeAcceptance.ipAddress}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-ink-400 text-[10px] uppercase">
|
|
Version
|
|
</p>
|
|
<p className="font-bold text-ink-900 mt-0.5">
|
|
v{activeAcceptance.document.version}
|
|
</p>
|
|
</div>
|
|
{activeAcceptance.signatureHash && (
|
|
<div>
|
|
<p className="font-semibold text-ink-400 text-[10px] uppercase">
|
|
Verification Hash
|
|
</p>
|
|
<p className="font-mono text-[10px] text-ink-650 bg-ink-50 p-1.5 rounded border border-ink-150 break-all mt-0.5">
|
|
{activeAcceptance.signatureHash}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-ink-400 italic">
|
|
No verification metadata available.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-auto pt-4 border-t border-ink-100 space-y-2">
|
|
<Button
|
|
onClick={() => onVerify(currentTab)}
|
|
disabled={!activeAcceptance}
|
|
variant={isCurrentVerified ? "secondary" : "primary"}
|
|
className="w-full flex justify-center items-center gap-1.5"
|
|
>
|
|
{isCurrentVerified ? (
|
|
<>
|
|
<div className="flex gap-2 items-center">
|
|
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
|
<span>Document Verified</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<span>Verify {currentTab} Signature</span>
|
|
)}
|
|
</Button>
|
|
<p className="text-[10px] text-ink-400 text-center font-medium leading-normal">
|
|
Marking this verified unlocks approval options for the
|
|
administrator.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sticky Footer */}
|
|
<div className="px-6 py-4 border-t border-ink-200 bg-ink-50 flex flex-col sm:flex-row justify-between items-center gap-4 shrink-0">
|
|
<div className="text-xs font-semibold text-ink-500">
|
|
{verifiedDocs.nda && verifiedDocs.msa ? (
|
|
<span className="text-emerald-700 font-bold flex items-center gap-1.5">
|
|
<CheckCircle className="w-4.5 h-4.5" /> Both agreements
|
|
verified. Access approval unlocked.
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1.5">
|
|
<AlertTriangle className="w-4 h-4 text-amber-500" /> Please
|
|
review and verify both the NDA and MSA documents.
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
|
<Button variant="secondary" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={onApprovePartner}
|
|
disabled={!isBothVerified || isApproving}
|
|
variant="primary"
|
|
className="font-bold shadow-md hover:shadow-lg transition-shadow"
|
|
>
|
|
{isApproving ? "Approving Partner..." : "Approve Partner Access"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
};
|