854 lines
36 KiB
TypeScript
854 lines
36 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { formatCurrency, formatCompactCurrency } from '@/lib/utils';
|
||
import {
|
||
TrendingUp,
|
||
DollarSign,
|
||
Percent,
|
||
Calendar,
|
||
Plus,
|
||
Loader2,
|
||
AlertCircle,
|
||
CheckCircle,
|
||
Zap,
|
||
ChevronDown,
|
||
ChevronUp,
|
||
} from 'lucide-react';
|
||
|
||
interface DebtFacility {
|
||
id: string;
|
||
name: string;
|
||
lender: string;
|
||
type: 'Revolving Credit' | 'Term Loan' | 'Line of Credit' | 'Equipment Financing' | 'Other';
|
||
commitmentAmount: number;
|
||
drawnAmount: number;
|
||
availableAmount: number;
|
||
interestRate: string;
|
||
maturityDate: string;
|
||
status: 'Active' | 'Inactive' | 'Closed';
|
||
draws: DebtDraw[];
|
||
}
|
||
|
||
interface DebtDraw {
|
||
id: string;
|
||
facilityId: string;
|
||
drawDate: string;
|
||
amount: number;
|
||
repaymentDate: string | null;
|
||
status: 'Active' | 'Repaid' | 'Partial';
|
||
interestRate: string;
|
||
}
|
||
|
||
interface Covenant {
|
||
id: string;
|
||
name: string;
|
||
type: string;
|
||
metric: string;
|
||
threshold: string | number;
|
||
currentValue: string | number;
|
||
status: 'Compliant' | 'Warning' | 'Breach';
|
||
lastChecked: string;
|
||
headroom: string;
|
||
}
|
||
|
||
interface NewFacilityForm {
|
||
name: string;
|
||
lender: string;
|
||
type: 'Revolving Credit' | 'Term Loan' | 'Line of Credit' | 'Equipment Financing' | 'Other';
|
||
commitmentAmount: string;
|
||
interestRate: string;
|
||
maturityDate: string;
|
||
}
|
||
|
||
interface NewDrawForm {
|
||
facilityId: string;
|
||
amount: string;
|
||
date: string;
|
||
interestRate: string;
|
||
}
|
||
|
||
interface Toast {
|
||
id: string;
|
||
message: string;
|
||
type: 'success' | 'error' | 'info';
|
||
}
|
||
|
||
export default function CapitalMarketsPage() {
|
||
const [loading, setLoading] = useState(true);
|
||
const [activeTab, setActiveTab] = useState('facilities');
|
||
const [showNewFacility, setShowNewFacility] = useState(false);
|
||
const [showRecordDraw, setShowRecordDraw] = useState(false);
|
||
const [expandedFacilityId, setExpandedFacilityId] = useState<string | null>(null);
|
||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||
|
||
const [facilities, setFacilities] = useState<DebtFacility[]>([]);
|
||
const [draws, setDraws] = useState<DebtDraw[]>([]);
|
||
const [covenants, setCovenants] = useState<Covenant[]>([]);
|
||
|
||
const [newFacilityForm, setNewFacilityForm] = useState<NewFacilityForm>({
|
||
name: '',
|
||
lender: '',
|
||
type: 'Revolving Credit',
|
||
commitmentAmount: '',
|
||
interestRate: '',
|
||
maturityDate: '',
|
||
});
|
||
|
||
const [newDrawForm, setNewDrawForm] = useState<NewDrawForm>({
|
||
facilityId: '',
|
||
amount: '',
|
||
date: new Date().toISOString().split('T')[0],
|
||
interestRate: '',
|
||
});
|
||
|
||
const [summary, setSummary] = useState({
|
||
totalDebtOutstanding: 0,
|
||
totalAvailable: 0,
|
||
weightedAvgRate: 0,
|
||
nextMaturityDate: null as string | null,
|
||
covenantStatus: 'All Clear',
|
||
});
|
||
|
||
// Toast helper
|
||
const addToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||
const id = Math.random().toString(36).substring(7);
|
||
setToasts((prev) => [...prev, { id, message, type }]);
|
||
setTimeout(() => {
|
||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||
}, 4000);
|
||
};
|
||
|
||
// Fetch data from API
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const res = await fetch('/api/capital-markets');
|
||
if (!res.ok) throw new Error('Failed to fetch capital markets data');
|
||
|
||
const data = await res.json();
|
||
setFacilities(data.facilities);
|
||
setDraws(data.draws);
|
||
setCovenants(data.covenants);
|
||
|
||
setSummary({
|
||
totalDebtOutstanding: data.summary?.totalOutstanding ?? 0,
|
||
totalAvailable: data.summary?.totalAvailable ?? 0,
|
||
weightedAvgRate: data.summary?.weightedAvgRate ?? 0,
|
||
nextMaturityDate: data.summary?.nextMaturityDate ?? '',
|
||
covenantStatus: data.summary?.covenantStatus ?? 'Unknown',
|
||
});
|
||
} catch (err) {
|
||
console.error('Error fetching capital markets data:', err);
|
||
addToast('Failed to fetch capital markets data', 'error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchData();
|
||
}, []);
|
||
|
||
const handleAddFacility = async () => {
|
||
if (!newFacilityForm.name || !newFacilityForm.lender || !newFacilityForm.commitmentAmount || !newFacilityForm.interestRate || !newFacilityForm.maturityDate) {
|
||
addToast('Please fill in all required fields', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
const res = await fetch('/api/capital-markets', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
action: 'create-facility',
|
||
facility: newFacilityForm,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const error = await res.json();
|
||
throw new Error(error.error || 'Failed to create facility');
|
||
}
|
||
|
||
// Reset form and close modal
|
||
setNewFacilityForm({
|
||
name: '',
|
||
lender: '',
|
||
type: 'Revolving Credit',
|
||
commitmentAmount: '',
|
||
interestRate: '',
|
||
maturityDate: '',
|
||
});
|
||
setShowNewFacility(false);
|
||
addToast('Facility created successfully', 'success');
|
||
|
||
// Refetch data to keep in sync
|
||
const refreshRes = await fetch('/api/capital-markets');
|
||
if (refreshRes.ok) {
|
||
const refreshData = await refreshRes.json();
|
||
setFacilities(refreshData.facilities);
|
||
setDraws(refreshData.draws);
|
||
setCovenants(refreshData.covenants);
|
||
setSummary({
|
||
totalDebtOutstanding: refreshData.summary.totalOutstanding,
|
||
totalAvailable: refreshData.summary.totalAvailable,
|
||
weightedAvgRate: refreshData.summary.weightedAvgRate,
|
||
nextMaturityDate: refreshData.summary.nextMaturityDate,
|
||
covenantStatus: refreshData.summary.covenantStatus,
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Error adding facility:', err);
|
||
addToast(err instanceof Error ? err.message : 'Failed to create facility', 'error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleRecordDraw = async () => {
|
||
if (!newDrawForm.facilityId || !newDrawForm.amount) {
|
||
addToast('Please select a facility and enter an amount', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
const res = await fetch('/api/capital-markets', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
action: 'record-draw',
|
||
draw: newDrawForm,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const error = await res.json();
|
||
throw new Error(error.error || 'Failed to record draw');
|
||
}
|
||
|
||
setNewDrawForm({
|
||
facilityId: '',
|
||
amount: '',
|
||
date: new Date().toISOString().split('T')[0],
|
||
interestRate: '',
|
||
});
|
||
setShowRecordDraw(false);
|
||
addToast('Draw recorded successfully', 'success');
|
||
|
||
// Refetch data to keep in sync
|
||
const refreshRes = await fetch('/api/capital-markets');
|
||
if (refreshRes.ok) {
|
||
const refreshData = await refreshRes.json();
|
||
setFacilities(refreshData.facilities);
|
||
setDraws(refreshData.draws);
|
||
setCovenants(refreshData.covenants);
|
||
setSummary({
|
||
totalDebtOutstanding: refreshData.summary.totalOutstanding,
|
||
totalAvailable: refreshData.summary.totalAvailable,
|
||
weightedAvgRate: refreshData.summary.weightedAvgRate,
|
||
nextMaturityDate: refreshData.summary.nextMaturityDate,
|
||
covenantStatus: refreshData.summary.covenantStatus,
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Error recording draw:', err);
|
||
addToast(err instanceof Error ? err.message : 'Failed to record draw', 'error');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
function formatDate(dateStr: string) {
|
||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'short',
|
||
day: 'numeric',
|
||
});
|
||
}
|
||
|
||
const getCovenanStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'Compliant':
|
||
return 'bg-green-900/25 text-green-800 border-green-200';
|
||
case 'Warning':
|
||
return 'bg-yellow-900/25 text-yellow-400 border-yellow-500/25';
|
||
case 'Breach':
|
||
return 'bg-red-900/25 text-red-800 border-red-200';
|
||
default:
|
||
return 'bg-[#1A1A1F] text-[#F0F0F3] border-[#2A2A32]';
|
||
}
|
||
};
|
||
|
||
const getCovenanStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'Compliant':
|
||
return <CheckCircle size={16} />;
|
||
case 'Warning':
|
||
return <AlertCircle size={16} />;
|
||
case 'Breach':
|
||
return <AlertCircle size={16} />;
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const getCovenantSummaryBadge = () => {
|
||
const status = summary.covenantStatus;
|
||
if (status === 'In Breach') {
|
||
return <span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-red-900/25 text-red-800 border border-red-200">In Breach</span>;
|
||
} else if (status === 'At Risk') {
|
||
return <span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-yellow-900/25 text-yellow-400 border border-yellow-500/25">At Risk</span>;
|
||
} else {
|
||
return <span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-green-900/25 text-green-800 border border-green-200">All Clear</span>;
|
||
}
|
||
};
|
||
|
||
if (loading && facilities.length === 0) {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen">
|
||
<Loader2 className="animate-spin" size={32} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#111114] p-8">
|
||
{/* Toast Notifications */}
|
||
<div className="fixed top-4 right-4 z-50 space-y-2">
|
||
{toasts.map((toast) => (
|
||
<div
|
||
key={toast.id}
|
||
className={`px-4 py-3 rounded-lg text-sm font-medium shadow-lg ${
|
||
toast.type === 'success'
|
||
? 'bg-green-500 text-white'
|
||
: toast.type === 'error'
|
||
? 'bg-red-500 text-white'
|
||
: 'bg-blue-500 text-white'
|
||
}`}
|
||
>
|
||
{toast.message}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="max-w-7xl mx-auto">
|
||
{/* Header */}
|
||
<div className="flex justify-between items-start mb-8">
|
||
<div>
|
||
<h1 className="text-4xl font-bold text-[#F0F0F3] mb-2">Capital Markets</h1>
|
||
<p className="text-lg text-[#8B8B9E]">Debt Management, Credit Facilities & Covenant Compliance</p>
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<button
|
||
onClick={() => setShowNewFacility(true)}
|
||
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||
>
|
||
<Plus size={18} />
|
||
New Facility
|
||
</button>
|
||
<button
|
||
onClick={() => setShowRecordDraw(true)}
|
||
className="flex items-center gap-2 bg-green-600 text-white px-6 py-2.5 rounded-lg hover:bg-green-700 transition-colors font-medium"
|
||
>
|
||
<DollarSign size={18} />
|
||
Record Draw
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Summary Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6" title={formatCurrency(summary.totalDebtOutstanding)}>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="bg-blue-900/25 p-3 rounded-lg">
|
||
<TrendingUp className="w-6 h-6 text-blue-600" />
|
||
</div>
|
||
</div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Total Debt Outstanding</p>
|
||
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">{formatCompactCurrency(summary.totalDebtOutstanding)}</p>
|
||
<p className="text-xs text-[#8B8B9E] mt-1">Across {facilities.length} facilities</p>
|
||
</div>
|
||
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6" title={formatCurrency(summary.totalAvailable)}>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="bg-green-900/25 p-3 rounded-lg">
|
||
<DollarSign className="w-6 h-6 text-green-600" />
|
||
</div>
|
||
</div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Available Credit</p>
|
||
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">{formatCompactCurrency(summary.totalAvailable)}</p>
|
||
<p className="text-xs text-[#8B8B9E] mt-1">Ready to draw</p>
|
||
</div>
|
||
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="bg-orange-900/25 p-3 rounded-lg">
|
||
<Percent className="w-6 h-6 text-orange-600" />
|
||
</div>
|
||
</div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Weighted Avg Rate</p>
|
||
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">{summary.weightedAvgRate.toFixed(2)}%</p>
|
||
<p className="text-xs text-[#8B8B9E] mt-1">All active facilities</p>
|
||
</div>
|
||
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="bg-purple-900/25 p-3 rounded-lg">
|
||
<Calendar className="w-6 h-6 text-purple-600" />
|
||
</div>
|
||
</div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Next Maturity</p>
|
||
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">
|
||
{summary.nextMaturityDate ? formatDate(summary.nextMaturityDate) : 'N/A'}
|
||
</p>
|
||
<p className="text-xs text-[#8B8B9E] mt-1">Upcoming refinancing</p>
|
||
</div>
|
||
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="bg-indigo-900/15 p-3 rounded-lg">
|
||
<Zap className="w-6 h-6 text-indigo-600" />
|
||
</div>
|
||
</div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Covenant Status</p>
|
||
<div className="mt-2">
|
||
{getCovenantSummaryBadge()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* New Facility Form */}
|
||
{showNewFacility && (
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6 mb-8">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold text-[#F0F0F3]">New Debt Facility</h3>
|
||
<button
|
||
onClick={() => setShowNewFacility(false)}
|
||
className="text-[#5A5A6E] hover:text-[#8B8B9E] text-2xl font-light"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Facility Name *</label>
|
||
<input
|
||
type="text"
|
||
value={newFacilityForm.name}
|
||
onChange={(e) => setNewFacilityForm({ ...newFacilityForm, name: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="e.g., Tech Bank Credit Line"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Lender *</label>
|
||
<input
|
||
type="text"
|
||
value={newFacilityForm.lender}
|
||
onChange={(e) => setNewFacilityForm({ ...newFacilityForm, lender: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="e.g., JP Morgan Chase"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Type *</label>
|
||
<select
|
||
value={newFacilityForm.type}
|
||
onChange={(e) => setNewFacilityForm({ ...newFacilityForm, type: e.target.value as any })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="Revolving Credit">Revolving Credit</option>
|
||
<option value="Term Loan">Term Loan</option>
|
||
<option value="Equipment Financing">Equipment Financing</option>
|
||
<option value="Line of Credit">Line of Credit</option>
|
||
<option value="Other">Other</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Commitment Amount ($) *</label>
|
||
<input
|
||
type="number"
|
||
value={newFacilityForm.commitmentAmount}
|
||
onChange={(e) => setNewFacilityForm({ ...newFacilityForm, commitmentAmount: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="0"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Interest Rate *</label>
|
||
<input
|
||
type="text"
|
||
value={newFacilityForm.interestRate}
|
||
onChange={(e) => setNewFacilityForm({ ...newFacilityForm, interestRate: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="e.g., 5.5% or SOFR+200bps"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Maturity Date *</label>
|
||
<input
|
||
type="date"
|
||
value={newFacilityForm.maturityDate}
|
||
onChange={(e) => setNewFacilityForm({ ...newFacilityForm, maturityDate: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-3 justify-end">
|
||
<button
|
||
onClick={() => setShowNewFacility(false)}
|
||
className="px-4 py-2 text-[#8B8B9E] border border-[#3A3A45] rounded-lg hover:bg-[#111114] transition-colors font-medium"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleAddFacility}
|
||
disabled={loading}
|
||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{loading ? 'Saving...' : 'Save Facility'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Record Draw Form */}
|
||
{showRecordDraw && (
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] p-6 mb-8">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-lg font-semibold text-[#F0F0F3]">Record Draw</h3>
|
||
<button
|
||
onClick={() => setShowRecordDraw(false)}
|
||
className="text-[#5A5A6E] hover:text-[#8B8B9E] text-2xl font-light"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Facility *</label>
|
||
<select
|
||
value={newDrawForm.facilityId}
|
||
onChange={(e) => {
|
||
const facilityId = e.target.value;
|
||
const facility = facilities.find((f) => f.id === facilityId);
|
||
setNewDrawForm({
|
||
...newDrawForm,
|
||
facilityId,
|
||
interestRate: facility?.interestRate || '',
|
||
});
|
||
}}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="">Select a facility...</option>
|
||
{facilities.map((f) => (
|
||
<option key={f.id} value={f.id}>
|
||
{f.name} - Available: {formatCurrency(f.availableAmount)}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Draw Amount ($) *</label>
|
||
<input
|
||
type="number"
|
||
value={newDrawForm.amount}
|
||
onChange={(e) => setNewDrawForm({ ...newDrawForm, amount: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
placeholder="0"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#8B8B9E] mb-1">Draw Date *</label>
|
||
<input
|
||
type="date"
|
||
value={newDrawForm.date}
|
||
onChange={(e) => setNewDrawForm({ ...newDrawForm, date: e.target.value })}
|
||
className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-3 justify-end">
|
||
<button
|
||
onClick={() => setShowRecordDraw(false)}
|
||
className="px-4 py-2 text-[#8B8B9E] border border-[#3A3A45] rounded-lg hover:bg-[#111114] transition-colors font-medium"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleRecordDraw}
|
||
disabled={loading}
|
||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{loading ? 'Recording...' : 'Record Draw'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tabs */}
|
||
<div className="bg-[#1A1A1F] rounded-lg border border-[#2A2A32] overflow-hidden">
|
||
<div className="border-b border-[#2A2A32]">
|
||
<div className="flex">
|
||
{[
|
||
{ id: 'facilities', label: 'Debt Facilities' },
|
||
{ id: 'draws', label: 'Active Draws' },
|
||
{ id: 'covenants', label: 'Covenant Compliance' },
|
||
].map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`px-6 py-4 font-medium whitespace-nowrap transition-colors ${
|
||
activeTab === tab.id
|
||
? 'text-blue-600 border-b-2 border-blue-600'
|
||
: 'text-[#8B8B9E] hover:text-[#F0F0F3]'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab Content */}
|
||
<div className="p-8">
|
||
{/* Facilities Tab */}
|
||
{activeTab === 'facilities' && (
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-[#F0F0F3] mb-6">Debt Facilities</h2>
|
||
<div className="space-y-4">
|
||
{facilities.map((facility) => (
|
||
<div
|
||
key={facility.id}
|
||
className="border border-[#2A2A32] rounded-lg overflow-hidden hover:shadow-md transition-shadow"
|
||
>
|
||
<div
|
||
className="bg-[#111114] p-4 cursor-pointer hover:bg-[#1A1A1F] transition-colors"
|
||
onClick={() =>
|
||
setExpandedFacilityId(
|
||
expandedFacilityId === facility.id ? null : facility.id
|
||
)
|
||
}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
{expandedFacilityId === facility.id ? (
|
||
<ChevronUp size={20} className="text-[#5A5A6E]" />
|
||
) : (
|
||
<ChevronDown size={20} className="text-[#5A5A6E]" />
|
||
)}
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-semibold text-[#F0F0F3]">{facility.name}</h3>
|
||
<p className="text-sm text-[#8B8B9E]">{facility.lender} • {facility.type}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-8 text-right">
|
||
<div>
|
||
<p className="text-xs text-[#8B8B9E]">Drawn Amount</p>
|
||
<p className="text-lg font-bold text-[#F0F0F3]">
|
||
{formatCurrency(facility.drawnAmount)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[#8B8B9E]">Available</p>
|
||
<p className="text-lg font-bold text-green-600">
|
||
{formatCurrency(facility.availableAmount)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[#8B8B9E]">Rate</p>
|
||
<p className="text-lg font-bold text-[#F0F0F3]">{facility.interestRate}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[#8B8B9E]">Maturity</p>
|
||
<p className="text-lg font-bold text-[#F0F0F3]">{formatDate(facility.maturityDate)}</p>
|
||
</div>
|
||
<div>
|
||
<span className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-green-900/25 text-green-800">
|
||
{facility.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Expanded Details */}
|
||
{expandedFacilityId === facility.id && (
|
||
<div className="border-t border-[#2A2A32] p-4 bg-[#1A1A1F]">
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||
<div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Commitment Amount</p>
|
||
<p className="text-2xl font-bold text-[#F0F0F3] mt-1">
|
||
{formatCurrency(facility.commitmentAmount)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Drawn</p>
|
||
<p className="text-2xl font-bold text-[#F0F0F3] mt-1">
|
||
{((facility.drawnAmount / facility.commitmentAmount) * 100).toFixed(1)}%
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-medium text-[#8B8B9E]">Available Capacity</p>
|
||
<p className="text-2xl font-bold text-green-600 mt-1">
|
||
{((facility.availableAmount / facility.commitmentAmount) * 100).toFixed(1)}%
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{facility.draws.length > 0 && (
|
||
<div>
|
||
<h4 className="text-sm font-semibold text-[#F0F0F3] mb-3">Draws on This Facility</h4>
|
||
<div className="space-y-2">
|
||
{facility.draws.map((draw) => (
|
||
<div
|
||
key={draw.id}
|
||
className="flex items-center justify-between p-3 bg-[#111114] rounded border border-[#2A2A32]"
|
||
>
|
||
<div>
|
||
<p className="text-sm font-medium text-[#F0F0F3]">{formatDate(draw.drawDate)}</p>
|
||
<p className="text-xs text-[#8B8B9E]">{draw.interestRate}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm font-bold text-[#F0F0F3]">{formatCurrency(draw.amount)}</p>
|
||
<span
|
||
className={`inline-block text-xs font-semibold px-2 py-1 rounded mt-1 ${
|
||
draw.status === 'Active'
|
||
? 'bg-blue-900/25 text-blue-800'
|
||
: draw.status === 'Repaid'
|
||
? 'bg-green-900/25 text-green-800'
|
||
: 'bg-orange-900/25 text-orange-800'
|
||
}`}
|
||
>
|
||
{draw.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Draws Tab */}
|
||
{activeTab === 'draws' && (
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-[#F0F0F3] mb-6">Active Draws</h2>
|
||
{draws.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<p className="text-[#8B8B9E]">No active draws. Record your first draw using the button at the top.</p>
|
||
</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-[#3A3A45] bg-[#111114]">
|
||
<th className="px-4 py-3 text-left font-semibold text-[#8B8B9E]">Facility</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-[#8B8B9E]">Draw Date</th>
|
||
<th className="px-4 py-3 text-right font-semibold text-[#8B8B9E]">Amount</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-[#8B8B9E]">Interest Rate</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-[#8B8B9E]">Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-[#2A2A32]">
|
||
{draws.map((draw) => {
|
||
const facility = facilities.find((f) => f.id === draw.facilityId);
|
||
return (
|
||
<tr key={draw.id} className="hover:bg-[#111114] transition-colors">
|
||
<td className="px-4 py-3 font-medium text-[#F0F0F3]">{facility?.name}</td>
|
||
<td className="px-4 py-3 text-[#8B8B9E]">{formatDate(draw.drawDate)}</td>
|
||
<td className="px-4 py-3 text-right font-medium">{formatCurrency(draw.amount)}</td>
|
||
<td className="px-4 py-3 text-[#8B8B9E]">{draw.interestRate}</td>
|
||
<td className="px-4 py-3">
|
||
<span
|
||
className={`inline-block px-3 py-1 rounded text-xs font-semibold ${
|
||
draw.status === 'Active'
|
||
? 'bg-blue-900/25 text-blue-800'
|
||
: draw.status === 'Repaid'
|
||
? 'bg-green-900/25 text-green-800'
|
||
: 'bg-orange-900/25 text-orange-800'
|
||
}`}
|
||
>
|
||
{draw.status}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Covenants Tab */}
|
||
{activeTab === 'covenants' && (
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-[#F0F0F3] mb-6">Covenant Compliance Dashboard</h2>
|
||
<div className="space-y-4">
|
||
{covenants.map((covenant) => (
|
||
<div
|
||
key={covenant.id}
|
||
className={`border rounded-lg p-4 hover:shadow-md transition-shadow ${getCovenanStatusColor(
|
||
covenant.status
|
||
)}`}
|
||
>
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-1">
|
||
{getCovenanStatusIcon(covenant.status)}
|
||
<p className="text-sm font-semibold text-[#F0F0F3]">{covenant.name}</p>
|
||
</div>
|
||
<p className="text-xs text-[#8B8B9E] mt-1">{covenant.type} Covenant</p>
|
||
</div>
|
||
<span
|
||
className={`px-3 py-1 rounded-full text-xs font-semibold flex items-center gap-1 ${getCovenanStatusColor(
|
||
covenant.status
|
||
)}`}
|
||
>
|
||
{covenant.status}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||
<div>
|
||
<p className="text-[#8B8B9E] font-medium">Metric</p>
|
||
<p className="font-semibold text-[#F0F0F3] mt-1">{covenant.metric}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-[#8B8B9E] font-medium">Threshold</p>
|
||
<p className="font-semibold text-[#F0F0F3] mt-1">{covenant.threshold}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-[#8B8B9E] font-medium">Current Value</p>
|
||
<p className="font-semibold text-[#F0F0F3] mt-1">{covenant.currentValue}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-[#8B8B9E] font-medium">Headroom</p>
|
||
<p className="font-semibold text-[#F0F0F3] mt-1">{covenant.headroom}</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-xs text-[#8B8B9E] mt-3">
|
||
Last checked: {formatDate(covenant.lastChecked)}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|