195 lines
8.7 KiB
TypeScript
195 lines
8.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { CheckCircle, Clock, Search, XCircle } from 'lucide-react';
|
|
import { axiosInstance } from '../../services/axios';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
interface PendingPartner {
|
|
id: string;
|
|
email: string;
|
|
createdAt: string;
|
|
acceptances: Array<{
|
|
id: string;
|
|
signatureHash: string | null;
|
|
documentUrl: string | null;
|
|
document: {
|
|
type: string;
|
|
version: string;
|
|
}
|
|
}>;
|
|
}
|
|
|
|
export const ApprovalsPage: React.FC = () => {
|
|
const [partners, setPartners] = useState<PendingPartner[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [processingId, setProcessingId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchPending();
|
|
}, []);
|
|
|
|
const fetchPending = async () => {
|
|
try {
|
|
const res = await axiosInstance.get('/legal/pending');
|
|
setPartners(res.data);
|
|
} catch (err) {
|
|
console.error('Failed to fetch pending partners', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const approvePartner = async (partnerId: string) => {
|
|
setProcessingId(partnerId);
|
|
try {
|
|
await axiosInstance.post(`/legal/approve/${partnerId}`);
|
|
setPartners(partners.filter(p => p.id !== partnerId));
|
|
} catch (err) {
|
|
console.error('Failed to approve', err);
|
|
} finally {
|
|
setProcessingId(null);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center min-h-[60vh]">
|
|
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto space-y-8 animate-fade-in text-ink-900">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-extrabold text-ink-900 tracking-tight flex items-center gap-3">
|
|
Approvals Queue
|
|
<span className="bg-ink-900 text-ink-0 text-xs px-2.5 py-1 rounded-full font-bold border border-ink-700 shadow-sm">
|
|
{partners.length} Pending
|
|
</span>
|
|
</h1>
|
|
<p className="text-ink-500 text-sm mt-1">
|
|
Review and approve partner legal documents to grant platform access.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Search className="w-4 h-4 text-ink-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search pending partners..."
|
|
className="pl-9 pr-4 py-2 bg-ink-0 border border-ink-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400 w-full sm:w-64"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* List */}
|
|
<div className="bg-ink-0 rounded-2xl border border-ink-200 overflow-hidden shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm whitespace-nowrap">
|
|
<thead className="bg-ink-50 border-b border-ink-200 text-ink-500 font-bold uppercase tracking-wider text-xs">
|
|
<tr>
|
|
<th className="px-6 py-4">Partner</th>
|
|
<th className="px-6 py-4">NDA Status</th>
|
|
<th className="px-6 py-4">MSA Status</th>
|
|
<th className="px-6 py-4 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-ink-200">
|
|
<AnimatePresence>
|
|
{partners.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={4} className="px-6 py-12 text-center">
|
|
<div className="w-12 h-12 rounded-full bg-ink-100 flex items-center justify-center mx-auto mb-4">
|
|
<CheckCircle className="w-6 h-6 text-ink-900" />
|
|
</div>
|
|
<p className="text-ink-900 font-bold">Queue is empty</p>
|
|
<p className="text-ink-500 text-xs mt-1">All partners have been reviewed.</p>
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
partners.map(partner => {
|
|
const nda = partner.acceptances.find(a => a.document.type === 'NDA');
|
|
const msa = partner.acceptances.find(a => a.document.type === 'MSA');
|
|
|
|
return (
|
|
<motion.tr
|
|
key={partner.id}
|
|
initial={{ opacity: 1 }}
|
|
exit={{ opacity: 0, x: -20, backgroundColor: 'rgba(0, 0, 0, 0.02)' }}
|
|
className="hover:bg-ink-50 transition-colors group"
|
|
>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-ink-900 to-ink-800 flex items-center justify-center text-ink-0 font-bold text-xs shadow-md">
|
|
{partner.email.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-ink-900">{partner.email}</p>
|
|
<p className="text-xs text-ink-500 flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{new Date(partner.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
{nda ? (
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle className="w-4 h-4 text-ink-900" />
|
|
<span className="text-xs font-bold text-ink-900 bg-ink-100 border border-ink-200 px-2 py-1 rounded-md">
|
|
{nda.documentUrl ? 'Uploaded PDF' : 'Digital Sign'}
|
|
</span>
|
|
{nda.documentUrl && (
|
|
<a href={nda.documentUrl} target="_blank" rel="noreferrer" className="text-xs font-bold text-ink-600 hover:text-ink-900 underline ml-2">View</a>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-ink-400">
|
|
<XCircle className="w-4 h-4" />
|
|
<span className="text-xs font-bold bg-ink-50 border border-ink-200 text-ink-400 px-2 py-1 rounded-md">Missing</span>
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
{msa ? (
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle className="w-4 h-4 text-ink-900" />
|
|
<span className="text-xs font-bold text-ink-900 bg-ink-100 border border-ink-200 px-2 py-1 rounded-md">
|
|
{msa.documentUrl ? 'Uploaded PDF' : 'Digital Sign'}
|
|
</span>
|
|
{msa.documentUrl && (
|
|
<a href={msa.documentUrl} target="_blank" rel="noreferrer" className="text-xs font-bold text-ink-600 hover:text-ink-900 underline ml-2">View</a>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-ink-400">
|
|
<XCircle className="w-4 h-4" />
|
|
<span className="text-xs font-bold bg-ink-50 border border-ink-200 text-ink-400 px-2 py-1 rounded-md">Missing</span>
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4 text-right">
|
|
<button
|
|
onClick={() => approvePartner(partner.id)}
|
|
disabled={!nda || !msa || processingId === partner.id}
|
|
className="px-4 py-2 bg-ink-900 text-ink-0 text-xs font-bold rounded-lg hover:bg-ink-800 transition-colors shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{processingId === partner.id ? 'Approving...' : 'Approve Access'}
|
|
</button>
|
|
</td>
|
|
</motion.tr>
|
|
);
|
|
})
|
|
)}
|
|
</AnimatePresence>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
export default ApprovalsPage;
|