Tech4biz-channel/Channel-Frontend/src/pages/admin/ApprovalsPage.tsx
2026-07-09 15:32:31 +05:30

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;